Browse Source

Adds ability to close (hide) repositories in the GitLens explorer

main
Eric Amodio 6 years ago
parent
commit
fe52312d4a
11 changed files with 186 additions and 114 deletions
  1. +1
    -0
      CHANGELOG.md
  2. +19
    -0
      package.json
  3. +69
    -50
      src/git/models/repository.ts
  4. +17
    -4
      src/gitService.ts
  5. +4
    -2
      src/system/iterable.ts
  6. +57
    -52
      src/system/searchTree.ts
  7. +1
    -1
      src/views/activeRepositoryNode.ts
  8. +9
    -0
      src/views/explorerCommands.ts
  9. +7
    -4
      src/views/gitExplorer.ts
  10. +1
    -0
      src/views/repositoriesNode.ts
  11. +1
    -1
      src/views/statusNode.ts

+ 1
- 0
CHANGELOG.md View File

@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
## [Unreleased]
### Added
- Adds (re-adds) support for handling single files — closes [#321](https://github.com/eamodio/vscode-gitlens/issues/321)
- Adds *Close Repository* (`gitlens.explorers.closeRepository`) command to repository and repository status nodes in the *GitLens* explorer — closes (hides) the repository in the *GitLens* explorer
### Fixed
- Fixes [#384](https://github.com/eamodio/vscode-gitlens/issues/384) - Absolute dates not always honored in GitLens Results explorer

+ 19
- 0
package.json View File

@ -1595,6 +1595,11 @@
"category": "GitLens"
},
{
"command": "gitlens.explorers.closeRepository",
"title": "Close Repository",
"category": "GitLens"
},
{
"command": "gitlens.explorers.compareAncestryWithWorking",
"title": "Compare Ancestry with Working Tree",
"category": "GitLens"
@ -2151,6 +2156,10 @@
"when": "false"
},
{
"command": "gitlens.explorers.closeRepository",
"when": "false"
},
{
"command": "gitlens.explorers.compareAncestryWithWorking",
"when": "false"
},
@ -2889,6 +2898,11 @@
"group": "1_gitlens@1"
},
{
"command": "gitlens.explorers.closeRepository",
"when": "viewItem == gitlens:status",
"group": "8_gitlens@1"
},
{
"command": "gitlens.openBranchesInRemote",
"when": "viewItem == gitlens:remote",
"group": "1_gitlens@1"
@ -2909,6 +2923,11 @@
"group": "1_gitlens@1"
},
{
"command": "gitlens.explorers.closeRepository",
"when": "viewItem == gitlens:repository",
"group": "8_gitlens@1"
},
{
"command": "gitlens.resultsExplorer.swapComparision",
"when": "viewItem == gitlens:results:comparison",
"group": "inline@1"

+ 69
- 50
src/git/models/repository.ts View File

@ -10,6 +10,7 @@ import * as _path from 'path';
export enum RepositoryChange {
Config = 'config',
Closed = 'closed',
// FileSystem = 'file-system',
Remotes = 'remotes',
Repository = 'repository',
@ -83,8 +84,9 @@ export class Repository extends Disposable {
public readonly folder: WorkspaceFolder,
public readonly path: string,
public readonly root: boolean,
private readonly onAnyRepositoryChanged: (repo: Repository) => void,
suspended: boolean
private readonly onAnyRepositoryChanged: (repo: Repository, reason: RepositoryChange) => void,
suspended: boolean,
closed: boolean = false
) {
super(() => this.dispose());
@ -103,7 +105,9 @@ export class Repository extends Disposable {
this.normalizedPath = (this.path.endsWith('/') ? this.path : `${this.path}/`).toLowerCase();
this._suspended = suspended;
this._closed = closed;
// TODO: createFileSystemWatcher doesn't work unless the folder is part of the workspaceFolders
const watcher = workspace.createFileSystemWatcher(new RelativePattern(folder, '{**/.git/config,**/.git/index,**/.git/HEAD,**/.git/refs/stash,**/.git/refs/heads/**,**/.git/refs/remotes/**,**/.git/refs/tags/**,**/.gitignore}'));
this._disposable = Disposable.from(
watcher,
@ -178,59 +182,21 @@ export class Repository extends Disposable {
return;
}
this.onAnyRepositoryChanged(this);
this.onAnyRepositoryChanged(this, RepositoryChange.Repository);
this.fireChange(RepositoryChange.Repository);
}
private fireChange(...reasons: RepositoryChange[]) {
if (this._fireChangeDebounced === undefined) {
this._fireChangeDebounced = Functions.debounce(this.fireChangeCore, 250);
}
if (this._pendingChanges.repo === undefined) {
this._pendingChanges.repo = new RepositoryChangeEvent(this);
}
const e = this._pendingChanges.repo;
for (const reason of reasons) {
if (!e.changes.includes(reason)) {
e.changes.push(reason);
}
}
if (this._suspended) return;
this._fireChangeDebounced(e);
}
private fireChangeCore(e: RepositoryChangeEvent) {
this._pendingChanges.repo = undefined;
this._onDidChange.fire(e);
private _closed: boolean = false;
get closed(): boolean {
return this._closed;
}
private fireFileSystemChange(uri: Uri) {
if (this._fireFileSystemChangeDebounced === undefined) {
this._fireFileSystemChangeDebounced = Functions.debounce(this.fireFileSystemChangeCore, 2500);
set closed(value: boolean) {
const changed = this._closed !== value;
this._closed = value;
if (changed) {
this.onAnyRepositoryChanged(this, RepositoryChange.Closed);
this.fireChange(RepositoryChange.Closed);
}
if (this._pendingChanges.fs === undefined) {
this._pendingChanges.fs = { repository: this, uris: [] };
}
const e = this._pendingChanges.fs;
e.uris.push(uri);
if (this._suspended) return;
this._fireFileSystemChangeDebounced(e);
}
private fireFileSystemChangeCore(e: RepositoryFileSystemChangeEvent) {
this._pendingChanges.fs = undefined;
this._onDidChangeFileSystem.fire(e);
}
containsUri(uri: Uri) {
@ -313,6 +279,7 @@ export class Repository extends Disposable {
this._fsWatchCounter++;
if (this._fsWatcherDisposable !== undefined) return;
// TODO: createFileSystemWatcher doesn't work unless the folder is part of the workspaceFolders
const watcher = workspace.createFileSystemWatcher(new RelativePattern(this.folder, `**`));
this._fsWatcherDisposable = Disposable.from(
watcher,
@ -333,4 +300,56 @@ export class Repository extends Disposable {
suspend() {
this._suspended = true;
}
private fireChange(...reasons: RepositoryChange[]) {
if (this._fireChangeDebounced === undefined) {
this._fireChangeDebounced = Functions.debounce(this.fireChangeCore, 250);
}
if (this._pendingChanges.repo === undefined) {
this._pendingChanges.repo = new RepositoryChangeEvent(this);
}
const e = this._pendingChanges.repo;
for (const reason of reasons) {
if (!e.changes.includes(reason)) {
e.changes.push(reason);
}
}
if (this._suspended) return;
this._fireChangeDebounced(e);
}
private fireChangeCore(e: RepositoryChangeEvent) {
this._pendingChanges.repo = undefined;
this._onDidChange.fire(e);
}
private fireFileSystemChange(uri: Uri) {
if (this._fireFileSystemChangeDebounced === undefined) {
this._fireFileSystemChangeDebounced = Functions.debounce(this.fireFileSystemChangeCore, 2500);
}
if (this._pendingChanges.fs === undefined) {
this._pendingChanges.fs = { repository: this, uris: [] };
}
const e = this._pendingChanges.fs;
e.uris.push(uri);
if (this._suspended) return;
this._fireFileSystemChangeDebounced(e);
}
private fireFileSystemChangeCore(e: RepositoryFileSystemChangeEvent) {
this._pendingChanges.fs = undefined;
this._onDidChangeFileSystem.fire(e);
}
}

+ 17
- 4
src/gitService.ts View File

@ -5,7 +5,7 @@ import { configuration, IRemotesConfig } from './configuration';
import { CommandContext, DocumentSchemes, setCommandContext } from './constants';
import { Container } from './container';
import { RemoteProviderFactory, RemoteProviderMap } from './git/remotes/factory';
import { CommitFormatting, Git, GitAuthor, GitBlame, GitBlameCommit, GitBlameLine, GitBlameLines, GitBlameParser, GitBranch, GitBranchParser, GitCommit, GitCommitType, GitDiff, GitDiffChunkLine, GitDiffParser, GitDiffShortStat, GitLog, GitLogCommit, GitLogParser, GitRemote, GitRemoteParser, GitStash, GitStashParser, GitStatus, GitStatusFile, GitStatusParser, GitTag, GitTagParser, IGit, Repository } from './git/git';
import { CommitFormatting, Git, GitAuthor, GitBlame, GitBlameCommit, GitBlameLine, GitBlameLines, GitBlameParser, GitBranch, GitBranchParser, GitCommit, GitCommitType, GitDiff, GitDiffChunkLine, GitDiffParser, GitDiffShortStat, GitLog, GitLogCommit, GitLogParser, GitRemote, GitRemoteParser, GitStash, GitStashParser, GitStatus, GitStatusFile, GitStatusParser, GitTag, GitTagParser, IGit, Repository, RepositoryChange } from './git/git';
import { CachedBlame, CachedDiff, CachedLog, GitDocumentState, TrackedDocument } from './trackers/gitDocumentTracker';
import { GitUri, IGitCommitInfo } from './git/gitUri';
import { Logger } from './logger';
@ -75,8 +75,17 @@ export class GitService extends Disposable {
return Container.config.advanced.caching.enabled;
}
private onAnyRepositoryChanged(repo: Repository) {
private onAnyRepositoryChanged(repo: Repository, reason: RepositoryChange) {
this._trackedCache.clear();
if (reason === RepositoryChange.Closed) {
// Send a notification that the repositories changed
setImmediate(async () => {
await this.updateContext(this._repositoryTree);
this.fireRepositoriesChanged();
});
}
}
private onConfigurationChanged(e: ConfigurationChangeEvent) {
@ -1248,9 +1257,13 @@ export class GitService extends Disposable {
return Container.git.getActiveRepoPath(editor);
}
async getRepositories(): Promise<Iterable<Repository>> {
async getRepositories(predicate?: (repo: Repository) => boolean): Promise<Iterable<Repository>> {
const repositoryTree = await this.getRepositoryTree();
return repositoryTree.values();
const values = repositoryTree.values();
return predicate !== undefined
? Iterables.filter(values, predicate)
: values;
}
private async getRepositoryTree(): Promise<TernarySearchTree<Repository>> {

+ 4
- 2
src/system/iterable.ts View File

@ -1,7 +1,7 @@
'use strict';
export namespace Iterables {
export function count<T>(source: Iterable<T> | IterableIterator<T>): number {
export function count<T>(source: Iterable<T> | IterableIterator<T>, predicate?: (item: T) => boolean): number {
let count = 0;
let next: IteratorResult<T>;
@ -9,7 +9,9 @@ export namespace Iterables {
next = (source as IterableIterator<T>).next();
if (next.done) break;
count++;
if (predicate === undefined || predicate(next.value)) {
count++;
}
}
return count;

+ 57
- 52
src/system/searchTree.ts View File

@ -6,7 +6,6 @@ import { Iterables } from '../system/iterable';
export interface IKeyIterator {
reset(key: string): this;
next(): this;
join(parts: string[]): string;
hasNext(): boolean;
cmp(a: string): number;
@ -29,10 +28,6 @@ export class StringIterator implements IKeyIterator {
return this;
}
join(parts: string[]): string {
return parts.join('');
}
hasNext(): boolean {
return this._pos < this._value.length - 1;
}
@ -48,10 +43,18 @@ export class StringIterator implements IKeyIterator {
}
}
export class PathIterator implements IKeyIterator {
const enum CharCode {
/**
* The `/` character.
*/
Slash = 47,
/**
* The `\` character.
*/
Backslash = 92
}
private static readonly _fwd = '/'.charCodeAt(0);
private static readonly _bwd = '\\'.charCodeAt(0);
export class PathIterator implements IKeyIterator {
private _value!: string;
private _from!: number;
@ -68,17 +71,13 @@ export class PathIterator implements IKeyIterator {
return this._to < this._value.length;
}
join(parts: string[]): string {
return parts.join('/');
}
next(): this {
// this._data = key.split(/[\\/]/).filter(s => !!s);
this._from = this._to;
let justSeps = true;
for (; this._to < this._value.length; this._to++) {
const ch = this._value.charCodeAt(this._to);
if (ch === PathIterator._fwd || ch === PathIterator._bwd) {
if (ch === CharCode.Slash || ch === CharCode.Backslash) {
if (justSeps) {
this._from++;
} else {
@ -121,14 +120,15 @@ export class PathIterator implements IKeyIterator {
}
class TernarySearchTreeNode<E> {
str!: string;
element: E | undefined;
segment!: string;
value: E | undefined;
key!: string;
left: TernarySearchTreeNode<E> | undefined;
mid: TernarySearchTreeNode<E> | undefined;
right: TernarySearchTreeNode<E> | undefined;
isEmpty(): boolean {
return this.left === undefined && this.mid === undefined && this.right === undefined && this.element === undefined;
return !this.left && !this.mid && !this.right && !this.value;
}
}
@ -159,17 +159,17 @@ export class TernarySearchTree {
if (!this._root) {
this._root = new TernarySearchTreeNode<E>();
this._root.str = iter.value();
this._root.segment = iter.value();
}
node = this._root;
while (true) {
const val = iter.cmp(node.str);
const val = iter.cmp(node.segment);
if (val > 0) {
// left
if (!node.left) {
node.left = new TernarySearchTreeNode<E>();
node.left.str = iter.value();
node.left.segment = iter.value();
}
node = node.left;
@ -177,7 +177,7 @@ export class TernarySearchTree {
// right
if (!node.right) {
node.right = new TernarySearchTreeNode<E>();
node.right.str = iter.value();
node.right.segment = iter.value();
}
node = node.right;
@ -186,15 +186,16 @@ export class TernarySearchTree {
iter.next();
if (!node.mid) {
node.mid = new TernarySearchTreeNode<E>();
node.mid.str = iter.value();
node.mid.segment = iter.value();
}
node = node.mid;
} else {
break;
}
}
const oldElement = node.element;
node.element = element;
const oldElement = node.value;
node.value = element;
node.key = key;
return oldElement;
}
@ -202,7 +203,7 @@ export class TernarySearchTree {
const iter = this._iter.reset(key);
let node = this._root;
while (node) {
const val = iter.cmp(node.str);
const val = iter.cmp(node.segment);
if (val > 0) {
// left
node = node.left;
@ -217,7 +218,7 @@ export class TernarySearchTree {
break;
}
}
return node ? node.element : undefined;
return node ? node.value : undefined;
}
delete(key: string): void {
@ -227,7 +228,7 @@ export class TernarySearchTree {
// find and unset node
while (node) {
const val = iter.cmp(node.str);
const val = iter.cmp(node.segment);
if (val > 0) {
// left
stack.push([1, node]);
@ -243,7 +244,7 @@ export class TernarySearchTree {
node = node.mid;
} else {
// remove element
node.element = undefined;
node.value = undefined;
// clean up empty nodes
while (stack.length > 0 && node.isEmpty()) {
@ -265,7 +266,7 @@ export class TernarySearchTree {
let node = this._root;
let candidate: E | undefined;
while (node) {
const val = iter.cmp(node.str);
const val = iter.cmp(node.segment);
if (val > 0) {
// left
node = node.left;
@ -275,20 +276,20 @@ export class TernarySearchTree {
} else if (iter.hasNext()) {
// mid
iter.next();
candidate = node.element || candidate;
candidate = node.value || candidate;
node = node.mid;
} else {
break;
}
}
return node && node.element || candidate;
return node && node.value || candidate;
}
findSuperstr(key: string): TernarySearchTree<E> | undefined {
const iter = this._iter.reset(key);
let node = this._root;
while (node) {
const val = iter.cmp(node.str);
const val = iter.cmp(node.segment);
if (val > 0) {
// left
node = node.left;
@ -313,44 +314,43 @@ export class TernarySearchTree {
}
forEach(callback: (value: E, index: string) => any) {
this._forEach(this._root!, [], callback);
this._forEach(this._root!, callback);
}
private _forEach(node: TernarySearchTreeNode<E>, parts: string[], callback: (value: E, index: string) => any) {
private _forEach(node: TernarySearchTreeNode<E>, callback: (value: E, index: string) => any) {
if (node === undefined) return;
// left
this._forEach(node.left!, parts, callback);
this._forEach(node.left!, callback);
// node
parts.push(node.str);
if (node.element) {
callback(node.element, this._iter.join(parts));
if (node.value) {
callback(node.value, node.key);
}
// mid
this._forEach(node.mid!, parts, callback);
parts.pop();
this._forEach(node.mid!, callback);
// right
this._forEach(node.right!, parts, callback);
this._forEach(node.right!, callback);
}
any(): boolean {
return this._root !== undefined && !this._root.isEmpty();
}
count(): number {
count(predicate?: (entry: E) => boolean): number {
if (this._root === undefined || this._root.isEmpty()) return 0;
return Iterables.count(this.entries());
return Iterables.count(this.entries(), predicate === undefined ? undefined : ([e]) => predicate(e));
}
entries(): Iterable<[E, string]> {
return this._iterator(this._root!, []);
return this._iterator(this._root!);
}
values(): Iterable<E> {
return Iterables.map(this.entries(), e => e[0]);
return Iterables.map(this.entries(), ([e]) => e);
}
highlander(): [E, string] | undefined {
@ -375,22 +375,27 @@ export class TernarySearchTree {
return value;
}
private *_iterator(node: TernarySearchTreeNode<E> | undefined, parts: string[]): IterableIterator<[E, string]> {
some(predicate: (entry: E) => boolean): boolean {
if (this._root === undefined || this._root.isEmpty()) return false;
return Iterables.some(this.entries(), ([e]) => predicate(e));
}
private *_iterator(node: TernarySearchTreeNode<E> | undefined): IterableIterator<[E, string]> {
if (node !== undefined) {
// left
yield* this._iterator(node.left!, parts);
yield* this._iterator(node.left!);
// node
parts.push(node.str);
if (node.element) {
yield [node.element, this._iter.join(parts)];
if (node.value) {
yield [node.value, node.key];
}
// mid
yield* this._iterator(node.mid!, parts);
parts.pop();
yield* this._iterator(node.mid!);
// right
yield* this._iterator(node.right!, parts);
yield* this._iterator(node.right!);
}
}
}

+ 1
- 1
src/views/activeRepositoryNode.ts View File

@ -58,7 +58,7 @@ export class ActiveRepositoryNode extends ExplorerNode {
if (this._repositoryNode !== undefined && this._repositoryNode.repo.path === repoPath) return;
const repo = await Container.git.getRepository(repoPath);
if (repo === undefined) {
if (repo === undefined || repo.closed) {
if (this._repositoryNode !== undefined) {
changed = true;

+ 9
- 0
src/views/explorerCommands.ts View File

@ -6,6 +6,8 @@ import { BranchNode, ExplorerNode, TagNode } from '../views/gitExplorer';
import { CommitFileNode, CommitNode, ExplorerRefNode, RemoteNode, StashFileNode, StashNode, StatusFileCommitsNode, StatusUpstreamNode } from './explorerNodes';
import { Commands, DiffWithCommandArgs, DiffWithCommandArgsRevision, DiffWithPreviousCommandArgs, DiffWithWorkingCommandArgs, openEditor, OpenFileInRemoteCommandArgs, OpenFileRevisionCommandArgs } from '../commands';
import { GitService, GitUri } from '../gitService';
import { RepositoryNode } from './repositoryNode';
import { StatusNode } from './statusNode';
export interface RefreshNodeCommandArgs {
maxCount?: number;
@ -36,6 +38,7 @@ export class ExplorerCommands extends Disposable {
commands.registerCommand('gitlens.explorers.openChangedFileChangesWithWorking', this.openChangedFileChangesWithWorking, this);
commands.registerCommand('gitlens.explorers.openChangedFileRevisions', this.openChangedFileRevisions, this);
commands.registerCommand('gitlens.explorers.applyChanges', this.applyChanges, this);
commands.registerCommand('gitlens.explorers.closeRepository', this.closeRepository, this);
commands.registerCommand('gitlens.explorers.compareAncestryWithWorking', this.compareAncestryWithWorking, this);
commands.registerCommand('gitlens.explorers.compareWithHead', this.compareWithHead, this);
commands.registerCommand('gitlens.explorers.compareWithRemote', this.compareWithRemote, this);
@ -68,6 +71,12 @@ export class ExplorerCommands extends Disposable {
return this.openFile(node);
}
private closeRepository(node: RepositoryNode | StatusNode) {
if (!(node instanceof RepositoryNode) && !(node instanceof StatusNode)) return;
node.repo.closed = true;
}
private compareWithHead(node: ExplorerNode) {
if (!(node instanceof ExplorerRefNode)) return;

+ 7
- 4
src/views/gitExplorer.ts View File

@ -330,14 +330,17 @@ export class GitExplorer extends Disposable implements TreeDataProvider
this._loading = promise.then(_ => Functions.wait(0));
const repositories = [...await promise];
if (repositories.length === 0) return undefined; // new MessageNode('No repositories found');
if (repositories.length === 0) return undefined;
if (repositories.length === 1) {
const repo = repositories[0];
const openedRepos = repositories.filter(r => !r.closed);
if (openedRepos.length === 0) return undefined;
if (openedRepos.length === 1) {
const repo = openedRepos[0];
return new RepositoryNode(GitUri.fromRepoPath(repo.path), repo, this, true);
}
return new RepositoriesNode(repositories, this);
return new RepositoriesNode(openedRepos, this);
}
}
}

+ 1
- 0
src/views/repositoriesNode.ts View File

@ -19,6 +19,7 @@ export class RepositoriesNode extends ExplorerNode {
if (this.children === undefined) {
this.children = this.repositories
.sort((a, b) => a.index - b.index)
.filter(repo => !repo.closed)
.map(repo => new RepositoryNode(GitUri.fromRepoPath(repo.path), repo, this.explorer));
if (this.children.length > 1) {

+ 1
- 1
src/views/statusNode.ts View File

@ -11,7 +11,7 @@ export class StatusNode extends ExplorerNode {
constructor(
uri: GitUri,
private readonly repo: Repository,
public readonly repo: Repository,
private readonly explorer: GitExplorer,
private readonly active: boolean = false
) {

Loading…
Cancel
Save