From 183c7e79d2f78cd8f0b1cd8e94ce7ebf42af60dc Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Fri, 20 Oct 2017 23:02:39 -0400 Subject: [PATCH] Adds multi-root support --- src/commands/closeUnchangedFiles.ts | 2 +- src/commands/diffDirectory.ts | 2 +- src/commands/externalDiff.ts | 2 +- src/commands/openChangedFiles.ts | 2 +- src/commands/showQuickCurrentBranchHistory.ts | 2 +- src/commands/showQuickRepoStatus.ts | 2 +- src/commands/showQuickStashList.ts | 2 +- src/constants.ts | 2 +- src/extension.ts | 7 +- src/git/git.ts | 67 ++++++++------- src/git/gitContextTracker.ts | 2 - src/git/gitUri.ts | 4 +- src/gitService.ts | 114 ++++++++++++++++++++------ src/quickPicks/branchHistory.ts | 2 +- src/quickPicks/commitFileDetails.ts | 2 +- src/views/explorerNode.ts | 1 + src/views/explorerNodes.ts | 1 + src/views/gitExplorer.ts | 86 +++++++++++++------ src/views/repositoriesNode.ts | 30 +++++++ src/views/repositoryNode.ts | 6 +- 20 files changed, 239 insertions(+), 99 deletions(-) create mode 100644 src/views/repositoriesNode.ts diff --git a/src/commands/closeUnchangedFiles.ts b/src/commands/closeUnchangedFiles.ts index 046917b..fd7603b 100644 --- a/src/commands/closeUnchangedFiles.ts +++ b/src/commands/closeUnchangedFiles.ts @@ -25,7 +25,7 @@ export class CloseUnchangedFilesCommand extends ActiveEditorCommand { if (args.uris === undefined) { args = { ...args }; - const repoPath = await this.git.getRepoPathFromUri(uri); + const repoPath = await this.git.getRepoPath(uri); if (!repoPath) return Messages.showNoRepositoryWarningMessage(`Unable to close unchanged files`); const status = await this.git.getStatusForRepo(repoPath); diff --git a/src/commands/diffDirectory.ts b/src/commands/diffDirectory.ts index 9910006..2ea57ae 100644 --- a/src/commands/diffDirectory.ts +++ b/src/commands/diffDirectory.ts @@ -41,7 +41,7 @@ export class DiffDirectoryCommand extends ActiveEditorCommand { uri = getCommandUri(uri, editor); try { - const repoPath = await this.git.getRepoPathFromUri(uri); + const repoPath = await this.git.getRepoPath(uri); if (!repoPath) return Messages.showNoRepositoryWarningMessage(`Unable to open directory compare`); if (!args.shaOrBranch1) { diff --git a/src/commands/externalDiff.ts b/src/commands/externalDiff.ts index 3b9ee73..719e7ed 100644 --- a/src/commands/externalDiff.ts +++ b/src/commands/externalDiff.ts @@ -85,7 +85,7 @@ export class ExternalDiffCommand extends Command { return commands.executeCommand(BuiltInCommands.Open, Uri.parse('https://git-scm.com/docs/git-config#git-config-difftool')); } - const repoPath = await this.git.getRepoPathFromUri(undefined); + const repoPath = await this.git.getRepoPath(undefined); if (!repoPath) return Messages.showNoRepositoryWarningMessage(`Unable to open changed files`); if (args.files === undefined) { diff --git a/src/commands/openChangedFiles.ts b/src/commands/openChangedFiles.ts index cd6355e..ebaeaf6 100644 --- a/src/commands/openChangedFiles.ts +++ b/src/commands/openChangedFiles.ts @@ -22,7 +22,7 @@ export class OpenChangedFilesCommand extends ActiveEditorCommand { if (args.uris === undefined) { args = { ...args }; - const repoPath = await this.git.getRepoPathFromUri(uri); + const repoPath = await this.git.getRepoPath(uri); if (!repoPath) return Messages.showNoRepositoryWarningMessage(`Unable to open changed files`); const status = await this.git.getStatusForRepo(repoPath); diff --git a/src/commands/showQuickCurrentBranchHistory.ts b/src/commands/showQuickCurrentBranchHistory.ts index 19053db..d4d256a 100644 --- a/src/commands/showQuickCurrentBranchHistory.ts +++ b/src/commands/showQuickCurrentBranchHistory.ts @@ -21,7 +21,7 @@ export class ShowQuickCurrentBranchHistoryCommand extends ActiveEditorCachedComm uri = getCommandUri(uri, editor); try { - const repoPath = await this.git.getRepoPathFromUri(uri); + const repoPath = await this.git.getRepoPath(uri); if (!repoPath) return Messages.showNoRepositoryWarningMessage(`Unable to show branch history`); const branch = await this.git.getBranch(repoPath); diff --git a/src/commands/showQuickRepoStatus.ts b/src/commands/showQuickRepoStatus.ts index 78b0722..140eadc 100644 --- a/src/commands/showQuickRepoStatus.ts +++ b/src/commands/showQuickRepoStatus.ts @@ -20,7 +20,7 @@ export class ShowQuickRepoStatusCommand extends ActiveEditorCachedCommand { uri = getCommandUri(uri, editor); try { - const repoPath = await this.git.getRepoPathFromUri(uri); + const repoPath = await this.git.getRepoPath(uri); if (!repoPath) return Messages.showNoRepositoryWarningMessage(`Unable to show repository status`); const status = await this.git.getStatusForRepo(repoPath); diff --git a/src/commands/showQuickStashList.ts b/src/commands/showQuickStashList.ts index d031a64..8dcc84f 100644 --- a/src/commands/showQuickStashList.ts +++ b/src/commands/showQuickStashList.ts @@ -23,7 +23,7 @@ export class ShowQuickStashListCommand extends ActiveEditorCachedCommand { uri = getCommandUri(uri, editor); try { - const repoPath = await this.git.getRepoPathFromUri(uri); + const repoPath = await this.git.getRepoPath(uri); if (!repoPath) return Messages.showNoRepositoryWarningMessage(`Unable to show stashed changes`); const stash = await this.git.getStashList(repoPath); diff --git a/src/constants.ts b/src/constants.ts index a49961b..3ddfe8b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -32,8 +32,8 @@ export enum CommandContext { GitExplorerFilesLayout = 'gitlens:gitExplorer:files:layout', GitExplorerView = 'gitlens:gitExplorer:view', HasRemotes = 'gitlens:hasRemotes', + HasRepository = 'gitlens:hasRepository', IsBlameable = 'gitlens:isBlameable', - IsRepository = 'gitlens:isRepository', IsTracked = 'gitlens:isTracked', Key = 'gitlens:key' } diff --git a/src/extension.ts b/src/extension.ts index 3983521..6547117 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -26,8 +26,7 @@ export async function activate(context: ExtensionContext) { const gitlens = extensions.getExtension(QualifiedExtensionId)!; const gitlensVersion = gitlens.packageJSON.version; - const rootPath = workspace.rootPath && workspace.rootPath.replace(/\\/g, '/'); - Logger.log(`GitLens(v${gitlensVersion}) active: ${rootPath}`); + Logger.log(`GitLens(v${gitlensVersion}) active`); const cfg = workspace.getConfiguration().get(ExtensionKey)!; const gitPath = cfg.advanced.git; @@ -44,8 +43,6 @@ export async function activate(context: ExtensionContext) { return; } - const repoPath = await GitService.getRepoPath(rootPath); - const gitVersion = GitService.getGitVersion(); Logger.log(`Git version: ${gitVersion}`); @@ -60,7 +57,7 @@ export async function activate(context: ExtensionContext) { await context.globalState.update(GlobalState.GitLensVersion, gitlensVersion); - const git = new GitService(repoPath); + const git = new GitService(); context.subscriptions.push(git); const gitContextTracker = new GitContextTracker(git); diff --git a/src/git/git.ts b/src/git/git.ts index abb4070..7ac9204 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -45,11 +45,11 @@ interface GitCommandOptions { cwd: string; env?: any; encoding?: string; - overrideErrorHandling?: boolean; + willHandleErrors?: boolean; } async function gitCommand(options: GitCommandOptions, ...args: any[]): Promise { - if (options.overrideErrorHandling) return gitCommandCore(options, ...args); + if (options.willHandleErrors) return gitCommandCore(options, ...args); try { return await gitCommandCore(options, ...args); @@ -108,15 +108,6 @@ export class Git { return git; } - static async getRepoPath(cwd: string | undefined) { - if (cwd === undefined) return ''; - - const data = await gitCommand({ cwd }, 'rev-parse', '--show-toplevel'); - if (!data) return ''; - - return data.replace(/\r?\n|\r/g, '').replace(/\\/g, '/'); - } - static async getVersionedFile(repoPath: string | undefined, fileName: string, branchOrSha: string) { const data = await Git.show(repoPath, fileName, branchOrSha, 'binary'); if (data === undefined) return undefined; @@ -215,22 +206,6 @@ export class Git { return gitCommand({ cwd: repoPath }, ...params); } - static async branch_current(repoPath: string) { - const params = [`rev-parse`, `--abbrev-ref`, `--symbolic-full-name`, `@`, `@{u}`]; - - const opts = { cwd: repoPath, overrideErrorHandling: true }; - try { - return await gitCommand(opts, ...params); - } - catch (ex) { - if (/no upstream configured for branch/.test(ex && ex.toString())) { - return ex.message.split('\n')[0]; - } - - return gitCommandDefaultErrorHandler(ex, opts, ...params); - } - } - static checkout(repoPath: string, fileName: string, sha: string) { const [file, root] = Git.splitPath(fileName, repoPath); @@ -239,7 +214,8 @@ export class Git { static async config_get(key: string, repoPath?: string) { try { - return await gitCommand({ cwd: repoPath || '', overrideErrorHandling: true }, `config`, `--get`, key); + const data = await gitCommand({ cwd: repoPath || '', willHandleErrors: true }, `config`, `--get`, key); + return data.trim(); } catch { return ''; @@ -372,7 +348,8 @@ export class Git { static async ls_files(repoPath: string, fileName: string): Promise { try { - return await gitCommand({ cwd: repoPath, overrideErrorHandling: true }, 'ls-files', fileName); + const data = await gitCommand({ cwd: repoPath, willHandleErrors: true }, 'ls-files', fileName); + return data.trim(); } catch { return ''; @@ -387,14 +364,42 @@ export class Git { return gitCommand({ cwd: repoPath }, 'remote', 'get-url', remote); } + static async revparse_currentBranch(repoPath: string) { + const params = [`rev-parse`, `--abbrev-ref`, `--symbolic-full-name`, `@`, `@{u}`]; + + const opts = { cwd: repoPath, willHandleErrors: true } as GitCommandOptions; + try { + const data = await gitCommand(opts, ...params); + return data; + } + catch (ex) { + if (/no upstream configured for branch/.test(ex && ex.toString())) { + return ex.message.split('\n')[0]; + } + + return gitCommandDefaultErrorHandler(ex, opts, ...params); + } + } + + static async revparse_toplevel(cwd: string): Promise { + try { + const data = await gitCommand({ cwd: cwd, willHandleErrors: true }, 'rev-parse', '--show-toplevel'); + return data.trim(); + } + catch { + return undefined; + } + } + static async show(repoPath: string | undefined, fileName: string, branchOrSha: string, encoding?: string) { const [file, root] = Git.splitPath(fileName, repoPath); if (Git.isUncommitted(branchOrSha)) throw new Error(`sha=${branchOrSha} is uncommitted`); - const opts = { cwd: root, encoding: encoding || defaultEncoding, overrideErrorHandling: true }; + const opts = { cwd: root, encoding: encoding || defaultEncoding, willHandleErrors: true } as GitCommandOptions; const args = `${branchOrSha}:./${file}`; try { - return await gitCommand(opts, 'show', args); + const data = await gitCommand(opts, 'show', args); + return data; } catch (ex) { const msg = ex && ex.toString(); diff --git a/src/git/gitContextTracker.ts b/src/git/gitContextTracker.ts index f4b2c80..c85baf1 100644 --- a/src/git/gitContextTracker.ts +++ b/src/git/gitContextTracker.ts @@ -35,8 +35,6 @@ export class GitContextTracker extends Disposable { ]; this._disposable = Disposable.from(...subscriptions); - setCommandContext(CommandContext.IsRepository, !!this.git.repoPath); - this._onConfigurationChanged(); } diff --git a/src/git/gitUri.ts b/src/git/gitUri.ts index a1612da..5e0530c 100644 --- a/src/git/gitUri.ts +++ b/src/git/gitUri.ts @@ -97,7 +97,7 @@ export class GitUri extends ((Uri as any) as UriEx) { static async fromUri(uri: Uri, git: GitService) { if (uri instanceof GitUri) return uri; - if (!git.isTrackable(uri)) return new GitUri(uri, git.repoPath); + if (!git.isTrackable(uri)) return new GitUri(uri, undefined); if (uri.scheme === DocumentSchemes.GitLensGit) return new GitUri(uri); @@ -110,7 +110,7 @@ export class GitUri extends ((Uri as any) as UriEx) { const gitUri = git.getGitUriForFile(uri); if (gitUri) return gitUri; - return new GitUri(uri, (await git.getRepoPathFromFile(uri.fsPath)) || git.repoPath); + return new GitUri(uri, await git.getRepoPath(uri.fsPath)); } static fromFileStatus(status: IGitStatusFile, repoPath: string, original?: boolean): GitUri; diff --git a/src/gitService.ts b/src/gitService.ts index 1a29590..ac6eafb 100644 --- a/src/gitService.ts +++ b/src/gitService.ts @@ -1,8 +1,8 @@ 'use strict'; import { Functions, Iterables, Objects } from './system'; -import { Disposable, Event, EventEmitter, FileSystemWatcher, Range, TextDocument, TextDocumentChangeEvent, TextEditor, Uri, window, WindowState, workspace } from 'vscode'; +import { Disposable, Event, EventEmitter, FileSystemWatcher, Range, TextDocument, TextDocumentChangeEvent, TextEditor, Uri, window, WindowState, workspace, WorkspaceFoldersChangeEvent } from 'vscode'; import { IConfig } from './configuration'; -import { DocumentSchemes, ExtensionKey } from './constants'; +import { CommandContext, DocumentSchemes, ExtensionKey, setCommandContext } from './constants'; import { RemoteProviderFactory } from './git/remotes/factory'; import { 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, IGit, setDefaultEncoding } from './git/git'; import { GitUri, IGitCommitInfo, IGitUriData } from './git/gitUri'; @@ -51,6 +51,12 @@ interface CachedBlame extends CachedItem { } interface CachedDiff extends CachedItem { } interface CachedLog extends CachedItem { } +export interface Repository { + readonly name: string; + readonly index: number; + readonly path: string; +} + enum RemoveCacheReason { DocumentClosed, DocumentSaved @@ -65,6 +71,7 @@ export enum GitRepoSearchBy { export enum RepoChangedReasons { Remotes = 'remotes', + Repositories = 'Repositories', Stash = 'stash', Unknown = '' } @@ -102,24 +109,29 @@ export class GitService extends Disposable { private _focused: boolean = true; private _gitCache: Map; private _remotesCache: Map; + private _repositories: Map; + private _repositoriesPromise: Promise | undefined; private _repoWatcher: FileSystemWatcher | undefined; private _trackedCache: Map; private _unfocusedChanges: { repo: boolean, fs: boolean } = { repo: false, fs: false }; private _versionedUriCache: Map; - constructor(public repoPath: string) { + constructor() { super(() => this.dispose()); this._gitCache = new Map(); this._remotesCache = new Map(); + this._repositories = new Map(); this._trackedCache = new Map(); this._versionedUriCache = new Map(); this.onConfigurationChanged(); + this._repositoriesPromise = this.onWorkspaceFoldersChanged(); const subscriptions: Disposable[] = [ window.onDidChangeWindowState(this.onWindowStateChanged, this), workspace.onDidChangeConfiguration(this.onConfigurationChanged, this), + workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this), RemoteProviderFactory.onDidChange(this.onRemoteProviderChanged, this) ]; this._disposable = Disposable.from(...subscriptions); @@ -142,6 +154,22 @@ export class GitService extends Disposable { this._versionedUriCache.clear(); } + public get repoPath(): string | undefined { + if (this._repositories.size !== 1) return undefined; + + const repo = Iterables.first(this._repositories.values()); + return repo === undefined ? undefined : repo.path; + } + + public async getRepositories(): Promise { + if (this._repositoriesPromise !== undefined) { + await this._repositoriesPromise; + this._repositoriesPromise = undefined; + } + + return [...Iterables.filter(this._repositories.values(), r => r !== undefined) as Iterable]; + } + public get UseCaching() { return this.config.advanced.caching.enabled; } @@ -212,6 +240,43 @@ export class GitService extends Disposable { } } + private async onWorkspaceFoldersChanged(e?: WorkspaceFoldersChangeEvent) { + let initializing = false; + if (e === undefined) { + if (workspace.workspaceFolders === undefined) return; + + initializing = true; + e = { + added: workspace.workspaceFolders, + removed: [] + } as WorkspaceFoldersChangeEvent; + } + + for (const f of e.added) { + if (f.uri.scheme !== DocumentSchemes.File) continue; + + const fsPath = f.uri.fsPath; + const rp = await this.getRepoPathCore(fsPath, true); + if (rp === undefined) { + Logger.log(`onWorkspaceFoldersChanged(${fsPath})`, 'No repository found'); + } + this._repositories.set(fsPath, { name: f.name, index: f.index, path: rp } as Repository); + } + + for (const f of e.removed) { + if (f.uri.scheme !== DocumentSchemes.File) continue; + + this._repositories.delete(f.uri.fsPath); + } + + const hasRepository = Iterables.some(this._repositories.values(), rp => rp !== undefined); + await setCommandContext(CommandContext.HasRepository, hasRepository); + + if (!initializing) { + this.fireRepoChange(RepoChangedReasons.Repositories); + } + } + private onTextDocumentChanged(e: TextDocumentChangeEvent) { if (!this.UseCaching) return; if (e.document.uri.scheme !== DocumentSchemes.File) return; @@ -571,7 +636,7 @@ export class GitService extends Disposable { Logger.log(`getBranch('${repoPath}')`); if (repoPath === undefined) return undefined; - const data = await Git.branch_current(repoPath); + const data = await Git.revparse_currentBranch(repoPath); const branch = data.split('\n'); return new GitBranch(repoPath, branch[0], true, branch[1]); } @@ -900,24 +965,27 @@ export class GitService extends Disposable { return remotes; } - getRepoPath(cwd: string): Promise { - return GitService.getRepoPath(cwd); - } - - async getRepoPathFromFile(fileName: string): Promise { - const log = await this.getLogForFile(undefined, fileName, undefined, { maxCount: 1 }); - if (log === undefined) return undefined; + async getRepoPath(filePath: string): Promise; + async getRepoPath(uri: Uri | undefined): Promise; + async getRepoPath(filePathOrUri: string | Uri | undefined): Promise { + if (filePathOrUri === undefined) return this.repoPath; + if (filePathOrUri instanceof GitUri) return filePathOrUri.repoPath; - return log.repoPath; - } + if (typeof filePathOrUri === 'string') return this.getRepoPathCore(filePathOrUri, false); - async getRepoPathFromUri(uri: Uri | undefined): Promise { - if (!(uri instanceof Uri)) return this.repoPath; + const folder = workspace.getWorkspaceFolder(filePathOrUri); + if (folder !== undefined) { + const rp = this._repositories.get(folder.uri.fsPath); + if (rp !== undefined) return rp.path; + } - const repoPath = (await GitUri.fromUri(uri, this)).repoPath; - if (!repoPath) return this.repoPath; + return this.getRepoPathCore(filePathOrUri.fsPath, false); + } - return repoPath; + private async getRepoPathCore(filePath: string, isDirectory: boolean): Promise { + const rp = await Git.revparse_toplevel(isDirectory ? filePath : path.dirname(filePath)); + console.log(filePath, rp); + return rp; } async getStashList(repoPath: string | undefined): Promise { @@ -1103,12 +1171,12 @@ export class GitService extends Disposable { return Git.gitInfo().version; } - static async getRepoPath(cwd: string | undefined): Promise { - const repoPath = await Git.getRepoPath(cwd); - if (!repoPath) return ''; + // static async getRepoPath(cwd: string | undefined): Promise { + // const repoPath = await Git.getRepoPath(cwd); + // if (!repoPath) return ''; - return repoPath; - } + // return repoPath; + // } static fromGitContentUri(uri: Uri): IGitUriData { if (uri.scheme !== DocumentSchemes.GitLensGit) throw new Error(`fromGitUri(uri=${uri}) invalid scheme`); diff --git a/src/quickPicks/branchHistory.ts b/src/quickPicks/branchHistory.ts index 77345c3..27aad34 100644 --- a/src/quickPicks/branchHistory.ts +++ b/src/quickPicks/branchHistory.ts @@ -35,7 +35,7 @@ export class BranchHistoryQuickPick { } as ShowQuickBranchHistoryCommandArgs ]); - const remotes = (await git.getRemotes((uri && uri.repoPath) || git.repoPath)).filter(r => r.provider !== undefined); + const remotes = (await git.getRemotes((uri && uri.repoPath) || log.repoPath)).filter(r => r.provider !== undefined); if (remotes.length) { items.splice(0, 0, new OpenRemotesCommandQuickPickItem(remotes, { type: 'branch', diff --git a/src/quickPicks/commitFileDetails.ts b/src/quickPicks/commitFileDetails.ts index 01eb35a..ede08fc 100644 --- a/src/quickPicks/commitFileDetails.ts +++ b/src/quickPicks/commitFileDetails.ts @@ -127,7 +127,7 @@ export class CommitFileDetailsQuickPick { const remotes = (await git.getRemotes(commit.repoPath)).filter(r => r.provider !== undefined); if (remotes.length) { if (commit.workingFileName && commit.status !== 'D') { - const branch = await git.getBranch(commit.repoPath || git.repoPath); + const branch = await git.getBranch(commit.repoPath); items.push(new OpenRemotesCommandQuickPickItem(remotes, { type: 'file', fileName: commit.workingFileName, diff --git a/src/views/explorerNode.ts b/src/views/explorerNode.ts index 36f6521..c378e4f 100644 --- a/src/views/explorerNode.ts +++ b/src/views/explorerNode.ts @@ -16,6 +16,7 @@ export declare type ResourceType = 'gitlens:pager' | 'gitlens:remote' | 'gitlens:remotes' | + 'gitlens:repositories' | 'gitlens:repository' | 'gitlens:stash' | 'gitlens:stash-file' | diff --git a/src/views/explorerNodes.ts b/src/views/explorerNodes.ts index 7fb4d53..1b3ddd9 100644 --- a/src/views/explorerNodes.ts +++ b/src/views/explorerNodes.ts @@ -9,6 +9,7 @@ export * from './fileHistoryNode'; export * from './historyNode'; export * from './remoteNode'; export * from './remotesNode'; +export * from './repositoriesNode'; export * from './repositoryNode'; export * from './stashesNode'; export * from './stashFileNode'; diff --git a/src/views/gitExplorer.ts b/src/views/gitExplorer.ts index 816d916..6073cd4 100644 --- a/src/views/gitExplorer.ts +++ b/src/views/gitExplorer.ts @@ -5,7 +5,7 @@ import { Commands, DiffWithCommandArgs, DiffWithCommandArgsRevision, DiffWithPre import { UriComparer } from '../comparers'; import { ExtensionKey, GitExplorerFilesLayout, IConfig } from '../configuration'; import { CommandContext, GlyphChars, setCommandContext, WorkspaceState } from '../constants'; -import { BranchHistoryNode, CommitFileNode, CommitNode, ExplorerNode, HistoryNode, MessageNode, RepositoryNode, StashNode } from './explorerNodes'; +import { BranchHistoryNode, CommitFileNode, CommitNode, ExplorerNode, HistoryNode, MessageNode, RepositoriesNode, RepositoryNode, StashNode } from './explorerNodes'; import { GitService, GitUri, RepoChangedReasons } from '../gitService'; export * from './explorerNodes'; @@ -72,43 +72,70 @@ export class GitExplorer implements TreeDataProvider { return node.getTreeItem(); } + private _loading: Promise | undefined; + async getChildren(node?: ExplorerNode): Promise { + if (this._loading !== undefined) { + await this._loading; + this._loading = undefined; + } + if (this._root === undefined) { if (this._view === GitExplorerView.History) return [new MessageNode(`No active file ${GlyphChars.Dash} no history to show`)]; - return []; + return [new MessageNode('No repositories found')]; } if (node === undefined) return this._root.getChildren(); return node.getChildren(); } - private getRootNode(editor?: TextEditor): ExplorerNode | undefined { + private async getRootNode(editor?: TextEditor): Promise { switch (this._view) { - case GitExplorerView.History: - return this.getHistoryNode(editor || window.activeTextEditor); + case GitExplorerView.History: { + const promise = this.getHistoryNode(editor || window.activeTextEditor); + this._loading = promise.then(async _ => await Functions.wait(0)); + return promise; + } + default: { + const promise = this.git.getRepositories(); + this._loading = promise.then(async _ => await Functions.wait(0)); - default: - const uri = new GitUri(Uri.file(this.git.repoPath), { repoPath: this.git.repoPath, fileName: this.git.repoPath }); - return new RepositoryNode(uri, this.context, this.git); + const repositories = await promise; + if (repositories.length === 0) return undefined; // new MessageNode('No repositories found'); + + if (repositories.length === 1) { + const repo = repositories[0]; + return new RepositoryNode(new GitUri(Uri.file(repo.path), { repoPath: repo.path, fileName: repo.path }), repo, this.context, this.git); + } + + return new RepositoriesNode(repositories, this.context, this.git); + } } } - private getHistoryNode(editor: TextEditor | undefined): ExplorerNode | undefined { + private async getHistoryNode(editor: TextEditor | undefined): Promise { // If we have no active editor, or no visible editors, or no trackable visible editors reset the view if (editor === undefined || window.visibleTextEditors.length === 0 || !window.visibleTextEditors.some(e => e.document && this.git.isTrackable(e.document.uri))) return undefined; // If we do have a visible trackable editor, don't change from the last state (avoids issues when focus switches to the problems/output/debug console panes) if (editor.document === undefined || !this.git.isTrackable(editor.document.uri)) return this._root; - const uri = this.git.getGitUriForFile(editor.document.uri) || new GitUri(editor.document.uri, { repoPath: this.git.repoPath, fileName: editor.document.uri.fsPath }); + let uri = this.git.getGitUriForFile(editor.document.uri); + if (uri === undefined) { + const repoPath = await this.git.getRepoPath(editor.document.uri); + if (repoPath === undefined) return undefined; + + uri = new GitUri(editor.document.uri, { repoPath: repoPath, fileName: editor.document.uri.fsPath }); + } + if (UriComparer.equals(uri, this._root && this._root.uri)) return this._root; return new HistoryNode(uri, this.context, this.git); } - private onActiveEditorChanged(editor: TextEditor | undefined) { + private async onActiveEditorChanged(editor: TextEditor | undefined) { if (this._view !== GitExplorerView.History) return; - const root = this.getRootNode(editor); + const root = await this.getRootNode(editor); if (root === this._root) return; this._root = root; @@ -136,15 +163,18 @@ export class GitExplorer implements TreeDataProvider { view = this.context.workspaceState.get(WorkspaceState.GitExplorerView, GitExplorerView.Repository); } - this.setView(view); - this._root = this.getRootNode(window.activeTextEditor); - this.refresh(); + this.reset(view); } } private onRepoChanged(reasons: RepoChangedReasons[]) { if (this._view !== GitExplorerView.Repository) return; + // If we are changing the set of repositories then force a root node reset + if (reasons.includes(RepoChangedReasons.Repositories)) { + this._root = undefined; + } + this.refresh(); } @@ -160,9 +190,9 @@ export class GitExplorer implements TreeDataProvider { } } - refresh(node?: ExplorerNode, root?: ExplorerNode) { - if (root === undefined && this._view === GitExplorerView.History) { - this._root = this.getRootNode(window.activeTextEditor); + async refresh(node?: ExplorerNode, root?: ExplorerNode) { + if (this._root === undefined || (root === undefined && this._view === GitExplorerView.History)) { + this._root = await this.getRootNode(window.activeTextEditor); } this._onDidChangeTreeData.fire(node); @@ -176,6 +206,18 @@ export class GitExplorer implements TreeDataProvider { this.refresh(node); } + async reset(view: GitExplorerView, force: boolean = false) { + this.setView(view); + + if (force) { + this._root = undefined; + } + this._root = await this.getRootNode(window.activeTextEditor); + if (force) { + this.refresh(); + } + } + setView(view: GitExplorerView) { if (this._view === view) return; @@ -191,14 +233,10 @@ export class GitExplorer implements TreeDataProvider { } } - switchTo(view: GitExplorerView) { + async switchTo(view: GitExplorerView) { if (this._view === view) return; - this.setView(view); - - this._root = undefined; - this._root = this.getRootNode(window.activeTextEditor); - this.refresh(); + this.reset(view, true); } private async applyChanges(node: CommitNode | StashNode) { diff --git a/src/views/repositoriesNode.ts b/src/views/repositoriesNode.ts new file mode 100644 index 0000000..63b0e69 --- /dev/null +++ b/src/views/repositoriesNode.ts @@ -0,0 +1,30 @@ +'use strict'; +import { ExtensionContext, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; +import { ExplorerNode, ResourceType } from './explorerNode'; +import { GitService, GitUri, Repository } from '../gitService'; +import { RepositoryNode } from './repositoryNode'; + +export class RepositoriesNode extends ExplorerNode { + + readonly resourceType: ResourceType = 'gitlens:repositories'; + + constructor( + private readonly repositories: Repository[], + protected readonly context: ExtensionContext, + protected readonly git: GitService + ) { + super(undefined!); + } + + async getChildren(): Promise { + return this.repositories + .sort((a, b) => a.index - b.index) + .map(repo => new RepositoryNode(new GitUri(Uri.file(repo.path), { repoPath: repo.path, fileName: repo.path }), repo, this.context, this.git)); + } + + getTreeItem(): TreeItem { + const item = new TreeItem(`Repositories`, TreeItemCollapsibleState.Expanded); + item.contextValue = this.resourceType; + return item; + } +} \ No newline at end of file diff --git a/src/views/repositoryNode.ts b/src/views/repositoryNode.ts index 551fe38..e5daf3c 100644 --- a/src/views/repositoryNode.ts +++ b/src/views/repositoryNode.ts @@ -1,9 +1,10 @@ 'use strict'; +import { Strings } from '../system'; import { ExtensionContext, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { BranchesNode } from './branchesNode'; import { GlyphChars } from '../constants'; import { ExplorerNode, ResourceType } from './explorerNode'; -import { GitService, GitUri } from '../gitService'; +import { GitService, GitUri, Repository } from '../gitService'; import { RemotesNode } from './remotesNode'; import { StatusNode } from './statusNode'; import { StashesNode } from './stashesNode'; @@ -14,6 +15,7 @@ export class RepositoryNode extends ExplorerNode { constructor( uri: GitUri, + private repo: Repository, protected readonly context: ExtensionContext, protected readonly git: GitService ) { @@ -30,7 +32,7 @@ export class RepositoryNode extends ExplorerNode { } getTreeItem(): TreeItem { - const item = new TreeItem(`Repository ${GlyphChars.Dash} ${this.uri.repoPath}`, TreeItemCollapsibleState.Expanded); + const item = new TreeItem(`Repository ${Strings.pad(GlyphChars.Dash, 1, 1)} ${this.repo.name || this.uri.repoPath}`, TreeItemCollapsibleState.Expanded); item.contextValue = this.resourceType; return item; }