From 0b0137f3a4cd88ad08fa1a44842e9a5ad9ec695b Mon Sep 17 00:00:00 2001 From: Eric Amodio Date: Sun, 9 Sep 2018 03:59:26 -0400 Subject: [PATCH] Moves gitService into git folder & renames a couple of git files --- src/annotations/annotations.ts | 2 +- src/annotations/blameAnnotationProvider.ts | 2 +- src/annotations/gutterBlameAnnotationProvider.ts | 2 +- src/annotations/heatmapBlameAnnotationProvider.ts | 2 +- src/annotations/recentChangesAnnotationProvider.ts | 2 +- src/codelens/codeLensController.ts | 2 +- src/codelens/codeLensProvider.ts | 642 +++++++ src/codelens/gitCodeLensProvider.ts | 642 ------- src/commands/common.ts | 2 +- src/commands/copyMessageToClipboard.ts | 2 +- src/commands/copyShaToClipboard.ts | 2 +- src/commands/diffLineWithPrevious.ts | 2 +- src/commands/diffLineWithWorking.ts | 2 +- src/commands/diffWith.ts | 2 +- src/commands/diffWithBranch.ts | 2 +- src/commands/diffWithNext.ts | 2 +- src/commands/diffWithPrevious.ts | 2 +- src/commands/diffWithRevision.ts | 2 +- src/commands/diffWithWorking.ts | 2 +- src/commands/openBranchInRemote.ts | 2 +- src/commands/openBranchesInRemote.ts | 2 +- src/commands/openCommitInRemote.ts | 2 +- src/commands/openFileInRemote.ts | 2 +- src/commands/openFileRevision.ts | 2 +- src/commands/openInRemote.ts | 2 +- src/commands/openRepoInRemote.ts | 2 +- src/commands/openWorkingFile.ts | 2 +- src/commands/showCommitSearch.ts | 2 +- src/commands/showQuickBranchHistory.ts | 2 +- src/commands/showQuickCommitDetails.ts | 2 +- src/commands/showQuickCommitFileDetails.ts | 2 +- src/commands/showQuickFileHistory.ts | 2 +- src/commands/stashApply.ts | 2 +- src/commands/stashDelete.ts | 2 +- src/configuration.ts | 2 +- src/container.ts | 2 +- src/extension.ts | 2 +- src/git/fsProvider.ts | 2 +- src/git/git.ts | 2 +- src/git/gitLocator.ts | 84 - src/git/gitService.ts | 1946 ++++++++++++++++++++ src/git/gitUri.ts | 2 +- src/git/locator.ts | 83 + src/gitService.ts | 1946 -------------------- src/messages.ts | 2 +- src/quickpicks/branchHistoryQuickPick.ts | 2 +- src/quickpicks/branchesAndTagsQuickPick.ts | 2 +- src/quickpicks/branchesQuickPick.ts | 2 +- src/quickpicks/commitFileQuickPick.ts | 2 +- src/quickpicks/commitQuickPick.ts | 2 +- src/quickpicks/commitsQuickPick.ts | 2 +- src/quickpicks/commonQuickPicks.ts | 2 +- src/quickpicks/fileHistoryQuickPick.ts | 2 +- src/quickpicks/remotesQuickPick.ts | 2 +- src/quickpicks/repoStatusQuickPick.ts | 2 +- src/quickpicks/repositoriesQuickPick.ts | 2 +- src/quickpicks/stashListQuickPick.ts | 2 +- src/statusbar/statusBarController.ts | 2 +- src/trackers/documentTracker.ts | 2 +- src/trackers/gitLineTracker.ts | 2 +- src/trackers/trackedDocument.ts | 2 +- src/views/explorerCommands.ts | 2 +- src/views/gitExplorer.ts | 2 +- src/views/nodes/activeRepositoryNode.ts | 2 +- src/views/nodes/branchNode.ts | 2 +- src/views/nodes/branchOrTagFolderNode.ts | 2 +- src/views/nodes/branchesNode.ts | 2 +- src/views/nodes/commitFileNode.ts | 2 +- src/views/nodes/commitNode.ts | 2 +- src/views/nodes/commitResultsNode.ts | 2 +- src/views/nodes/commitsNode.ts | 2 +- src/views/nodes/commitsResultsNode.ts | 2 +- src/views/nodes/comparisonResultsNode.ts | 2 +- src/views/nodes/explorerNode.ts | 2 +- src/views/nodes/fileHistoryNode.ts | 2 +- src/views/nodes/folderNode.ts | 2 +- src/views/nodes/historyNode.ts | 2 +- src/views/nodes/remoteNode.ts | 2 +- src/views/nodes/remotesNode.ts | 2 +- src/views/nodes/repositoriesNode.ts | 2 +- src/views/nodes/repositoryNode.ts | 2 +- src/views/nodes/stashFileNode.ts | 2 +- src/views/nodes/stashNode.ts | 2 +- src/views/nodes/stashesNode.ts | 2 +- src/views/nodes/statusFileCommitsNode.ts | 2 +- src/views/nodes/statusFileNode.ts | 2 +- src/views/nodes/statusFilesNode.ts | 2 +- src/views/nodes/statusFilesResultsNode.ts | 2 +- src/views/nodes/statusNode.ts | 2 +- src/views/nodes/statusUpstreamNode.ts | 2 +- src/views/nodes/tagNode.ts | 2 +- src/views/nodes/tagsNode.ts | 2 +- src/views/resultsExplorer.ts | 2 +- 93 files changed, 2758 insertions(+), 2759 deletions(-) create mode 100644 src/codelens/codeLensProvider.ts delete mode 100644 src/codelens/gitCodeLensProvider.ts delete mode 100644 src/git/gitLocator.ts create mode 100644 src/git/gitService.ts create mode 100644 src/git/locator.ts delete mode 100644 src/gitService.ts diff --git a/src/annotations/annotations.ts b/src/annotations/annotations.ts index 1fbde3b..ad2a8a7 100644 --- a/src/annotations/annotations.ts +++ b/src/annotations/annotations.ts @@ -24,7 +24,7 @@ import { GitService, GitUri, ICommitFormatOptions -} from '../gitService'; +} from '../git/gitService'; import { Objects, Strings } from '../system'; import { toRgba } from '../ui/shared/colors'; diff --git a/src/annotations/blameAnnotationProvider.ts b/src/annotations/blameAnnotationProvider.ts index 8e38cf8..6ea0767 100644 --- a/src/annotations/blameAnnotationProvider.ts +++ b/src/annotations/blameAnnotationProvider.ts @@ -12,7 +12,7 @@ import { TextEditorDecorationType } from 'vscode'; import { Container } from '../container'; -import { GitBlame, GitCommit, GitUri } from '../gitService'; +import { GitBlame, GitCommit, GitUri } from '../git/gitService'; import { Arrays, Iterables } from '../system'; import { GitDocumentState, TrackedDocument } from '../trackers/gitDocumentTracker'; import { AnnotationProviderBase } from './annotationProvider'; diff --git a/src/annotations/gutterBlameAnnotationProvider.ts b/src/annotations/gutterBlameAnnotationProvider.ts index 0387f8a..9226ada 100644 --- a/src/annotations/gutterBlameAnnotationProvider.ts +++ b/src/annotations/gutterBlameAnnotationProvider.ts @@ -3,7 +3,7 @@ import { DecorationOptions, DecorationRenderOptions, Range, TextEditorDecoration import { FileAnnotationType, GravatarDefaultStyle } from '../configuration'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitBlameCommit, ICommitFormatOptions } from '../gitService'; +import { GitBlameCommit, ICommitFormatOptions } from '../git/gitService'; import { Logger } from '../logger'; import { Objects, Strings } from '../system'; import { Annotations } from './annotations'; diff --git a/src/annotations/heatmapBlameAnnotationProvider.ts b/src/annotations/heatmapBlameAnnotationProvider.ts index 81aca05..098e786 100644 --- a/src/annotations/heatmapBlameAnnotationProvider.ts +++ b/src/annotations/heatmapBlameAnnotationProvider.ts @@ -2,7 +2,7 @@ import { DecorationOptions, Range } from 'vscode'; import { FileAnnotationType } from '../configuration'; import { Container } from '../container'; -import { GitBlameCommit } from '../gitService'; +import { GitBlameCommit } from '../git/gitService'; import { Logger } from '../logger'; import { Strings } from '../system'; import { Annotations } from './annotations'; diff --git a/src/annotations/recentChangesAnnotationProvider.ts b/src/annotations/recentChangesAnnotationProvider.ts index 91481ef..39eb674 100644 --- a/src/annotations/recentChangesAnnotationProvider.ts +++ b/src/annotations/recentChangesAnnotationProvider.ts @@ -2,7 +2,7 @@ import { DecorationOptions, MarkdownString, Position, Range, TextEditor, TextEditorDecorationType } from 'vscode'; import { FileAnnotationType } from '../configuration'; import { Container } from '../container'; -import { GitUri } from '../gitService'; +import { GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { Strings } from '../system'; import { GitDocumentState, TrackedDocument } from '../trackers/gitDocumentTracker'; diff --git a/src/codelens/codeLensController.ts b/src/codelens/codeLensController.ts index 7226372..ea84060 100644 --- a/src/codelens/codeLensController.ts +++ b/src/codelens/codeLensController.ts @@ -9,7 +9,7 @@ import { DocumentDirtyIdleTriggerEvent, GitDocumentState } from '../trackers/gitDocumentTracker'; -import { GitCodeLensProvider } from './gitCodeLensProvider'; +import { GitCodeLensProvider } from './codeLensProvider'; export class CodeLensController implements Disposable { private _canToggle: boolean = false; diff --git a/src/codelens/codeLensProvider.ts b/src/codelens/codeLensProvider.ts new file mode 100644 index 0000000..ebd74bb --- /dev/null +++ b/src/codelens/codeLensProvider.ts @@ -0,0 +1,642 @@ +'use strict'; +import { + CancellationToken, + CodeLens, + CodeLensProvider, + Command, + commands, + DocumentSelector, + Event, + EventEmitter, + ExtensionContext, + Location, + Position, + Range, + SymbolInformation, + SymbolKind, + TextDocument, + Uri +} from 'vscode'; +import { + Commands, + DiffWithPreviousCommandArgs, + ShowQuickCommitDetailsCommandArgs, + ShowQuickCommitFileDetailsCommandArgs, + ShowQuickFileHistoryCommandArgs +} from '../commands'; +import { + CodeLensCommand, + CodeLensLanguageScope, + CodeLensScopes, + configuration, + ICodeLensConfig +} from '../configuration'; +import { BuiltInCommands, DocumentSchemes } from '../constants'; +import { Container } from '../container'; +import { GitBlame, GitBlameCommit, GitBlameLines, GitService, GitUri } from '../git/gitService'; +import { Logger } from '../logger'; +import { Functions, Iterables } from '../system'; +import { DocumentTracker, GitDocumentState } from '../trackers/gitDocumentTracker'; + +export class GitRecentChangeCodeLens extends CodeLens { + constructor( + public readonly languageId: string, + public readonly symbol: SymbolInformation, + public readonly uri: GitUri | undefined, + private readonly blame: (() => GitBlameLines | undefined) | undefined, + public readonly blameRange: Range, + public readonly isFullRange: boolean, + range: Range, + public readonly desiredCommand: CodeLensCommand, + command?: Command | undefined + ) { + super(range, command); + } + + getBlame(): GitBlameLines | undefined { + return this.blame && this.blame(); + } +} + +export class GitAuthorsCodeLens extends CodeLens { + constructor( + public readonly languageId: string, + public readonly symbol: SymbolInformation, + public readonly uri: GitUri | undefined, + private readonly blame: () => GitBlameLines | undefined, + public readonly blameRange: Range, + public readonly isFullRange: boolean, + range: Range, + public readonly desiredCommand: CodeLensCommand + ) { + super(range); + } + + getBlame(): GitBlameLines | undefined { + return this.blame(); + } +} + +export class GitCodeLensProvider implements CodeLensProvider { + private _onDidChangeCodeLenses = new EventEmitter(); + public get onDidChangeCodeLenses(): Event { + return this._onDidChangeCodeLenses.event; + } + + static selector: DocumentSelector = [ + { scheme: DocumentSchemes.File }, + { scheme: DocumentSchemes.Git }, + { scheme: DocumentSchemes.GitLens } + ]; + + constructor( + context: ExtensionContext, + private readonly _git: GitService, + private readonly _tracker: DocumentTracker + ) {} + + reset(reason?: 'idle' | 'saved') { + this._onDidChangeCodeLenses.fire(); + } + + async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { + const trackedDocument = await this._tracker.getOrAdd(document); + if (!trackedDocument.isBlameable) return []; + + let dirty = false; + if (document.isDirty) { + // Only allow dirty blames if we are idle + if (trackedDocument.isDirtyIdle) { + const maxLines = Container.config.advanced.blame.sizeThresholdAfterEdit; + if (maxLines > 0 && document.lineCount > maxLines) { + dirty = true; + } + } + else { + dirty = true; + } + } + + const cfg = configuration.get(configuration.name('codeLens').value, document.uri); + + let languageScope = + cfg.scopesByLanguage && + cfg.scopesByLanguage.find( + ll => ll.language !== undefined && ll.language.toLowerCase() === document.languageId + ); + if (languageScope == null) { + languageScope = { + language: undefined + } as CodeLensLanguageScope; + } + if (languageScope.scopes == null) { + languageScope.scopes = cfg.scopes; + } + if (languageScope.symbolScopes == null) { + languageScope.symbolScopes = cfg.symbolScopes; + } + + languageScope.symbolScopes = + languageScope.symbolScopes != null + ? (languageScope.symbolScopes = languageScope.symbolScopes.map(s => s.toLowerCase())) + : []; + + const lenses: CodeLens[] = []; + + const gitUri = trackedDocument.uri; + let blame: GitBlame | undefined; + let symbols; + + if (!dirty) { + if (token.isCancellationRequested) return lenses; + + if (languageScope.scopes.length === 1 && languageScope.scopes.includes(CodeLensScopes.Document)) { + blame = document.isDirty + ? await this._git.getBlameForFileContents(gitUri, document.getText()) + : await this._git.getBlameForFile(gitUri); + } + else { + [blame, symbols] = await Promise.all([ + document.isDirty + ? this._git.getBlameForFileContents(gitUri, document.getText()) + : this._git.getBlameForFile(gitUri), + commands.executeCommand(BuiltInCommands.ExecuteDocumentSymbolProvider, document.uri) as Promise< + SymbolInformation[] + > + ]); + } + + if (blame === undefined || blame.lines.length === 0) return lenses; + } + else { + if (languageScope.scopes.length !== 1 || !languageScope.scopes.includes(CodeLensScopes.Document)) { + symbols = (await commands.executeCommand( + BuiltInCommands.ExecuteDocumentSymbolProvider, + document.uri + )) as SymbolInformation[]; + } + } + + if (token.isCancellationRequested) return lenses; + + const documentRangeFn = Functions.once(() => document.validateRange(new Range(0, 1000000, 1000000, 1000000))); + + // Since blame information isn't valid when there are unsaved changes -- update the lenses appropriately + const dirtyCommand = dirty ? ({ title: this.getDirtyTitle(cfg) } as Command) : undefined; + + if (symbols !== undefined) { + Logger.log('GitCodeLensProvider.provideCodeLenses:', `${symbols.length} symbol(s) found`); + symbols.forEach(sym => + this.provideCodeLens( + lenses, + document, + sym, + languageScope as Required, + documentRangeFn, + blame, + gitUri, + cfg, + dirty, + dirtyCommand + ) + ); + } + + if ( + (languageScope.scopes.includes(CodeLensScopes.Document) || languageScope.symbolScopes.includes('file')) && + !languageScope.symbolScopes.includes('!file') + ) { + // Check if we have a lens for the whole document -- if not add one + if (!lenses.find(l => l.range.start.line === 0 && l.range.end.line === 0)) { + const blameRange = documentRangeFn(); + + let blameForRangeFn: (() => GitBlameLines | undefined) | undefined = undefined; + if (dirty || cfg.recentChange.enabled) { + if (!dirty) { + blameForRangeFn = Functions.once(() => + this._git.getBlameForRangeSync(blame!, gitUri!, blameRange) + ); + } + + const fileSymbol = new SymbolInformation( + gitUri.getFilename(), + SymbolKind.File, + '', + new Location(gitUri.documentUri(), new Range(0, 0, 0, blameRange.start.character)) + ); + lenses.push( + new GitRecentChangeCodeLens( + document.languageId, + fileSymbol, + gitUri, + blameForRangeFn, + blameRange, + true, + getRangeFromSymbol(fileSymbol), + cfg.recentChange.command, + dirtyCommand + ) + ); + } + if (!dirty && cfg.authors.enabled) { + if (blameForRangeFn === undefined) { + blameForRangeFn = Functions.once(() => + this._git.getBlameForRangeSync(blame!, gitUri!, blameRange) + ); + } + + const fileSymbol = new SymbolInformation( + gitUri.getFilename(), + SymbolKind.File, + '', + new Location(gitUri.documentUri(), new Range(0, 1, 0, blameRange.start.character)) + ); + lenses.push( + new GitAuthorsCodeLens( + document.languageId, + fileSymbol, + gitUri, + blameForRangeFn, + blameRange, + true, + getRangeFromSymbol(fileSymbol), + cfg.authors.command + ) + ); + } + } + } + + return lenses; + } + + private validateSymbolAndGetBlameRange( + symbol: SymbolInformation, + languageScope: Required, + documentRangeFn: () => Range + ): Range | undefined { + let valid = false; + let range: Range | undefined; + + const symbolName = SymbolKind[symbol.kind].toLowerCase(); + switch (symbol.kind) { + case SymbolKind.File: + if ( + languageScope.scopes.includes(CodeLensScopes.Containers) || + languageScope.symbolScopes!.includes(symbolName) + ) { + valid = !languageScope.symbolScopes!.includes(`!${symbolName}`); + } + + if (valid) { + // Adjust the range to be for the whole file + range = documentRangeFn(); + } + break; + + case SymbolKind.Package: + if ( + languageScope.scopes.includes(CodeLensScopes.Containers) || + languageScope.symbolScopes!.includes(symbolName) + ) { + valid = !languageScope.symbolScopes!.includes(`!${symbolName}`); + } + + if (valid) { + // Adjust the range to be for the whole file + if (getRangeFromSymbol(symbol).start.line === 0 && getRangeFromSymbol(symbol).end.line === 0) { + range = documentRangeFn(); + } + } + break; + + case SymbolKind.Class: + case SymbolKind.Interface: + case SymbolKind.Module: + case SymbolKind.Namespace: + case SymbolKind.Struct: + if ( + languageScope.scopes.includes(CodeLensScopes.Containers) || + languageScope.symbolScopes!.includes(symbolName) + ) { + valid = !languageScope.symbolScopes!.includes(`!${symbolName}`); + } + break; + + case SymbolKind.Constructor: + case SymbolKind.Enum: + case SymbolKind.Function: + case SymbolKind.Method: + if ( + languageScope.scopes.includes(CodeLensScopes.Blocks) || + languageScope.symbolScopes!.includes(symbolName) + ) { + valid = !languageScope.symbolScopes!.includes(`!${symbolName}`); + } + break; + + default: + if (languageScope.symbolScopes!.includes(symbolName)) { + valid = !languageScope.symbolScopes!.includes(`!${symbolName}`); + } + break; + } + + return valid ? range || getRangeFromSymbol(symbol) : undefined; + } + + private provideCodeLens( + lenses: CodeLens[], + document: TextDocument, + symbol: SymbolInformation, + languageScope: Required, + documentRangeFn: () => Range, + blame: GitBlame | undefined, + gitUri: GitUri | undefined, + cfg: ICodeLensConfig, + dirty: boolean, + dirtyCommand: Command | undefined + ): void { + const blameRange = this.validateSymbolAndGetBlameRange(symbol, languageScope, documentRangeFn); + if (blameRange === undefined) return; + + const line = document.lineAt(getRangeFromSymbol(symbol).start); + // Make sure there is only 1 lens per line + if (lenses.length && lenses[lenses.length - 1].range.start.line === line.lineNumber) return; + + // Anchor the code lens to the start of the line -- so that the range won't change with edits (otherwise the code lens will be removed and re-added) + let startChar = 0; + + let blameForRangeFn: (() => GitBlameLines | undefined) | undefined; + if (dirty || cfg.recentChange.enabled) { + if (!dirty) { + blameForRangeFn = Functions.once(() => this._git.getBlameForRangeSync(blame!, gitUri!, blameRange)); + } + lenses.push( + new GitRecentChangeCodeLens( + document.languageId, + symbol, + gitUri, + blameForRangeFn, + blameRange, + false, + line.range.with(new Position(line.range.start.line, startChar)), + cfg.recentChange.command, + dirtyCommand + ) + ); + startChar++; + } + + if (cfg.authors.enabled) { + let multiline = !blameRange.isSingleLine; + // HACK for Omnisharp, since it doesn't return full ranges + if (!multiline && document.languageId === 'csharp') { + switch (symbol.kind) { + case SymbolKind.File: + break; + case SymbolKind.Package: + case SymbolKind.Module: + case SymbolKind.Namespace: + case SymbolKind.Class: + case SymbolKind.Interface: + case SymbolKind.Constructor: + case SymbolKind.Method: + case SymbolKind.Function: + case SymbolKind.Enum: + multiline = true; + break; + } + } + + if (multiline && !dirty) { + if (blameForRangeFn === undefined) { + blameForRangeFn = Functions.once(() => this._git.getBlameForRangeSync(blame!, gitUri!, blameRange)); + } + lenses.push( + new GitAuthorsCodeLens( + document.languageId, + symbol, + gitUri, + blameForRangeFn, + blameRange, + false, + line.range.with(new Position(line.range.start.line, startChar)), + cfg.authors.command + ) + ); + } + } + } + + resolveCodeLens(lens: CodeLens, token: CancellationToken): CodeLens | Thenable { + if (lens instanceof GitRecentChangeCodeLens) return this.resolveGitRecentChangeCodeLens(lens, token); + if (lens instanceof GitAuthorsCodeLens) return this.resolveGitAuthorsCodeLens(lens, token); + return Promise.reject(undefined); + } + + private resolveGitRecentChangeCodeLens(lens: GitRecentChangeCodeLens, token: CancellationToken): CodeLens { + const blame = lens.getBlame(); + if (blame === undefined) return lens; + + const recentCommit = Iterables.first(blame.commits.values()); + let title = `${recentCommit.author}, ${recentCommit.formattedDate}`; + if (Container.config.debug) { + title += ` [${lens.languageId}: ${SymbolKind[lens.symbol.kind]}(${lens.range.start.character}-${ + lens.range.end.character + }${lens.symbol.containerName ? `|${lens.symbol.containerName}` : ''}), Lines (${lens.blameRange.start.line + + 1}-${lens.blameRange.end.line + 1}), Commit (${recentCommit.shortSha})]`; + } + + switch (lens.desiredCommand) { + case CodeLensCommand.DiffWithPrevious: + return this.applyDiffWithPreviousCommand(title, lens, blame, recentCommit); + case CodeLensCommand.ShowQuickCommitDetails: + return this.applyShowQuickCommitDetailsCommand( + title, + lens, + blame, + recentCommit + ); + case CodeLensCommand.ShowQuickCommitFileDetails: + return this.applyShowQuickCommitFileDetailsCommand( + title, + lens, + blame, + recentCommit + ); + case CodeLensCommand.ShowQuickCurrentBranchHistory: + return this.applyShowQuickCurrentBranchHistoryCommand( + title, + lens, + blame, + recentCommit + ); + case CodeLensCommand.ShowQuickFileHistory: + return this.applyShowQuickFileHistoryCommand(title, lens, blame, recentCommit); + case CodeLensCommand.ToggleFileBlame: + return this.applyToggleFileBlameCommand(title, lens, blame); + default: + return lens; + } + } + + private resolveGitAuthorsCodeLens(lens: GitAuthorsCodeLens, token: CancellationToken): CodeLens { + const blame = lens.getBlame(); + if (blame === undefined) return lens; + + const count = blame.authors.size; + let title = `${count} ${count > 1 ? 'authors' : 'author'} (${Iterables.first(blame.authors.values()).name}${ + count > 1 ? ' and others' : '' + })`; + if (Container.config.debug) { + title += ` [${lens.languageId}: ${SymbolKind[lens.symbol.kind]}(${lens.range.start.character}-${ + lens.range.end.character + }${lens.symbol.containerName ? `|${lens.symbol.containerName}` : ''}), Lines (${lens.blameRange.start.line + + 1}-${lens.blameRange.end.line + 1}), Authors (${Iterables.join( + Iterables.map(blame.authors.values(), a => a.name), + ', ' + )})]`; + } + + switch (lens.desiredCommand) { + case CodeLensCommand.DiffWithPrevious: + return this.applyDiffWithPreviousCommand(title, lens, blame); + case CodeLensCommand.ShowQuickCommitDetails: + return this.applyShowQuickCommitDetailsCommand(title, lens, blame); + case CodeLensCommand.ShowQuickCommitFileDetails: + return this.applyShowQuickCommitFileDetailsCommand(title, lens, blame); + case CodeLensCommand.ShowQuickCurrentBranchHistory: + return this.applyShowQuickCurrentBranchHistoryCommand(title, lens, blame); + case CodeLensCommand.ShowQuickFileHistory: + return this.applyShowQuickFileHistoryCommand(title, lens, blame); + case CodeLensCommand.ToggleFileBlame: + return this.applyToggleFileBlameCommand(title, lens, blame); + default: + return lens; + } + } + + private applyDiffWithPreviousCommand( + title: string, + lens: T, + blame: GitBlameLines, + commit?: GitBlameCommit + ): T { + if (commit === undefined) { + const blameLine = blame.allLines[lens.range.start.line]; + commit = blame.commits.get(blameLine.sha); + } + + lens.command = { + title: title, + command: Commands.DiffWithPrevious, + arguments: [ + Uri.file(lens.uri!.fsPath), + { + commit: commit + } as DiffWithPreviousCommandArgs + ] + }; + return lens; + } + + private applyShowQuickCommitDetailsCommand( + title: string, + lens: T, + blame: GitBlameLines, + commit?: GitBlameCommit + ): T { + lens.command = { + title: title, + command: commit !== undefined && commit.isUncommitted ? '' : CodeLensCommand.ShowQuickCommitDetails, + arguments: [ + Uri.file(lens.uri!.fsPath), + { + commit, + sha: commit === undefined ? undefined : commit.sha + } as ShowQuickCommitDetailsCommandArgs + ] + }; + return lens; + } + + private applyShowQuickCommitFileDetailsCommand( + title: string, + lens: T, + blame: GitBlameLines, + commit?: GitBlameCommit + ): T { + lens.command = { + title: title, + command: commit !== undefined && commit.isUncommitted ? '' : CodeLensCommand.ShowQuickCommitFileDetails, + arguments: [ + Uri.file(lens.uri!.fsPath), + { + commit, + sha: commit === undefined ? undefined : commit.sha + } as ShowQuickCommitFileDetailsCommandArgs + ] + }; + return lens; + } + + private applyShowQuickCurrentBranchHistoryCommand( + title: string, + lens: T, + blame: GitBlameLines, + commit?: GitBlameCommit + ): T { + lens.command = { + title: title, + command: CodeLensCommand.ShowQuickCurrentBranchHistory, + arguments: [Uri.file(lens.uri!.fsPath)] + }; + return lens; + } + + private applyShowQuickFileHistoryCommand( + title: string, + lens: T, + blame: GitBlameLines, + commit?: GitBlameCommit + ): T { + lens.command = { + title: title, + command: CodeLensCommand.ShowQuickFileHistory, + arguments: [ + Uri.file(lens.uri!.fsPath), + { + range: lens.isFullRange ? undefined : lens.blameRange + } as ShowQuickFileHistoryCommandArgs + ] + }; + return lens; + } + + private applyToggleFileBlameCommand( + title: string, + lens: T, + blame: GitBlameLines + ): T { + lens.command = { + title: title, + command: Commands.ToggleFileBlame, + arguments: [Uri.file(lens.uri!.fsPath)] + }; + return lens; + } + + private getDirtyTitle(cfg: ICodeLensConfig) { + if (cfg.recentChange.enabled && cfg.authors.enabled) { + return Container.config.strings.codeLens.unsavedChanges.recentChangeAndAuthors; + } + if (cfg.recentChange.enabled) return Container.config.strings.codeLens.unsavedChanges.recentChangeOnly; + return Container.config.strings.codeLens.unsavedChanges.authorsOnly; + } +} + +function getRangeFromSymbol(symbol: SymbolInformation) { + // Normalize the range to deal with the new api + return (symbol.location && symbol.location.range) || (symbol as any).range; +} diff --git a/src/codelens/gitCodeLensProvider.ts b/src/codelens/gitCodeLensProvider.ts deleted file mode 100644 index d4b45b1..0000000 --- a/src/codelens/gitCodeLensProvider.ts +++ /dev/null @@ -1,642 +0,0 @@ -'use strict'; -import { - CancellationToken, - CodeLens, - CodeLensProvider, - Command, - commands, - DocumentSelector, - Event, - EventEmitter, - ExtensionContext, - Location, - Position, - Range, - SymbolInformation, - SymbolKind, - TextDocument, - Uri -} from 'vscode'; -import { - Commands, - DiffWithPreviousCommandArgs, - ShowQuickCommitDetailsCommandArgs, - ShowQuickCommitFileDetailsCommandArgs, - ShowQuickFileHistoryCommandArgs -} from '../commands'; -import { - CodeLensCommand, - CodeLensLanguageScope, - CodeLensScopes, - configuration, - ICodeLensConfig -} from '../configuration'; -import { BuiltInCommands, DocumentSchemes } from '../constants'; -import { Container } from '../container'; -import { GitBlame, GitBlameCommit, GitBlameLines, GitService, GitUri } from '../gitService'; -import { Logger } from '../logger'; -import { Functions, Iterables } from '../system'; -import { DocumentTracker, GitDocumentState } from '../trackers/gitDocumentTracker'; - -export class GitRecentChangeCodeLens extends CodeLens { - constructor( - public readonly languageId: string, - public readonly symbol: SymbolInformation, - public readonly uri: GitUri | undefined, - private readonly blame: (() => GitBlameLines | undefined) | undefined, - public readonly blameRange: Range, - public readonly isFullRange: boolean, - range: Range, - public readonly desiredCommand: CodeLensCommand, - command?: Command | undefined - ) { - super(range, command); - } - - getBlame(): GitBlameLines | undefined { - return this.blame && this.blame(); - } -} - -export class GitAuthorsCodeLens extends CodeLens { - constructor( - public readonly languageId: string, - public readonly symbol: SymbolInformation, - public readonly uri: GitUri | undefined, - private readonly blame: () => GitBlameLines | undefined, - public readonly blameRange: Range, - public readonly isFullRange: boolean, - range: Range, - public readonly desiredCommand: CodeLensCommand - ) { - super(range); - } - - getBlame(): GitBlameLines | undefined { - return this.blame(); - } -} - -export class GitCodeLensProvider implements CodeLensProvider { - private _onDidChangeCodeLenses = new EventEmitter(); - public get onDidChangeCodeLenses(): Event { - return this._onDidChangeCodeLenses.event; - } - - static selector: DocumentSelector = [ - { scheme: DocumentSchemes.File }, - { scheme: DocumentSchemes.Git }, - { scheme: DocumentSchemes.GitLens } - ]; - - constructor( - context: ExtensionContext, - private readonly _git: GitService, - private readonly _tracker: DocumentTracker - ) {} - - reset(reason?: 'idle' | 'saved') { - this._onDidChangeCodeLenses.fire(); - } - - async provideCodeLenses(document: TextDocument, token: CancellationToken): Promise { - const trackedDocument = await this._tracker.getOrAdd(document); - if (!trackedDocument.isBlameable) return []; - - let dirty = false; - if (document.isDirty) { - // Only allow dirty blames if we are idle - if (trackedDocument.isDirtyIdle) { - const maxLines = Container.config.advanced.blame.sizeThresholdAfterEdit; - if (maxLines > 0 && document.lineCount > maxLines) { - dirty = true; - } - } - else { - dirty = true; - } - } - - const cfg = configuration.get(configuration.name('codeLens').value, document.uri); - - let languageScope = - cfg.scopesByLanguage && - cfg.scopesByLanguage.find( - ll => ll.language !== undefined && ll.language.toLowerCase() === document.languageId - ); - if (languageScope == null) { - languageScope = { - language: undefined - } as CodeLensLanguageScope; - } - if (languageScope.scopes == null) { - languageScope.scopes = cfg.scopes; - } - if (languageScope.symbolScopes == null) { - languageScope.symbolScopes = cfg.symbolScopes; - } - - languageScope.symbolScopes = - languageScope.symbolScopes != null - ? (languageScope.symbolScopes = languageScope.symbolScopes.map(s => s.toLowerCase())) - : []; - - const lenses: CodeLens[] = []; - - const gitUri = trackedDocument.uri; - let blame: GitBlame | undefined; - let symbols; - - if (!dirty) { - if (token.isCancellationRequested) return lenses; - - if (languageScope.scopes.length === 1 && languageScope.scopes.includes(CodeLensScopes.Document)) { - blame = document.isDirty - ? await this._git.getBlameForFileContents(gitUri, document.getText()) - : await this._git.getBlameForFile(gitUri); - } - else { - [blame, symbols] = await Promise.all([ - document.isDirty - ? this._git.getBlameForFileContents(gitUri, document.getText()) - : this._git.getBlameForFile(gitUri), - commands.executeCommand(BuiltInCommands.ExecuteDocumentSymbolProvider, document.uri) as Promise< - SymbolInformation[] - > - ]); - } - - if (blame === undefined || blame.lines.length === 0) return lenses; - } - else { - if (languageScope.scopes.length !== 1 || !languageScope.scopes.includes(CodeLensScopes.Document)) { - symbols = (await commands.executeCommand( - BuiltInCommands.ExecuteDocumentSymbolProvider, - document.uri - )) as SymbolInformation[]; - } - } - - if (token.isCancellationRequested) return lenses; - - const documentRangeFn = Functions.once(() => document.validateRange(new Range(0, 1000000, 1000000, 1000000))); - - // Since blame information isn't valid when there are unsaved changes -- update the lenses appropriately - const dirtyCommand = dirty ? ({ title: this.getDirtyTitle(cfg) } as Command) : undefined; - - if (symbols !== undefined) { - Logger.log('GitCodeLensProvider.provideCodeLenses:', `${symbols.length} symbol(s) found`); - symbols.forEach(sym => - this.provideCodeLens( - lenses, - document, - sym, - languageScope as Required, - documentRangeFn, - blame, - gitUri, - cfg, - dirty, - dirtyCommand - ) - ); - } - - if ( - (languageScope.scopes.includes(CodeLensScopes.Document) || languageScope.symbolScopes.includes('file')) && - !languageScope.symbolScopes.includes('!file') - ) { - // Check if we have a lens for the whole document -- if not add one - if (!lenses.find(l => l.range.start.line === 0 && l.range.end.line === 0)) { - const blameRange = documentRangeFn(); - - let blameForRangeFn: (() => GitBlameLines | undefined) | undefined = undefined; - if (dirty || cfg.recentChange.enabled) { - if (!dirty) { - blameForRangeFn = Functions.once(() => - this._git.getBlameForRangeSync(blame!, gitUri!, blameRange) - ); - } - - const fileSymbol = new SymbolInformation( - gitUri.getFilename(), - SymbolKind.File, - '', - new Location(gitUri.documentUri(), new Range(0, 0, 0, blameRange.start.character)) - ); - lenses.push( - new GitRecentChangeCodeLens( - document.languageId, - fileSymbol, - gitUri, - blameForRangeFn, - blameRange, - true, - getRangeFromSymbol(fileSymbol), - cfg.recentChange.command, - dirtyCommand - ) - ); - } - if (!dirty && cfg.authors.enabled) { - if (blameForRangeFn === undefined) { - blameForRangeFn = Functions.once(() => - this._git.getBlameForRangeSync(blame!, gitUri!, blameRange) - ); - } - - const fileSymbol = new SymbolInformation( - gitUri.getFilename(), - SymbolKind.File, - '', - new Location(gitUri.documentUri(), new Range(0, 1, 0, blameRange.start.character)) - ); - lenses.push( - new GitAuthorsCodeLens( - document.languageId, - fileSymbol, - gitUri, - blameForRangeFn, - blameRange, - true, - getRangeFromSymbol(fileSymbol), - cfg.authors.command - ) - ); - } - } - } - - return lenses; - } - - private validateSymbolAndGetBlameRange( - symbol: SymbolInformation, - languageScope: Required, - documentRangeFn: () => Range - ): Range | undefined { - let valid = false; - let range: Range | undefined; - - const symbolName = SymbolKind[symbol.kind].toLowerCase(); - switch (symbol.kind) { - case SymbolKind.File: - if ( - languageScope.scopes.includes(CodeLensScopes.Containers) || - languageScope.symbolScopes!.includes(symbolName) - ) { - valid = !languageScope.symbolScopes!.includes(`!${symbolName}`); - } - - if (valid) { - // Adjust the range to be for the whole file - range = documentRangeFn(); - } - break; - - case SymbolKind.Package: - if ( - languageScope.scopes.includes(CodeLensScopes.Containers) || - languageScope.symbolScopes!.includes(symbolName) - ) { - valid = !languageScope.symbolScopes!.includes(`!${symbolName}`); - } - - if (valid) { - // Adjust the range to be for the whole file - if (getRangeFromSymbol(symbol).start.line === 0 && getRangeFromSymbol(symbol).end.line === 0) { - range = documentRangeFn(); - } - } - break; - - case SymbolKind.Class: - case SymbolKind.Interface: - case SymbolKind.Module: - case SymbolKind.Namespace: - case SymbolKind.Struct: - if ( - languageScope.scopes.includes(CodeLensScopes.Containers) || - languageScope.symbolScopes!.includes(symbolName) - ) { - valid = !languageScope.symbolScopes!.includes(`!${symbolName}`); - } - break; - - case SymbolKind.Constructor: - case SymbolKind.Enum: - case SymbolKind.Function: - case SymbolKind.Method: - if ( - languageScope.scopes.includes(CodeLensScopes.Blocks) || - languageScope.symbolScopes!.includes(symbolName) - ) { - valid = !languageScope.symbolScopes!.includes(`!${symbolName}`); - } - break; - - default: - if (languageScope.symbolScopes!.includes(symbolName)) { - valid = !languageScope.symbolScopes!.includes(`!${symbolName}`); - } - break; - } - - return valid ? range || getRangeFromSymbol(symbol) : undefined; - } - - private provideCodeLens( - lenses: CodeLens[], - document: TextDocument, - symbol: SymbolInformation, - languageScope: Required, - documentRangeFn: () => Range, - blame: GitBlame | undefined, - gitUri: GitUri | undefined, - cfg: ICodeLensConfig, - dirty: boolean, - dirtyCommand: Command | undefined - ): void { - const blameRange = this.validateSymbolAndGetBlameRange(symbol, languageScope, documentRangeFn); - if (blameRange === undefined) return; - - const line = document.lineAt(getRangeFromSymbol(symbol).start); - // Make sure there is only 1 lens per line - if (lenses.length && lenses[lenses.length - 1].range.start.line === line.lineNumber) return; - - // Anchor the code lens to the start of the line -- so that the range won't change with edits (otherwise the code lens will be removed and re-added) - let startChar = 0; - - let blameForRangeFn: (() => GitBlameLines | undefined) | undefined; - if (dirty || cfg.recentChange.enabled) { - if (!dirty) { - blameForRangeFn = Functions.once(() => this._git.getBlameForRangeSync(blame!, gitUri!, blameRange)); - } - lenses.push( - new GitRecentChangeCodeLens( - document.languageId, - symbol, - gitUri, - blameForRangeFn, - blameRange, - false, - line.range.with(new Position(line.range.start.line, startChar)), - cfg.recentChange.command, - dirtyCommand - ) - ); - startChar++; - } - - if (cfg.authors.enabled) { - let multiline = !blameRange.isSingleLine; - // HACK for Omnisharp, since it doesn't return full ranges - if (!multiline && document.languageId === 'csharp') { - switch (symbol.kind) { - case SymbolKind.File: - break; - case SymbolKind.Package: - case SymbolKind.Module: - case SymbolKind.Namespace: - case SymbolKind.Class: - case SymbolKind.Interface: - case SymbolKind.Constructor: - case SymbolKind.Method: - case SymbolKind.Function: - case SymbolKind.Enum: - multiline = true; - break; - } - } - - if (multiline && !dirty) { - if (blameForRangeFn === undefined) { - blameForRangeFn = Functions.once(() => this._git.getBlameForRangeSync(blame!, gitUri!, blameRange)); - } - lenses.push( - new GitAuthorsCodeLens( - document.languageId, - symbol, - gitUri, - blameForRangeFn, - blameRange, - false, - line.range.with(new Position(line.range.start.line, startChar)), - cfg.authors.command - ) - ); - } - } - } - - resolveCodeLens(lens: CodeLens, token: CancellationToken): CodeLens | Thenable { - if (lens instanceof GitRecentChangeCodeLens) return this.resolveGitRecentChangeCodeLens(lens, token); - if (lens instanceof GitAuthorsCodeLens) return this.resolveGitAuthorsCodeLens(lens, token); - return Promise.reject(undefined); - } - - private resolveGitRecentChangeCodeLens(lens: GitRecentChangeCodeLens, token: CancellationToken): CodeLens { - const blame = lens.getBlame(); - if (blame === undefined) return lens; - - const recentCommit = Iterables.first(blame.commits.values()); - let title = `${recentCommit.author}, ${recentCommit.formattedDate}`; - if (Container.config.debug) { - title += ` [${lens.languageId}: ${SymbolKind[lens.symbol.kind]}(${lens.range.start.character}-${ - lens.range.end.character - }${lens.symbol.containerName ? `|${lens.symbol.containerName}` : ''}), Lines (${lens.blameRange.start.line + - 1}-${lens.blameRange.end.line + 1}), Commit (${recentCommit.shortSha})]`; - } - - switch (lens.desiredCommand) { - case CodeLensCommand.DiffWithPrevious: - return this.applyDiffWithPreviousCommand(title, lens, blame, recentCommit); - case CodeLensCommand.ShowQuickCommitDetails: - return this.applyShowQuickCommitDetailsCommand( - title, - lens, - blame, - recentCommit - ); - case CodeLensCommand.ShowQuickCommitFileDetails: - return this.applyShowQuickCommitFileDetailsCommand( - title, - lens, - blame, - recentCommit - ); - case CodeLensCommand.ShowQuickCurrentBranchHistory: - return this.applyShowQuickCurrentBranchHistoryCommand( - title, - lens, - blame, - recentCommit - ); - case CodeLensCommand.ShowQuickFileHistory: - return this.applyShowQuickFileHistoryCommand(title, lens, blame, recentCommit); - case CodeLensCommand.ToggleFileBlame: - return this.applyToggleFileBlameCommand(title, lens, blame); - default: - return lens; - } - } - - private resolveGitAuthorsCodeLens(lens: GitAuthorsCodeLens, token: CancellationToken): CodeLens { - const blame = lens.getBlame(); - if (blame === undefined) return lens; - - const count = blame.authors.size; - let title = `${count} ${count > 1 ? 'authors' : 'author'} (${Iterables.first(blame.authors.values()).name}${ - count > 1 ? ' and others' : '' - })`; - if (Container.config.debug) { - title += ` [${lens.languageId}: ${SymbolKind[lens.symbol.kind]}(${lens.range.start.character}-${ - lens.range.end.character - }${lens.symbol.containerName ? `|${lens.symbol.containerName}` : ''}), Lines (${lens.blameRange.start.line + - 1}-${lens.blameRange.end.line + 1}), Authors (${Iterables.join( - Iterables.map(blame.authors.values(), a => a.name), - ', ' - )})]`; - } - - switch (lens.desiredCommand) { - case CodeLensCommand.DiffWithPrevious: - return this.applyDiffWithPreviousCommand(title, lens, blame); - case CodeLensCommand.ShowQuickCommitDetails: - return this.applyShowQuickCommitDetailsCommand(title, lens, blame); - case CodeLensCommand.ShowQuickCommitFileDetails: - return this.applyShowQuickCommitFileDetailsCommand(title, lens, blame); - case CodeLensCommand.ShowQuickCurrentBranchHistory: - return this.applyShowQuickCurrentBranchHistoryCommand(title, lens, blame); - case CodeLensCommand.ShowQuickFileHistory: - return this.applyShowQuickFileHistoryCommand(title, lens, blame); - case CodeLensCommand.ToggleFileBlame: - return this.applyToggleFileBlameCommand(title, lens, blame); - default: - return lens; - } - } - - private applyDiffWithPreviousCommand( - title: string, - lens: T, - blame: GitBlameLines, - commit?: GitBlameCommit - ): T { - if (commit === undefined) { - const blameLine = blame.allLines[lens.range.start.line]; - commit = blame.commits.get(blameLine.sha); - } - - lens.command = { - title: title, - command: Commands.DiffWithPrevious, - arguments: [ - Uri.file(lens.uri!.fsPath), - { - commit: commit - } as DiffWithPreviousCommandArgs - ] - }; - return lens; - } - - private applyShowQuickCommitDetailsCommand( - title: string, - lens: T, - blame: GitBlameLines, - commit?: GitBlameCommit - ): T { - lens.command = { - title: title, - command: commit !== undefined && commit.isUncommitted ? '' : CodeLensCommand.ShowQuickCommitDetails, - arguments: [ - Uri.file(lens.uri!.fsPath), - { - commit, - sha: commit === undefined ? undefined : commit.sha - } as ShowQuickCommitDetailsCommandArgs - ] - }; - return lens; - } - - private applyShowQuickCommitFileDetailsCommand( - title: string, - lens: T, - blame: GitBlameLines, - commit?: GitBlameCommit - ): T { - lens.command = { - title: title, - command: commit !== undefined && commit.isUncommitted ? '' : CodeLensCommand.ShowQuickCommitFileDetails, - arguments: [ - Uri.file(lens.uri!.fsPath), - { - commit, - sha: commit === undefined ? undefined : commit.sha - } as ShowQuickCommitFileDetailsCommandArgs - ] - }; - return lens; - } - - private applyShowQuickCurrentBranchHistoryCommand( - title: string, - lens: T, - blame: GitBlameLines, - commit?: GitBlameCommit - ): T { - lens.command = { - title: title, - command: CodeLensCommand.ShowQuickCurrentBranchHistory, - arguments: [Uri.file(lens.uri!.fsPath)] - }; - return lens; - } - - private applyShowQuickFileHistoryCommand( - title: string, - lens: T, - blame: GitBlameLines, - commit?: GitBlameCommit - ): T { - lens.command = { - title: title, - command: CodeLensCommand.ShowQuickFileHistory, - arguments: [ - Uri.file(lens.uri!.fsPath), - { - range: lens.isFullRange ? undefined : lens.blameRange - } as ShowQuickFileHistoryCommandArgs - ] - }; - return lens; - } - - private applyToggleFileBlameCommand( - title: string, - lens: T, - blame: GitBlameLines - ): T { - lens.command = { - title: title, - command: Commands.ToggleFileBlame, - arguments: [Uri.file(lens.uri!.fsPath)] - }; - return lens; - } - - private getDirtyTitle(cfg: ICodeLensConfig) { - if (cfg.recentChange.enabled && cfg.authors.enabled) { - return Container.config.strings.codeLens.unsavedChanges.recentChangeAndAuthors; - } - if (cfg.recentChange.enabled) return Container.config.strings.codeLens.unsavedChanges.recentChangeOnly; - return Container.config.strings.codeLens.unsavedChanges.authorsOnly; - } -} - -function getRangeFromSymbol(symbol: SymbolInformation) { - // Normalize the range to deal with the new api - return (symbol.location && symbol.location.range) || (symbol as any).range; -} diff --git a/src/commands/common.ts b/src/commands/common.ts index 5e2826c..707fcf1 100644 --- a/src/commands/common.ts +++ b/src/commands/common.ts @@ -15,7 +15,7 @@ import { } from 'vscode'; import { BuiltInCommands, DocumentSchemes, ImageMimetypes } from '../constants'; import { Container } from '../container'; -import { GitBranch, GitCommit, GitRemote, GitUri } from '../gitService'; +import { GitBranch, GitCommit, GitRemote, GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { CommandQuickPickItem, RepositoriesQuickPick } from '../quickpicks'; // import { Telemetry } from '../telemetry'; diff --git a/src/commands/copyMessageToClipboard.ts b/src/commands/copyMessageToClipboard.ts index f5fab11..6165ee1 100644 --- a/src/commands/copyMessageToClipboard.ts +++ b/src/commands/copyMessageToClipboard.ts @@ -1,7 +1,7 @@ 'use strict'; import { TextEditor, Uri, window } from 'vscode'; import { Container } from '../container'; -import { GitUri } from '../gitService'; +import { GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { Iterables } from '../system'; import { ActiveEditorCommand, CommandContext, Commands, getCommandUri, isCommandViewContextWithCommit } from './common'; diff --git a/src/commands/copyShaToClipboard.ts b/src/commands/copyShaToClipboard.ts index e0bd04a..0da464a 100644 --- a/src/commands/copyShaToClipboard.ts +++ b/src/commands/copyShaToClipboard.ts @@ -1,7 +1,7 @@ 'use strict'; import { TextEditor, Uri, window } from 'vscode'; import { Container } from '../container'; -import { GitUri } from '../gitService'; +import { GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { Iterables } from '../system'; import { ActiveEditorCommand, CommandContext, Commands, getCommandUri, isCommandViewContextWithCommit } from './common'; diff --git a/src/commands/diffLineWithPrevious.ts b/src/commands/diffLineWithPrevious.ts index 9402a94..c3ce062 100644 --- a/src/commands/diffLineWithPrevious.ts +++ b/src/commands/diffLineWithPrevious.ts @@ -1,7 +1,7 @@ 'use strict'; import { commands, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; import { Container } from '../container'; -import { GitCommit, GitService, GitUri } from '../gitService'; +import { GitCommit, GitService, GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { ActiveEditorCommand, Commands, getCommandUri } from './common'; diff --git a/src/commands/diffLineWithWorking.ts b/src/commands/diffLineWithWorking.ts index 87b2d9a..c835d06 100644 --- a/src/commands/diffLineWithWorking.ts +++ b/src/commands/diffLineWithWorking.ts @@ -1,7 +1,7 @@ 'use strict'; import { commands, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; import { Container } from '../container'; -import { GitCommit, GitService, GitUri } from '../gitService'; +import { GitCommit, GitService, GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { ActiveEditorCommand, Commands, getCommandUri } from './common'; diff --git a/src/commands/diffWith.ts b/src/commands/diffWith.ts index 7555ac1..2a565d7 100644 --- a/src/commands/diffWith.ts +++ b/src/commands/diffWith.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { commands, Range, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window } from 'vscode'; import { BuiltInCommands, GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitCommit, GitService, GitUri } from '../gitService'; +import { GitCommit, GitService, GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { ActiveEditorCommand, Commands } from './common'; diff --git a/src/commands/diffWithBranch.ts b/src/commands/diffWithBranch.ts index 25b3b02..fe05795 100644 --- a/src/commands/diffWithBranch.ts +++ b/src/commands/diffWithBranch.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { commands, TextDocumentShowOptions, TextEditor, Uri } from 'vscode'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitUri } from '../gitService'; +import { GitUri } from '../git/gitService'; import { Messages } from '../messages'; import { BranchesAndTagsQuickPick, CommandQuickPickItem } from '../quickpicks'; import { Strings } from '../system'; diff --git a/src/commands/diffWithNext.ts b/src/commands/diffWithNext.ts index d24b0f9..8f6040d 100644 --- a/src/commands/diffWithNext.ts +++ b/src/commands/diffWithNext.ts @@ -1,7 +1,7 @@ 'use strict'; import { commands, Range, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; import { Container } from '../container'; -import { GitLogCommit, GitService, GitStatusFile, GitUri } from '../gitService'; +import { GitLogCommit, GitService, GitStatusFile, GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { Iterables } from '../system'; diff --git a/src/commands/diffWithPrevious.ts b/src/commands/diffWithPrevious.ts index c3d4de4..70d821f 100644 --- a/src/commands/diffWithPrevious.ts +++ b/src/commands/diffWithPrevious.ts @@ -1,7 +1,7 @@ 'use strict'; import { commands, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; import { Container } from '../container'; -import { GitCommit, GitService, GitUri } from '../gitService'; +import { GitCommit, GitService, GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { Iterables } from '../system'; diff --git a/src/commands/diffWithRevision.ts b/src/commands/diffWithRevision.ts index 72f7ed8..6dc8b15 100644 --- a/src/commands/diffWithRevision.ts +++ b/src/commands/diffWithRevision.ts @@ -2,7 +2,7 @@ import { commands, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitBranch, GitTag, GitUri } from '../gitService'; +import { GitBranch, GitTag, GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { ChooseFromBranchesAndTagsQuickPickItem, CommandQuickPickItem, FileHistoryQuickPick } from '../quickpicks'; diff --git a/src/commands/diffWithWorking.ts b/src/commands/diffWithWorking.ts index d8255a4..808752a 100644 --- a/src/commands/diffWithWorking.ts +++ b/src/commands/diffWithWorking.ts @@ -1,7 +1,7 @@ 'use strict'; import { commands, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; import { Container } from '../container'; -import { GitCommit, GitService, GitUri } from '../gitService'; +import { GitCommit, GitService, GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { ActiveEditorCommand, Commands, getCommandUri } from './common'; diff --git a/src/commands/openBranchInRemote.ts b/src/commands/openBranchInRemote.ts index 980bcbb..4357966 100644 --- a/src/commands/openBranchInRemote.ts +++ b/src/commands/openBranchInRemote.ts @@ -2,7 +2,7 @@ import { commands, TextEditor, Uri, window } from 'vscode'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitUri } from '../gitService'; +import { GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { BranchesQuickPick, CommandQuickPickItem } from '../quickpicks'; import { diff --git a/src/commands/openBranchesInRemote.ts b/src/commands/openBranchesInRemote.ts index cc05402..9a4487e 100644 --- a/src/commands/openBranchesInRemote.ts +++ b/src/commands/openBranchesInRemote.ts @@ -2,7 +2,7 @@ import { commands, TextEditor, Uri, window } from 'vscode'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitUri } from '../gitService'; +import { GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { ActiveEditorCommand, diff --git a/src/commands/openCommitInRemote.ts b/src/commands/openCommitInRemote.ts index e4d854a..b8d9e36 100644 --- a/src/commands/openCommitInRemote.ts +++ b/src/commands/openCommitInRemote.ts @@ -1,7 +1,7 @@ 'use strict'; import { commands, TextEditor, Uri, window } from 'vscode'; import { Container } from '../container'; -import { GitUri } from '../gitService'; +import { GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { ActiveEditorCommand, CommandContext, Commands, getCommandUri, isCommandViewContextWithCommit } from './common'; diff --git a/src/commands/openFileInRemote.ts b/src/commands/openFileInRemote.ts index b48dca8..1fb9bba 100644 --- a/src/commands/openFileInRemote.ts +++ b/src/commands/openFileInRemote.ts @@ -2,7 +2,7 @@ import { commands, Range, TextEditor, Uri, window } from 'vscode'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitUri } from '../gitService'; +import { GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { BranchesQuickPick, CommandQuickPickItem } from '../quickpicks'; import { diff --git a/src/commands/openFileRevision.ts b/src/commands/openFileRevision.ts index e0fcde4..11c7fcc 100644 --- a/src/commands/openFileRevision.ts +++ b/src/commands/openFileRevision.ts @@ -3,7 +3,7 @@ import { CancellationTokenSource, commands, Range, TextDocumentShowOptions, Text import { FileAnnotationType } from '../configuration'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitBranch, GitTag, GitUri } from '../gitService'; +import { GitBranch, GitTag, GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { ChooseFromBranchesAndTagsQuickPickItem, CommandQuickPickItem, FileHistoryQuickPick } from '../quickpicks'; diff --git a/src/commands/openInRemote.ts b/src/commands/openInRemote.ts index 3f57784..2c17c55 100644 --- a/src/commands/openInRemote.ts +++ b/src/commands/openInRemote.ts @@ -1,7 +1,7 @@ 'use strict'; import { TextEditor, Uri, window } from 'vscode'; import { GlyphChars } from '../constants'; -import { GitLogCommit, GitRemote, GitService, RemoteResource, RemoteResourceType } from '../gitService'; +import { GitLogCommit, GitRemote, GitService, RemoteResource, RemoteResourceType } from '../git/gitService'; import { Logger } from '../logger'; import { CommandQuickPickItem, OpenRemoteCommandQuickPickItem, RemotesQuickPick } from '../quickpicks'; import { Strings } from '../system'; diff --git a/src/commands/openRepoInRemote.ts b/src/commands/openRepoInRemote.ts index 97ae6cf..60549ff 100644 --- a/src/commands/openRepoInRemote.ts +++ b/src/commands/openRepoInRemote.ts @@ -2,7 +2,7 @@ import { commands, TextEditor, Uri, window } from 'vscode'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitUri } from '../gitService'; +import { GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { ActiveEditorCommand, diff --git a/src/commands/openWorkingFile.ts b/src/commands/openWorkingFile.ts index bae37fa..daf1441 100644 --- a/src/commands/openWorkingFile.ts +++ b/src/commands/openWorkingFile.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { Range, TextDocumentShowOptions, TextEditor, Uri, window } from 'vscode'; import { FileAnnotationType } from '../configuration'; import { Container } from '../container'; -import { GitUri } from '../gitService'; +import { GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { ActiveEditorCommand, Commands, getCommandUri, openEditor } from './common'; diff --git a/src/commands/showCommitSearch.ts b/src/commands/showCommitSearch.ts index 75f0a82..762a712 100644 --- a/src/commands/showCommitSearch.ts +++ b/src/commands/showCommitSearch.ts @@ -2,7 +2,7 @@ import { commands, InputBoxOptions, TextEditor, Uri, window } from 'vscode'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitRepoSearchBy, GitService, GitUri } from '../gitService'; +import { GitRepoSearchBy, GitService, GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { CommandQuickPickItem, CommitsQuickPick, ShowCommitsSearchInResultsQuickPickItem } from '../quickpicks'; import { Strings } from '../system'; diff --git a/src/commands/showQuickBranchHistory.ts b/src/commands/showQuickBranchHistory.ts index 2998ba3..7ac81ce 100644 --- a/src/commands/showQuickBranchHistory.ts +++ b/src/commands/showQuickBranchHistory.ts @@ -2,7 +2,7 @@ import { commands, TextEditor, Uri, window } from 'vscode'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitLog, GitUri } from '../gitService'; +import { GitLog, GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { BranchesQuickPick, BranchHistoryQuickPick, CommandQuickPickItem } from '../quickpicks'; import { Strings } from '../system'; diff --git a/src/commands/showQuickCommitDetails.ts b/src/commands/showQuickCommitDetails.ts index e7e763e..709b1a6 100644 --- a/src/commands/showQuickCommitDetails.ts +++ b/src/commands/showQuickCommitDetails.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { commands, TextEditor, Uri, window } from 'vscode'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitCommit, GitLog, GitLogCommit, GitUri } from '../gitService'; +import { GitCommit, GitLog, GitLogCommit, GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { CommandQuickPickItem, CommitQuickPick, CommitWithFileStatusQuickPickItem } from '../quickpicks'; diff --git a/src/commands/showQuickCommitFileDetails.ts b/src/commands/showQuickCommitFileDetails.ts index 55e5c4c..d900918 100644 --- a/src/commands/showQuickCommitFileDetails.ts +++ b/src/commands/showQuickCommitFileDetails.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { TextEditor, Uri, window } from 'vscode'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitCommit, GitLog, GitLogCommit, GitService, GitUri } from '../gitService'; +import { GitCommit, GitLog, GitLogCommit, GitService, GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { CommandQuickPickItem, CommitFileQuickPick } from '../quickpicks'; diff --git a/src/commands/showQuickFileHistory.ts b/src/commands/showQuickFileHistory.ts index 0140b30..741e8b2 100644 --- a/src/commands/showQuickFileHistory.ts +++ b/src/commands/showQuickFileHistory.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { commands, Range, TextEditor, Uri, window } from 'vscode'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitBranch, GitLog, GitTag, GitUri } from '../gitService'; +import { GitBranch, GitLog, GitTag, GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { Messages } from '../messages'; import { diff --git a/src/commands/stashApply.ts b/src/commands/stashApply.ts index 13d788e..899ba72 100644 --- a/src/commands/stashApply.ts +++ b/src/commands/stashApply.ts @@ -2,7 +2,7 @@ import { MessageItem, window } from 'vscode'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitStashCommit } from '../gitService'; +import { GitStashCommit } from '../git/gitService'; import { Logger } from '../logger'; import { CommandQuickPickItem, RepositoriesQuickPick, StashListQuickPick } from '../quickpicks'; import { Strings } from '../system'; diff --git a/src/commands/stashDelete.ts b/src/commands/stashDelete.ts index 9c94fec..837fd58 100644 --- a/src/commands/stashDelete.ts +++ b/src/commands/stashDelete.ts @@ -2,7 +2,7 @@ import { MessageItem, window } from 'vscode'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitStashCommit } from '../gitService'; +import { GitStashCommit } from '../git/gitService'; import { Logger } from '../logger'; import { CommandQuickPickItem } from '../quickpicks'; import { Command, CommandContext, Commands, isCommandViewContextWithCommit } from './common'; diff --git a/src/configuration.ts b/src/configuration.ts index f92f053..f953b84 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -12,7 +12,7 @@ import { } from 'vscode'; import { CommandContext, extensionId, setCommandContext } from './constants'; import { Container } from './container'; -import { clearGravatarCache } from './gitService'; +import { clearGravatarCache } from './git/gitService'; import { Functions } from './system'; import { IConfig, KeyMap } from './ui/config'; diff --git a/src/container.ts b/src/container.ts index b34c877..10ca797 100644 --- a/src/container.ts +++ b/src/container.ts @@ -5,7 +5,7 @@ import { LineAnnotationController } from './annotations/lineAnnotationController import { CodeLensController } from './codelens/codeLensController'; import { configuration, IConfig } from './configuration'; import { GitFileSystemProvider } from './git/fsProvider'; -import { GitService } from './gitService'; +import { GitService } from './git/gitService'; import { LineHoverController } from './hovers/lineHoverController'; import { Keyboard } from './keyboard'; import { StatusBarController } from './statusbar/statusBarController'; diff --git a/src/extension.ts b/src/extension.ts index bb16880..6ea20a5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -15,7 +15,7 @@ import { } from './configuration'; import { CommandContext, extensionQualifiedId, GlobalState, GlyphChars, setCommandContext } from './constants'; import { Container } from './container'; -import { GitService } from './gitService'; +import { GitService } from './git/gitService'; import { Logger } from './logger'; import { Messages } from './messages'; import { Strings, Versions } from './system'; diff --git a/src/git/fsProvider.ts b/src/git/fsProvider.ts index 200c44e..bfdb19e 100644 --- a/src/git/fsProvider.ts +++ b/src/git/fsProvider.ts @@ -13,7 +13,7 @@ import { } from 'vscode'; import { DocumentSchemes } from '../constants'; import { Container } from '../container'; -import { GitService, GitTree, GitUri } from '../gitService'; +import { GitService, GitTree, GitUri } from '../git/gitService'; import { Iterables, TernarySearchTree } from '../system'; export function fromGitLensFSUri(uri: Uri): { path: string; ref: string; repoPath: string } { diff --git a/src/git/git.ts b/src/git/git.ts index fb03479..af049ac 100644 --- a/src/git/git.ts +++ b/src/git/git.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { GlyphChars } from '../constants'; import { Logger } from '../logger'; import { Objects, Strings } from '../system'; -import { findGitPath, IGitInfo } from './gitLocator'; +import { findGitPath, IGitInfo } from './locator'; import { run, RunOptions } from './shell'; export { IGitInfo }; diff --git a/src/git/gitLocator.ts b/src/git/gitLocator.ts deleted file mode 100644 index a2d3c98..0000000 --- a/src/git/gitLocator.ts +++ /dev/null @@ -1,84 +0,0 @@ -'use strict'; -// import { findActualExecutable, spawnPromise } from 'spawn-rx'; -import * as path from 'path'; -import { findExecutable, run } from './shell'; - -export interface IGitInfo { - path: string; - version: string; -} - -function parseVersion(raw: string): string { - return raw.replace(/^git version /, ''); -} - -async function findSpecificGit(path: string): Promise { - const version = await run(path, ['--version'], 'utf8'); - // If needed, let's update our path to avoid the search on every command - if (!path || path === 'git') { - path = findExecutable(path, ['--version']).cmd; - } - - return { - path, - version: parseVersion(version.trim()) - }; -} - -async function findGitDarwin(): Promise { - try { - let path = await run('which', ['git'], 'utf8'); - path = path.replace(/^\s+|\s+$/g, ''); - - if (path !== '/usr/bin/git') { - return findSpecificGit(path); - } - - try { - await run('xcode-select', ['-p'], 'utf8'); - return findSpecificGit(path); - } - catch (ex) { - if (ex.code === 2) { - return Promise.reject(new Error('Unable to find git')); - } - return findSpecificGit(path); - } - } - catch (ex) { - return Promise.reject(new Error('Unable to find git')); - } -} - -function findSystemGitWin32(basePath: string): Promise { - if (!basePath) return Promise.reject(new Error('Unable to find git')); - return findSpecificGit(path.join(basePath, 'Git', 'cmd', 'git.exe')); -} - -function findGitWin32(): Promise { - return findSystemGitWin32(process.env['ProgramW6432']!) - .then(null, () => findSystemGitWin32(process.env['ProgramFiles(x86)']!)) - .then(null, () => findSystemGitWin32(process.env['ProgramFiles']!)) - .then(null, () => findSpecificGit('git')); -} - -export async function findGitPath(path?: string): Promise { - try { - return await findSpecificGit(path || 'git'); - } - catch (ex) { - try { - switch (process.platform) { - case 'darwin': - return await findGitDarwin(); - case 'win32': - return await findGitWin32(); - default: - return Promise.reject('Unable to find git'); - } - } - catch (ex) { - return Promise.reject(new Error('Unable to find git')); - } - } -} diff --git a/src/git/gitService.ts b/src/git/gitService.ts new file mode 100644 index 0000000..7c578f8 --- /dev/null +++ b/src/git/gitService.ts @@ -0,0 +1,1946 @@ +'use strict'; +import * as fs from 'fs'; +import * as path from 'path'; +import { + ConfigurationChangeEvent, + Disposable, + Event, + EventEmitter, + extensions, + Range, + TextEditor, + Uri, + window, + WindowState, + workspace, + WorkspaceFolder, + WorkspaceFoldersChangeEvent +} from 'vscode'; +import { GitExtension } from '../@types/git'; +import { configuration, IRemotesConfig } from '../configuration'; +import { CommandContext, DocumentSchemes, GlyphChars, setCommandContext } from '../constants'; +import { Container } from '../container'; +import { Logger } from '../logger'; +import { Iterables, Objects, Strings, TernarySearchTree, Versions } from '../system'; +import { CachedBlame, CachedDiff, CachedLog, GitDocumentState, TrackedDocument } from '../trackers/gitDocumentTracker'; +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, + GitTree, + GitTreeParser, + Repository, + RepositoryChange +} from './git'; +import { GitUri, IGitCommitInfo } from './gitUri'; +import { RemoteProviderFactory, RemoteProviderMap } from './remotes/factory'; + +export { GitUri, IGitCommitInfo }; +export * from './models/models'; +export * from './formatters/formatters'; +export { getNameFromRemoteResource, RemoteProvider, RemoteResource, RemoteResourceType } from './remotes/provider'; +export { RemoteProviderFactory } from './remotes/factory'; + +const RepoSearchWarnings = { + doesNotExist: /no such file or directory/i +}; + +const userConfigRegex = /^user\.(name|email) (.*)$/gm; +const mappedAuthorRegex = /(.+)\s<(.+)>/; + +export enum GitRepoSearchBy { + Author = 'author', + ChangedLines = 'changed-lines', + Changes = 'changes', + Files = 'files', + Message = 'message', + Sha = 'sha' +} + +export class GitService implements Disposable { + static emptyPromise: Promise = Promise.resolve(undefined); + static deletedSha = 'ffffffffffffffffffffffffffffffffffffffff'; + static stagedUncommittedSha = Git.stagedUncommittedSha; + static uncommittedSha = Git.uncommittedSha; + + private _onDidChangeRepositories = new EventEmitter(); + get onDidChangeRepositories(): Event { + return this._onDidChangeRepositories.event; + } + + private readonly _disposable: Disposable; + private readonly _repositoryTree: TernarySearchTree; + private _repositoriesLoadingPromise: Promise | undefined; + private _suspended: boolean = false; + private readonly _trackedCache: Map>; + private _versionedUriCache: Map; + + constructor() { + this._repositoryTree = TernarySearchTree.forPaths(); + this._trackedCache = new Map(); + this._versionedUriCache = new Map(); + + this._disposable = Disposable.from( + window.onDidChangeWindowState(this.onWindowStateChanged, this), + workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this), + configuration.onDidChange(this.onConfigurationChanged, this) + ); + this.onConfigurationChanged(configuration.initializingChangeEvent); + + this._repositoriesLoadingPromise = this.onWorkspaceFoldersChanged(); + } + + dispose() { + this._repositoryTree.forEach(r => r.dispose()); + this._trackedCache.clear(); + this._versionedUriCache.clear(); + + this._disposable && this._disposable.dispose(); + } + + get UseCaching() { + return Container.config.advanced.caching.enabled; + } + + private onAnyRepositoryChanged(repo: Repository, reason: RepositoryChange) { + this._trackedCache.clear(); + + if (reason === RepositoryChange.Config) { + this._userMapCache.delete(repo.path); + } + + if (reason === RepositoryChange.Closed) { + // Send a notification that the repositories changed + setImmediate(async () => { + await this.updateContext(this._repositoryTree); + + this.fireRepositoriesChanged(); + }); + } + } + + private onConfigurationChanged(e: ConfigurationChangeEvent) { + const initializing = configuration.initializing(e); + + if ( + initializing || + configuration.changed(e, configuration.name('defaultDateStyle').value) || + configuration.changed(e, configuration.name('defaultDateFormat').value) + ) { + CommitFormatting.reset(); + } + } + + private onWindowStateChanged(e: WindowState) { + if (e.focused) { + this._repositoryTree.forEach(r => r.resume()); + } + else { + this._repositoryTree.forEach(r => r.suspend()); + } + + this._suspended = !e.focused; + } + + private async onWorkspaceFoldersChanged(e?: WorkspaceFoldersChangeEvent) { + let initializing = false; + if (e === undefined) { + initializing = true; + e = { + added: workspace.workspaceFolders || [], + removed: [] + } as WorkspaceFoldersChangeEvent; + + Logger.log(`Starting repository search in ${e.added.length} folders`); + } + + for (const f of e.added) { + if (f.uri.scheme !== DocumentSchemes.File) continue; + + // Search for and add all repositories (nested and/or submodules) + const repositories = await this.repositorySearch(f); + for (const r of repositories) { + this._repositoryTree.set(r.path, r); + } + } + + for (const f of e.removed) { + if (f.uri.scheme !== DocumentSchemes.File) continue; + + const fsPath = f.uri.fsPath; + const repos = this._repositoryTree.findSuperstr(fsPath); + const reposToDelete = + repos !== undefined + ? // Since the filtered tree will have keys that are relative to the fsPath, normalize to the full path + [...Iterables.map(repos, r => [r, r.path])] + : []; + + // const filteredTree = this._repositoryTree.findSuperstr(fsPath); + // const reposToDelete = + // filteredTree !== undefined + // ? // Since the filtered tree will have keys that are relative to the fsPath, normalize to the full path + // [ + // ...Iterables.map<[Repository, string], [Repository, string]>( + // filteredTree.entries(), + // ([r, k]) => [r, path.join(fsPath, k)] + // ) + // ] + // : []; + + const repo = this._repositoryTree.get(fsPath); + if (repo !== undefined) { + reposToDelete.push([repo, fsPath]); + } + + for (const [r, k] of reposToDelete) { + this._repositoryTree.delete(k); + r.dispose(); + } + } + + await this.updateContext(this._repositoryTree); + + if (!initializing) { + // Defer the event trigger enough to let everything unwind + setImmediate(() => this.fireRepositoriesChanged()); + } + } + + private async repositorySearch(folder: WorkspaceFolder): Promise { + const folderUri = folder.uri; + + const depth = configuration.get( + configuration.name('advanced')('repositorySearchDepth').value, + folderUri + ); + + Logger.log(`Searching for repositories (depth=${depth}) in '${folderUri.fsPath}' ...`); + + const start = process.hrtime(); + + const repositories: Repository[] = []; + const anyRepoChangedFn = this.onAnyRepositoryChanged.bind(this); + + const rootPath = await this.getRepoPathCore(folderUri.fsPath, true); + if (rootPath !== undefined) { + Logger.log(`Repository found in '${rootPath}'`); + repositories.push(new Repository(folder, rootPath, true, anyRepoChangedFn, this._suspended)); + } + + if (depth <= 0) { + Logger.log( + `Completed repository search (depth=${depth}) in '${folderUri.fsPath}' ${ + GlyphChars.Dot + } ${Strings.getDurationMilliseconds(start)} ms` + ); + + return repositories; + } + + // Get any specified excludes -- this is a total hack, but works for some simple cases and something is better than nothing :) + let excludes = { + ...workspace.getConfiguration('files', folderUri).get<{ [key: string]: boolean }>('exclude', {}), + ...workspace.getConfiguration('search', folderUri).get<{ [key: string]: boolean }>('exclude', {}) + }; + + const excludedPaths = [ + ...Iterables.filterMap(Objects.entries(excludes), ([key, value]) => { + if (!value) return undefined; + if (key.startsWith('**/')) return key.substring(3); + return key; + }) + ]; + + excludes = excludedPaths.reduce( + (accumulator, current) => { + accumulator[current] = true; + return accumulator; + }, + Object.create(null) as any + ); + + let paths; + try { + paths = await this.repositorySearchCore(folderUri.fsPath, depth, excludes); + } + catch (ex) { + if (RepoSearchWarnings.doesNotExist.test(ex.message || '')) { + Logger.log( + `Repository search (depth=${depth}) in '${folderUri.fsPath}' FAILED${ + ex.message ? `(${ex.message})` : '' + }` + ); + } + else { + Logger.error(ex, `Repository search (depth=${depth}) in '${folderUri.fsPath}' FAILED`); + } + + return repositories; + } + + for (let p of paths) { + p = path.dirname(p); + // If we are the same as the root, skip it + if (Strings.normalizePath(p) === rootPath) continue; + + const rp = await this.getRepoPathCore(p, true); + if (rp === undefined) continue; + + Logger.log(`Repository found in '${rp}'`); + repositories.push(new Repository(folder, rp, false, anyRepoChangedFn, this._suspended)); + } + + Logger.log( + `Completed repository search (depth=${depth}) in '${folderUri.fsPath}' ${ + GlyphChars.Dot + } ${Strings.getDurationMilliseconds(start)} ms` + ); + + return repositories; + } + + private async repositorySearchCore( + root: string, + depth: number, + excludes: { [key: string]: boolean }, + repositories: string[] = [] + ): Promise { + return new Promise((resolve, reject) => { + fs.readdir(root, async (err, files) => { + if (err != null) { + reject(err); + return; + } + + if (files.length === 0) { + resolve(repositories); + return; + } + + const folders: string[] = []; + + const promises = files.map(file => { + const fullPath = path.resolve(root, file); + + return new Promise((res, rej) => { + fs.stat(fullPath, (err, stat) => { + if (file === '.git') { + repositories.push(fullPath); + } + else if (err == null && excludes[file] !== true && stat != null && stat.isDirectory()) { + folders.push(fullPath); + } + + res(); + }); + }); + }); + + await Promise.all(promises); + + if (depth-- > 0) { + for (const folder of folders) { + await this.repositorySearchCore(folder, depth, excludes, repositories); + } + } + + resolve(repositories); + }); + }); + } + + private async updateContext(repositoryTree: TernarySearchTree) { + const hasRepository = repositoryTree.any(); + await setCommandContext(CommandContext.Enabled, hasRepository); + + let hasRemotes = false; + if (hasRepository) { + for (const repo of repositoryTree.values()) { + hasRemotes = await repo.hasRemotes(); + if (hasRemotes) break; + } + } + + await setCommandContext(CommandContext.HasRemotes, hasRemotes); + + // If we have no repositories setup a watcher in case one is initialized + if (!hasRepository) { + const watcher = workspace.createFileSystemWatcher('**/.git', false, true, true); + const disposable = Disposable.from( + watcher, + watcher.onDidCreate(async uri => { + const f = workspace.getWorkspaceFolder(uri); + if (f === undefined) return; + + // Search for and add all repositories (nested and/or submodules) + const repositories = await this.repositorySearch(f); + if (repositories.length === 0) return; + + disposable.dispose(); + + for (const r of repositories) { + this._repositoryTree.set(r.path, r); + } + + await this.updateContext(this._repositoryTree); + + // Defer the event trigger enough to let everything unwind + setImmediate(() => this.fireRepositoriesChanged()); + }, this) + ); + } + } + + private fireRepositoriesChanged() { + this._onDidChangeRepositories.fire(); + } + + checkoutFile(uri: GitUri, ref?: string) { + ref = ref || uri.sha; + Logger.log(`checkoutFile('${uri.repoPath}', '${uri.fsPath}', '${ref}')`); + + return Git.checkout(uri.repoPath!, uri.fsPath, ref!); + } + + private async fileExists( + repoPath: string, + fileName: string, + options: { ensureCase: boolean } = { ensureCase: false } + ): Promise { + const filePath = path.resolve(repoPath, fileName); + const exists = await new Promise((resolve, reject) => fs.exists(filePath, resolve)); + if (!options.ensureCase || !exists) return exists; + + // Deal with renames in case only on case-insensative file systems + const normalizedRepoPath = path.normalize(repoPath); + return this.fileExistsWithCase(filePath, normalizedRepoPath, normalizedRepoPath.length); + } + + private async fileExistsWithCase(filePath: string, repoPath: string, repoPathLength: number): Promise { + const dir = path.dirname(filePath); + if (dir.length < repoPathLength) return false; + if (dir === repoPath) return true; + + const filenames = await new Promise((resolve, reject) => + fs.readdir(dir, (err: NodeJS.ErrnoException, files: string[]) => { + if (err) { + reject(err); + } + else { + resolve(files); + } + }) + ); + if (filenames.indexOf(path.basename(filePath)) === -1) { + return false; + } + return this.fileExistsWithCase(dir, repoPath, repoPathLength); + } + + async findNextCommit(repoPath: string, fileName: string, ref?: string): Promise { + let log = await this.getLogForFile(repoPath, fileName, { maxCount: 1, ref: ref, renames: true, reverse: true }); + let commit = log && Iterables.first(log.commits.values()); + if (commit) return commit; + + const nextFileName = await this.findNextFileName(repoPath, fileName, ref); + if (nextFileName) { + log = await this.getLogForFile(repoPath, nextFileName, { + maxCount: 1, + ref: ref, + renames: true, + reverse: true + }); + commit = log && Iterables.first(log.commits.values()); + } + + return commit; + } + + async findNextFileName(repoPath: string | undefined, fileName: string, ref?: string): Promise { + [fileName, repoPath] = Git.splitPath(fileName, repoPath); + + return (await this.fileExists(repoPath, fileName, { ensureCase: true })) + ? fileName + : await this.findNextFileNameCore(repoPath, fileName, ref); + } + + private async findNextFileNameCore(repoPath: string, fileName: string, ref?: string): Promise { + if (ref === undefined) { + // Get the most recent commit for this file name + ref = await this.getRecentShaForFile(repoPath, fileName); + if (ref === undefined) return undefined; + } + + // Get the full commit (so we can see if there are any matching renames in the file statuses) + const log = await this.getLog(repoPath, { maxCount: 1, ref: ref }); + if (log === undefined) return undefined; + + const c = Iterables.first(log.commits.values()); + const status = c.fileStatuses.find(f => f.originalFileName === fileName); + if (status === undefined) return undefined; + + return status.fileName; + } + + async findWorkingFileName(commit: GitCommit): Promise<[string | undefined, string | undefined]>; + async findWorkingFileName( + fileName: string, + repoPath?: string, + ref?: string + ): Promise<[string | undefined, string | undefined]>; + async findWorkingFileName( + commitOrFileName: GitCommit | string, + repoPath?: string, + ref?: string + ): Promise<[string | undefined, string | undefined]> { + let fileName; + if (typeof commitOrFileName === 'string') { + fileName = commitOrFileName; + if (repoPath === undefined) { + repoPath = await this.getRepoPath(fileName, { ref: ref }); + [fileName, repoPath] = Git.splitPath(fileName, repoPath); + } + else { + fileName = Strings.normalizePath(path.relative(repoPath, fileName)); + } + } + else { + const c = commitOrFileName; + repoPath = c.repoPath; + if (c.workingFileName && (await this.fileExists(repoPath, c.workingFileName, { ensureCase: true }))) { + return [c.workingFileName, repoPath]; + } + fileName = c.fileName; + } + + // Keep walking up to the most recent commit for a given filename, until it exists on disk + while (true) { + if (await this.fileExists(repoPath, fileName, { ensureCase: true })) return [fileName, repoPath]; + + fileName = await this.findNextFileNameCore(repoPath, fileName); + if (fileName === undefined) return [undefined, undefined]; + } + } + + async getActiveRepoPath(editor?: TextEditor): Promise { + editor = editor || window.activeTextEditor; + + let repoPath; + if (editor != null) { + const doc = await Container.tracker.getOrAdd(editor.document.uri); + if (doc !== undefined) { + repoPath = doc.uri.repoPath; + } + } + + if (repoPath != null) return repoPath; + + return this.getHighlanderRepoPath(); + } + + getHighlanderRepoPath(): string | undefined { + const entry = this._repositoryTree.highlander(); + if (entry === undefined) return undefined; + + const [repo] = entry; + return repo.path; + } + + async getBlameForFile(uri: GitUri): Promise { + let key = 'blame'; + if (uri.sha !== undefined) { + key += `:${uri.sha}`; + } + + const doc = await Container.tracker.getOrAdd(uri); + if (this.UseCaching) { + if (doc.state !== undefined) { + const cachedBlame = doc.state.get(key); + if (cachedBlame !== undefined) { + Logger.log(`getBlameForFile[Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`); + return cachedBlame.item; + } + } + + Logger.log(`getBlameForFile[Not Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`); + + if (doc.state === undefined) { + doc.state = new GitDocumentState(doc.key); + } + } + else { + Logger.log(`getBlameForFile('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`); + } + + const promise = this.getBlameForFileCore(uri, doc, key); + + if (doc.state !== undefined) { + Logger.log(`Add blame cache for '${doc.state.key}:${key}'`); + + doc.state.set(key, { + item: promise + } as CachedBlame); + } + + return promise; + } + + private async getBlameForFileCore( + uri: GitUri, + document: TrackedDocument, + key: string + ): Promise { + if (!(await this.isTracked(uri))) { + Logger.log(`Skipping blame; '${uri.fsPath}' is not tracked`); + return GitService.emptyPromise as Promise; + } + + const [file, root] = Git.splitPath(uri.fsPath, uri.repoPath, false); + + try { + const data = await Git.blame(root, file, uri.sha, { + args: Container.config.advanced.blame.customArguments, + ignoreWhitespace: Container.config.blame.ignoreWhitespace + }); + const blame = GitBlameParser.parse(data, root, file, await this.getCurrentUser(root)); + return blame; + } + catch (ex) { + // Trap and cache expected blame errors + if (document.state !== undefined) { + const msg = ex && ex.toString(); + Logger.log(`Replace blame cache with empty promise for '${document.state.key}:${key}'`); + + document.state.set(key, { + item: GitService.emptyPromise, + errorMessage: msg + } as CachedBlame); + + document.setBlameFailure(); + + return GitService.emptyPromise as Promise; + } + + return undefined; + } + } + + async getBlameForFileContents(uri: GitUri, contents: string): Promise { + const key = `blame:${Strings.sha1(contents)}`; + + const doc = await Container.tracker.getOrAdd(uri); + if (this.UseCaching) { + if (doc.state !== undefined) { + const cachedBlame = doc.state.get(key); + if (cachedBlame !== undefined) { + Logger.log( + `getBlameForFileContents[Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')` + ); + return cachedBlame.item; + } + } + + Logger.log(`getBlameForFileContents[Not Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`); + + if (doc.state === undefined) { + doc.state = new GitDocumentState(doc.key); + } + } + else { + Logger.log(`getBlameForFileContents('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`); + } + + const promise = this.getBlameForFileContentsCore(uri, contents, doc, key); + + if (doc.state !== undefined) { + Logger.log(`Add blame cache for '${doc.state.key}:${key}'`); + + doc.state.set(key, { + item: promise + } as CachedBlame); + } + + return promise; + } + + async getBlameForFileContentsCore( + uri: GitUri, + contents: string, + document: TrackedDocument, + key: string + ): Promise { + if (!(await this.isTracked(uri))) { + Logger.log(`Skipping blame; '${uri.fsPath}' is not tracked`); + return GitService.emptyPromise as Promise; + } + + const [file, root] = Git.splitPath(uri.fsPath, uri.repoPath, false); + + try { + const data = await Git.blame_contents(root, file, contents, { + args: Container.config.advanced.blame.customArguments, + correlationKey: `:${key}`, + ignoreWhitespace: Container.config.blame.ignoreWhitespace + }); + const blame = GitBlameParser.parse(data, root, file, await this.getCurrentUser(root)); + return blame; + } + catch (ex) { + // Trap and cache expected blame errors + if (document.state !== undefined) { + const msg = ex && ex.toString(); + Logger.log(`Replace blame cache with empty promise for '${document.state.key}:${key}'`); + + document.state.set(key, { + item: GitService.emptyPromise, + errorMessage: msg + } as CachedBlame); + + document.setBlameFailure(); + return GitService.emptyPromise as Promise; + } + + return undefined; + } + } + + async getBlameForLine( + uri: GitUri, + line: number, + options: { skipCache?: boolean } = {} + ): Promise { + Logger.log(`getBlameForLine('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}', ${line})`); + + if (!options.skipCache && this.UseCaching) { + const blame = await this.getBlameForFile(uri); + if (blame === undefined) return undefined; + + let blameLine = blame.lines[line]; + if (blameLine === undefined) { + if (blame.lines.length !== line) return undefined; + blameLine = blame.lines[line - 1]; + } + + const commit = blame.commits.get(blameLine.sha); + if (commit === undefined) return undefined; + + return { + author: { ...blame.authors.get(commit.author), lineCount: commit.lines.length }, + commit: commit, + line: blameLine + } as GitBlameLine; + } + + const lineToBlame = line + 1; + const fileName = uri.fsPath; + + try { + const data = await Git.blame(uri.repoPath, fileName, uri.sha, { + args: Container.config.advanced.blame.customArguments, + ignoreWhitespace: Container.config.blame.ignoreWhitespace, + startLine: lineToBlame, + endLine: lineToBlame + }); + const blame = GitBlameParser.parse(data, uri.repoPath, fileName, await this.getCurrentUser(uri.repoPath!)); + if (blame === undefined) return undefined; + + return { + author: Iterables.first(blame.authors.values()), + commit: Iterables.first(blame.commits.values()), + line: blame.lines[line] + } as GitBlameLine; + } + catch { + return undefined; + } + } + + async getBlameForLineContents( + uri: GitUri, + line: number, + contents: string, + options: { skipCache?: boolean } = {} + ): Promise { + Logger.log(`getBlameForLineContents('${uri.repoPath}', '${uri.fsPath}', ${line})`); + + if (!options.skipCache && this.UseCaching) { + const blame = await this.getBlameForFileContents(uri, contents); + if (blame === undefined) return undefined; + + let blameLine = blame.lines[line]; + if (blameLine === undefined) { + if (blame.lines.length !== line) return undefined; + blameLine = blame.lines[line - 1]; + } + + const commit = blame.commits.get(blameLine.sha); + if (commit === undefined) return undefined; + + return { + author: { ...blame.authors.get(commit.author), lineCount: commit.lines.length }, + commit: commit, + line: blameLine + } as GitBlameLine; + } + + const lineToBlame = line + 1; + const fileName = uri.fsPath; + + try { + const data = await Git.blame_contents(uri.repoPath, fileName, contents, { + args: Container.config.advanced.blame.customArguments, + ignoreWhitespace: Container.config.blame.ignoreWhitespace, + startLine: lineToBlame, + endLine: lineToBlame + }); + const currentUser = await this.getCurrentUser(uri.repoPath!); + const blame = GitBlameParser.parse(data, uri.repoPath, fileName, currentUser); + if (blame === undefined) return undefined; + + return { + author: Iterables.first(blame.authors.values()), + commit: Iterables.first(blame.commits.values()), + line: blame.lines[line] + } as GitBlameLine; + } + catch { + return undefined; + } + } + + async getBlameForRange(uri: GitUri, range: Range): Promise { + Logger.log( + `getBlameForRange('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}', [${range.start.line}, ${ + range.end.line + }])` + ); + + const blame = await this.getBlameForFile(uri); + if (blame === undefined) return undefined; + + return this.getBlameForRangeSync(blame, uri, range); + } + + getBlameForRangeSync(blame: GitBlame, uri: GitUri, range: Range): GitBlameLines | undefined { + Logger.log( + `getBlameForRangeSync('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}', [${range.start.line}, ${ + range.end.line + }])` + ); + + if (blame.lines.length === 0) return { allLines: blame.lines, ...blame }; + + if (range.start.line === 0 && range.end.line === blame.lines.length - 1) { + return { allLines: blame.lines, ...blame }; + } + + const lines = blame.lines.slice(range.start.line, range.end.line + 1); + const shas = new Set(lines.map(l => l.sha)); + + const authors: Map = new Map(); + const commits: Map = new Map(); + for (const c of blame.commits.values()) { + if (!shas.has(c.sha)) continue; + + const commit = c.with({ + lines: c.lines.filter(l => l.line >= range.start.line && l.line <= range.end.line) + }); + commits.set(c.sha, commit); + + let author = authors.get(commit.author); + if (author === undefined) { + author = { + name: commit.author, + lineCount: 0 + }; + authors.set(author.name, author); + } + + author.lineCount += commit.lines.length; + } + + const sortedAuthors = new Map([...authors.entries()].sort((a, b) => b[1].lineCount - a[1].lineCount)); + + return { + authors: sortedAuthors, + commits: commits, + lines: lines, + allLines: blame.lines + } as GitBlameLines; + } + + async getBranch(repoPath: string | undefined): Promise { + if (repoPath === undefined) return undefined; + + Logger.log(`getBranch('${repoPath}')`); + + const data = await Git.revparse_currentBranch(repoPath); + if (data === undefined) return undefined; + + const branch = data[0].split('\n'); + return new GitBranch(repoPath, branch[0], true, data[1], branch[1]); + } + + async getBranches(repoPath: string | undefined): Promise { + if (repoPath === undefined) return []; + + Logger.log(`getBranches('${repoPath}')`); + + const data = await Git.branch(repoPath, { all: true }); + // If we don't get any data, assume the repo doesn't have any commits yet so check if we have a current branch + if (data === '') { + const current = await this.getBranch(repoPath); + return current !== undefined ? [current] : []; + } + + return GitBranchParser.parse(data, repoPath) || []; + } + + async getChangedFilesCount(repoPath: string, sha?: string): Promise { + Logger.log(`getChangedFilesCount('${repoPath}', '${sha}')`); + + const data = await Git.diff_shortstat(repoPath, sha); + return GitDiffParser.parseShortStat(data); + } + + async getConfig(key: string, repoPath?: string): Promise { + Logger.log(`getConfig('${key}', '${repoPath}')`); + + return await Git.config_get(key, repoPath); + } + + private _userMapCache = new Map(); + + async getCurrentUser(repoPath: string) { + let user = this._userMapCache.get(repoPath); + if (user != null) return user; + // If we found the repo, but no user data was found just return + if (user === null) return undefined; + + const data = await Git.config_getRegex('user.(name|email)', repoPath); + if (!data) { + // If we found no user data, mark it so we won't bother trying again + this._userMapCache.set(repoPath, null); + return undefined; + } + + user = { name: undefined, email: undefined }; + + let match: RegExpExecArray | null = null; + do { + match = userConfigRegex.exec(data); + if (match == null) break; + + // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 + user[match[1] as 'name' | 'email'] = (' ' + match[2]).substr(1); + } while (match != null); + + const author = `${user.name} <${user.email}>`; + // Check if there is a mailmap for the current user + const mappedAuthor = await Git.check_mailmap(repoPath, author); + if (author !== mappedAuthor) { + match = mappedAuthorRegex.exec(mappedAuthor); + if (match != null) { + [, user.name, user.email] = match; + } + } + + this._userMapCache.set(repoPath, user); + return user; + } + + async getDiffForFile(uri: GitUri, sha1?: string, sha2?: string): Promise { + if (sha1 !== undefined && sha2 === undefined && uri.sha !== undefined) { + sha2 = uri.sha; + } + + let key = 'diff'; + if (sha1 !== undefined) { + key += `:${sha1}`; + } + if (sha2 !== undefined) { + key += `:${sha2}`; + } + + const doc = await Container.tracker.getOrAdd(uri); + if (this.UseCaching) { + if (doc.state !== undefined) { + const cachedDiff = doc.state.get(key); + if (cachedDiff !== undefined) { + Logger.log( + `getDiffForFile[Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${sha1}', '${sha2}')` + ); + return cachedDiff.item; + } + } + + Logger.log(`getDiffForFile[Not Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${sha1}', '${sha2}')`); + + if (doc.state === undefined) { + doc.state = new GitDocumentState(doc.key); + } + } + else { + Logger.log(`getDiffForFile('${uri.repoPath}', '${uri.fsPath}', '${sha1}', '${sha2}')`); + } + + const promise = this.getDiffForFileCore( + uri.repoPath, + uri.fsPath, + sha1, + sha2, + { encoding: GitService.getEncoding(uri) }, + doc, + key + ); + + if (doc.state !== undefined) { + Logger.log(`Add log cache for '${doc.state.key}:${key}'`); + + doc.state.set(key, { + item: promise + } as CachedDiff); + } + + return promise; + } + + private async getDiffForFileCore( + repoPath: string | undefined, + fileName: string, + sha1: string | undefined, + sha2: string | undefined, + options: { encoding?: string }, + document: TrackedDocument, + key: string + ): Promise { + const [file, root] = Git.splitPath(fileName, repoPath, false); + + try { + const data = await Git.diff(root, file, sha1, sha2, options); + const diff = GitDiffParser.parse(data); + return diff; + } + catch (ex) { + // Trap and cache expected diff errors + if (document.state !== undefined) { + const msg = ex && ex.toString(); + Logger.log(`Replace diff cache with empty promise for '${document.state.key}:${key}'`); + + document.state.set(key, { + item: GitService.emptyPromise, + errorMessage: msg + } as CachedDiff); + + return GitService.emptyPromise as Promise; + } + + return undefined; + } + } + + async getDiffForLine( + uri: GitUri, + line: number, + sha1?: string, + sha2?: string + ): Promise { + Logger.log(`getDiffForLine('${uri.repoPath}', '${uri.fsPath}', ${line}, '${sha1}', '${sha2}')`); + + try { + const diff = await this.getDiffForFile(uri, sha1, sha2); + if (diff === undefined) return undefined; + + const chunk = diff.chunks.find(c => c.currentPosition.start <= line && c.currentPosition.end >= line); + if (chunk === undefined) return undefined; + + return chunk.lines[line - chunk.currentPosition.start + 1]; + } + catch (ex) { + return undefined; + } + } + + async getDiffStatus( + repoPath: string, + sha1?: string, + sha2?: string, + options: { filter?: string } = {} + ): Promise { + Logger.log(`getDiffStatus('${repoPath}', '${sha1}', '${sha2}', ${options.filter})`); + + try { + const data = await Git.diff_nameStatus(repoPath, sha1, sha2, options); + const diff = GitDiffParser.parseNameStatus(data, repoPath); + return diff; + } + catch (ex) { + return undefined; + } + } + + async getRecentLogCommitForFile(repoPath: string | undefined, fileName: string): Promise { + return this.getLogCommitForFile(repoPath, fileName, undefined); + } + + async getRecentShaForFile(repoPath: string, fileName: string) { + return await Git.log_recent(repoPath, fileName); + } + + async getLogCommit(repoPath: string, ref: string): Promise { + Logger.log(`getLogCommit('${repoPath}', '${ref}'`); + + const log = await this.getLog(repoPath, { maxCount: 2, ref: ref }); + if (log === undefined) return undefined; + + return log.commits.get(ref); + } + + async getLogCommitForFile( + repoPath: string | undefined, + fileName: string, + options: { ref?: string; firstIfNotFound?: boolean } = {} + ): Promise { + Logger.log(`getFileLogCommit('${repoPath}', '${fileName}', '${options.ref}', ${options.firstIfNotFound})`); + + const log = await this.getLogForFile(repoPath, fileName, { maxCount: 2, ref: options.ref }); + if (log === undefined) return undefined; + + const commit = options.ref && log.commits.get(options.ref); + if (commit === undefined && !options.firstIfNotFound && options.ref) { + // If the sha isn't resolved we will never find it, so let it fall through so we return the first + if (!Git.isResolveRequired(options.ref)) return undefined; + } + + return commit || Iterables.first(log.commits.values()); + } + + async getLog( + repoPath: string, + options: { author?: string; maxCount?: number; ref?: string; reverse?: boolean } = {} + ): Promise { + options = { reverse: false, ...options }; + + Logger.log(`getLog('${repoPath}', '${options.ref}', ${options.maxCount}, ${options.reverse})`); + + const maxCount = options.maxCount == null ? Container.config.advanced.maxListItems || 0 : options.maxCount; + + try { + const data = await Git.log(repoPath, { + author: options.author, + maxCount: maxCount, + ref: options.ref, + reverse: options.reverse + }); + const log = GitLogParser.parse( + data, + GitCommitType.Branch, + repoPath, + undefined, + options.ref, + await this.getCurrentUser(repoPath), + maxCount, + options.reverse!, + undefined + ); + + if (log !== undefined) { + const opts = { ...options }; + log.query = (maxCount: number | undefined) => this.getLog(repoPath, { ...opts, maxCount: maxCount }); + } + + return log; + } + catch (ex) { + return undefined; + } + } + + async getLogForSearch( + repoPath: string, + search: string, + searchBy: GitRepoSearchBy, + options: { maxCount?: number } = {} + ): Promise { + Logger.log(`getLogForSearch('${repoPath}', '${search}', '${searchBy}', ${options.maxCount})`); + + let maxCount = options.maxCount == null ? Container.config.advanced.maxListItems || 0 : options.maxCount; + + let searchArgs: string[] | undefined = undefined; + switch (searchBy) { + case GitRepoSearchBy.Author: + searchArgs = ['-m', '-M', '--all', '--full-history', '-i', `--author=${search}`]; + break; + case GitRepoSearchBy.ChangedLines: + searchArgs = ['-M', '--all', '--full-history', '-i', `-G${search}`]; + break; + case GitRepoSearchBy.Changes: + searchArgs = ['-M', '--all', '--full-history', '-i', '--pickaxe-regex', `-S${search}`]; + break; + case GitRepoSearchBy.Files: + searchArgs = ['-M', '--all', '--full-history', '-i', `--`, `${search}`]; + break; + case GitRepoSearchBy.Message: + searchArgs = ['-m', '-M', '--all', '--full-history']; + if (search) { + searchArgs.push(`--grep=${search}`); + } + break; + case GitRepoSearchBy.Sha: + searchArgs = [`-m`, '-M', search]; + maxCount = 1; + break; + } + + try { + const data = await Git.log_search(repoPath, searchArgs, { maxCount: maxCount }); + const log = GitLogParser.parse( + data, + GitCommitType.Branch, + repoPath, + undefined, + undefined, + await this.getCurrentUser(repoPath), + maxCount, + false, + undefined + ); + + if (log !== undefined) { + const opts = { ...options }; + log.query = (maxCount: number | undefined) => + this.getLogForSearch(repoPath, search, searchBy, { ...opts, maxCount: maxCount }); + } + + return log; + } + catch (ex) { + return undefined; + } + } + + async getLogForFile( + repoPath: string | undefined, + fileName: string, + options: { maxCount?: number; range?: Range; ref?: string; renames?: boolean; reverse?: boolean } = {} + ): Promise { + if (repoPath !== undefined && repoPath === Strings.normalizePath(fileName)) { + throw new Error(`File name cannot match the repository path; fileName=${fileName}`); + } + + options = { reverse: false, ...options }; + + if (options.renames === undefined) { + options.renames = Container.config.advanced.fileHistoryFollowsRenames; + } + + let key = 'log'; + if (options.ref !== undefined) { + key += `:${options.ref}`; + } + if (options.maxCount !== undefined) { + key += `:n${options.maxCount}`; + } + if (options.renames) { + key += `:follow`; + } + if (options.reverse) { + key += `:reverse`; + } + + const doc = await Container.tracker.getOrAdd( + new GitUri(Uri.file(fileName), { repoPath: repoPath!, sha: options.ref }) + ); + if (this.UseCaching && options.range === undefined) { + if (doc.state !== undefined) { + const cachedLog = doc.state.get(key); + if (cachedLog !== undefined) { + Logger.log( + `getLogForFile[Cached(${key})]('${repoPath}', '${fileName}', '${options.ref}', ${ + options.maxCount + }, undefined, ${options.renames}, ${options.reverse})` + ); + return cachedLog.item; + } + + if (options.ref !== undefined || options.maxCount !== undefined) { + // Since we are looking for partial log, see if we have the log of the whole file + const cachedLog = doc.state.get( + `log${options.renames ? ':follow' : ''}${options.reverse ? ':reverse' : ''}` + ); + if (cachedLog !== undefined) { + if (options.ref === undefined) { + Logger.log( + `getLogForFile[Cached(~${key})]('${repoPath}', '${fileName}', '', ${ + options.maxCount + }, undefined, ${options.renames}, ${options.reverse})` + ); + return cachedLog.item; + } + + Logger.log( + `getLogForFile[? Cache(${key})]('${repoPath}', '${fileName}', '${options.ref}', ${ + options.maxCount + }, undefined, ${options.renames}, ${options.reverse})` + ); + const log = await cachedLog.item; + if (log !== undefined && log.commits.has(options.ref)) { + Logger.log( + `getLogForFile[Cached(${key})]('${repoPath}', '${fileName}', '${options.ref}', ${ + options.maxCount + }, undefined, ${options.renames}, ${options.reverse})` + ); + return cachedLog.item; + } + } + } + } + + Logger.log( + `getLogForFile[Not Cached(${key})]('${repoPath}', '${fileName}', ${options.ref}, ${ + options.maxCount + }, undefined, ${options.reverse})` + ); + + if (doc.state === undefined) { + doc.state = new GitDocumentState(doc.key); + } + } + else { + Logger.log( + `getLogForFile('${repoPath}', '${fileName}', ${options.ref}, ${options.maxCount}, ${options.range && + `[${options.range.start.line}, ${options.range.end.line}]`}, ${options.reverse})` + ); + } + + const promise = this.getLogForFileCore(repoPath, fileName, options, doc, key); + + if (doc.state !== undefined && options.range === undefined) { + Logger.log(`Add log cache for '${doc.state.key}:${key}'`); + + doc.state.set(key, { + item: promise + } as CachedLog); + } + + return promise; + } + + private async getLogForFileCore( + repoPath: string | undefined, + fileName: string, + options: { maxCount?: number; range?: Range; ref?: string; renames?: boolean; reverse?: boolean }, + document: TrackedDocument, + key: string + ): Promise { + if (!(await this.isTracked(fileName, repoPath, { ref: options.ref }))) { + Logger.log(`Skipping log; '${fileName}' is not tracked`); + return GitService.emptyPromise as Promise; + } + + const [file, root] = Git.splitPath(fileName, repoPath, false); + + try { + const { range, ...opts } = options; + + const maxCount = options.maxCount == null ? Container.config.advanced.maxListItems || 0 : options.maxCount; + + const data = await Git.log_file(root, file, { + ...opts, + maxCount: maxCount, + startLine: range && range.start.line + 1, + endLine: range && range.end.line + 1 + }); + const log = GitLogParser.parse( + data, + GitCommitType.File, + root, + file, + opts.ref, + await this.getCurrentUser(root), + maxCount, + opts.reverse!, + range + ); + + if (log !== undefined) { + const opts = { ...options }; + log.query = (maxCount: number | undefined) => + this.getLogForFile(repoPath, fileName, { ...opts, maxCount: maxCount }); + } + + return log; + } + catch (ex) { + // Trap and cache expected log errors + if (document.state !== undefined && options.range === undefined && !options.reverse) { + const msg = ex && ex.toString(); + Logger.log(`Replace log cache with empty promise for '${document.state.key}:${key}'`); + + document.state.set(key, { + item: GitService.emptyPromise, + errorMessage: msg + } as CachedLog); + + return GitService.emptyPromise as Promise; + } + + return undefined; + } + } + + async hasRemotes(repoPath: string | undefined): Promise { + if (repoPath === undefined) return false; + + const repository = await this.getRepository(repoPath); + if (repository === undefined) return false; + + return repository.hasRemotes(); + } + + async hasTrackingBranch(repoPath: string | undefined): Promise { + if (repoPath === undefined) return false; + + const repository = await this.getRepository(repoPath); + if (repository === undefined) return false; + + return repository.hasTrackingBranch(); + } + + async getMergeBase(repoPath: string, ref1: string, ref2: string, options: { forkPoint?: boolean } = {}) { + try { + const data = await Git.merge_base(repoPath, ref1, ref2, options); + if (data === undefined) return undefined; + + return data.split('\n')[0]; + } + catch (ex) { + Logger.error(ex, 'GitService.getMergeBase'); + return undefined; + } + } + + async getRemotes(repoPath: string | undefined, options: { includeAll?: boolean } = {}): Promise { + if (repoPath === undefined) return []; + + Logger.log(`getRemotes('${repoPath}')`); + + const repository = await this.getRepository(repoPath); + const remotes = repository !== undefined ? repository.getRemotes() : this.getRemotesCore(repoPath); + + if (options.includeAll) return remotes; + + return (await remotes).filter(r => r.provider !== undefined); + } + + async getRemotesCore(repoPath: string | undefined, providerMap?: RemoteProviderMap): Promise { + if (repoPath === undefined) return []; + + Logger.log(`getRemotesCore('${repoPath}')`); + + providerMap = + providerMap || + RemoteProviderFactory.createMap( + configuration.get(configuration.name('remotes').value, null) + ); + + try { + const data = await Git.remote(repoPath); + return GitRemoteParser.parse(data, repoPath, RemoteProviderFactory.factory(providerMap)); + } + catch (ex) { + Logger.error(ex, 'GitService.getRemotesCore'); + return []; + } + } + + async getRepoPath(filePath: string, options?: { ref?: string }): Promise; + async getRepoPath(uri: Uri | undefined, options?: { ref?: string }): Promise; + async getRepoPath( + filePathOrUri: string | Uri | undefined, + options: { ref?: string } = {} + ): Promise { + if (filePathOrUri == null) return await this.getActiveRepoPath(); + if (filePathOrUri instanceof GitUri) return filePathOrUri.repoPath; + + // Don't save the tracking info to the cache, because we could be looking in the wrong place (e.g. looking in the root when the file is in a submodule) + let repo = await this.getRepository(filePathOrUri, { ...options, skipCacheUpdate: true }); + if (repo !== undefined) return repo.path; + + if (typeof filePathOrUri !== 'string') { + const versionedUri = await Container.git.getVersionedUri(filePathOrUri); + if (versionedUri !== undefined) return versionedUri.repoPath; + } + + const rp = await this.getRepoPathCore( + typeof filePathOrUri === 'string' ? filePathOrUri : filePathOrUri.fsPath, + false + ); + if (rp === undefined) return undefined; + + // Recheck this._repositoryTree.get(rp) to make sure we haven't already tried adding this due to awaits + if (this._repositoryTree.get(rp) !== undefined) return rp; + + // If this new repo is inside one of our known roots and we we don't already know about, add it + const root = this._repositoryTree.findSubstr(rp); + let folder = root === undefined ? workspace.getWorkspaceFolder(Uri.file(rp)) : root.folder; + + if (folder === undefined) { + const parts = rp.split('/'); + folder = { uri: Uri.file(rp), name: parts[parts.length - 1], index: this._repositoryTree.count() }; + } + + Logger.log(`Repository found in '${rp}'`); + repo = new Repository(folder, rp, false, this.onAnyRepositoryChanged.bind(this), this._suspended); + this._repositoryTree.set(rp, repo); + + // Send a notification that the repositories changed + setImmediate(async () => { + await this.updateContext(this._repositoryTree); + + this.fireRepositoriesChanged(); + }); + + return rp; + } + + private async getRepoPathCore(filePath: string, isDirectory: boolean): Promise { + try { + return await Git.revparse_toplevel(isDirectory ? filePath : path.dirname(filePath)); + } + catch (ex) { + Logger.error(ex, 'GitService.getRepoPathCore'); + return undefined; + } + } + + async getRepoPathOrActive(uri: Uri | undefined, editor: TextEditor | undefined) { + const repoPath = await Container.git.getRepoPath(uri); + if (repoPath) return repoPath; + + return Container.git.getActiveRepoPath(editor); + } + + async getRepositories(predicate?: (repo: Repository) => boolean): Promise> { + const repositoryTree = await this.getRepositoryTree(); + + const values = repositoryTree.values(); + return predicate !== undefined ? Iterables.filter(values, predicate) : values; + } + + private async getRepositoryTree(): Promise> { + if (this._repositoriesLoadingPromise !== undefined) { + await this._repositoriesLoadingPromise; + this._repositoriesLoadingPromise = undefined; + } + + return this._repositoryTree; + } + + async getRepository( + repoPath: string, + options?: { ref?: string; skipCacheUpdate?: boolean } + ): Promise; + async getRepository( + uri: Uri, + options?: { ref?: string; skipCacheUpdate?: boolean } + ): Promise; + async getRepository( + repoPathOrUri: string | Uri, + options?: { ref?: string; skipCacheUpdate?: boolean } + ): Promise; + async getRepository( + repoPathOrUri: string | Uri, + options: { ref?: string; skipCacheUpdate?: boolean } = {} + ): Promise { + const repositoryTree = await this.getRepositoryTree(); + + let path: string; + if (typeof repoPathOrUri === 'string') { + const repo = repositoryTree.get(repoPathOrUri); + if (repo !== undefined) return repo; + + path = repoPathOrUri; + } + else { + if (repoPathOrUri instanceof GitUri) { + if (repoPathOrUri.repoPath) { + const repo = repositoryTree.get(repoPathOrUri.repoPath); + if (repo !== undefined) return repo; + } + + path = repoPathOrUri.fsPath; + } + else { + path = repoPathOrUri.fsPath; + } + } + + const repo = repositoryTree.findSubstr(path); + if (repo === undefined) return undefined; + + // Make sure the file is tracked in this repo before returning -- it could be from a submodule + if (!(await this.isTracked(path, repo.path, options))) return undefined; + return repo; + } + + async getRepositoryCount(): Promise { + const repositoryTree = await this.getRepositoryTree(); + return repositoryTree.count(); + } + + async getStashList(repoPath: string | undefined): Promise { + if (repoPath === undefined) return undefined; + + Logger.log(`getStashList('${repoPath}')`); + + const data = await Git.stash_list(repoPath); + const stash = GitStashParser.parse(data, repoPath); + return stash; + } + + async getStatusForFile(repoPath: string, fileName: string): Promise { + Logger.log(`getStatusForFile('${repoPath}', '${fileName}')`); + + const porcelainVersion = Git.validateVersion(2, 11) ? 2 : 1; + + const data = await Git.status_file(repoPath, fileName, porcelainVersion); + const status = GitStatusParser.parse(data, repoPath, porcelainVersion); + if (status === undefined || !status.files.length) return undefined; + + return status.files[0]; + } + + async getStatusForRepo(repoPath: string | undefined): Promise { + if (repoPath === undefined) return undefined; + + Logger.log(`getStatusForRepo('${repoPath}')`); + + const porcelainVersion = Git.validateVersion(2, 11) ? 2 : 1; + + const data = await Git.status(repoPath, porcelainVersion); + const status = GitStatusParser.parse(data, repoPath, porcelainVersion); + return status; + } + + async getTags(repoPath: string | undefined): Promise { + if (repoPath === undefined) return []; + + Logger.log(`getTags('${repoPath}')`); + + const data = await Git.tag(repoPath); + return GitTagParser.parse(data, repoPath) || []; + } + + async getTreeFileForRevision(repoPath: string, fileName: string, ref: string): Promise { + if (repoPath === undefined) return undefined; + + Logger.log(`getTreeFileForRevision('${repoPath}', '${fileName}', '${ref}')`); + + const data = await Git.ls_tree(repoPath, ref, { fileName: fileName }); + const trees = GitTreeParser.parse(data); + return trees === undefined || trees.length === 0 ? undefined : trees[0]; + } + + async getTreeForRevision(repoPath: string, ref: string): Promise { + if (repoPath === undefined) return []; + + Logger.log(`getTreeForRevision('${repoPath}', '${ref}')`); + + const data = await Git.ls_tree(repoPath, ref); + return GitTreeParser.parse(data) || []; + } + + async getVersionedFile( + repoPath: string | undefined, + fileName: string, + sha: string | undefined + ): Promise { + Logger.log(`getVersionedFile('${repoPath}', '${fileName}', '${sha}')`); + + if (sha === GitService.deletedSha) return undefined; + + if (!sha || (Git.isUncommitted(sha) && !Git.isStagedUncommitted(sha))) { + if (await this.fileExists(repoPath!, fileName)) return Uri.file(fileName); + + return undefined; + } + + return GitUri.toRevisionUri(sha, fileName, repoPath!); + } + + getVersionedFileBuffer(repoPath: string, fileName: string, sha: string) { + Logger.log(`getVersionedFileBuffer('${repoPath}', '${fileName}', ${sha})`); + + return Git.show(repoPath, fileName, sha, { encoding: 'buffer' }); + } + + // getVersionedFileText(repoPath: string, fileName: string, sha: string) { + // Logger.log(`getVersionedFileText('${repoPath}', '${fileName}', ${sha})`); + + // return Git.show(repoPath, fileName, sha, { encoding: GitService.getEncoding(repoPath, fileName) }); + // } + + getVersionedUri(uri: Uri) { + return this._versionedUriCache.get(GitUri.toKey(uri)); + } + + isTrackable(scheme: string): boolean; + isTrackable(uri: Uri): boolean; + isTrackable(schemeOruri: string | Uri): boolean { + let scheme: string; + if (typeof schemeOruri === 'string') { + scheme = schemeOruri; + } + else { + scheme = schemeOruri.scheme; + } + + return scheme === DocumentSchemes.File || scheme === DocumentSchemes.Git || scheme === DocumentSchemes.GitLens; + } + + async isTracked( + fileName: string, + repoPath?: string, + options?: { ref?: string; skipCacheUpdate?: boolean } + ): Promise; + async isTracked(uri: GitUri): Promise; + async isTracked( + fileNameOrUri: string | GitUri, + repoPath?: string, + options: { ref?: string; skipCacheUpdate?: boolean } = {} + ): Promise { + if (options.ref === GitService.deletedSha) return false; + + let ref = options.ref; + let cacheKey: string; + let fileName: string; + if (typeof fileNameOrUri === 'string') { + [fileName, repoPath] = Git.splitPath(fileNameOrUri, repoPath); + cacheKey = GitUri.toKey(fileNameOrUri); + } + else { + if (!this.isTrackable(fileNameOrUri)) return false; + + fileName = fileNameOrUri.fsPath; + repoPath = fileNameOrUri.repoPath; + ref = fileNameOrUri.sha; + cacheKey = GitUri.toKey(fileName); + } + + if (ref !== undefined) { + cacheKey += `:${ref}`; + } + + let tracked = this._trackedCache.get(cacheKey); + try { + if (tracked !== undefined) { + tracked = await tracked; + + return tracked; + } + + tracked = this.isTrackedCore(fileName, repoPath === undefined ? '' : repoPath, ref); + if (options.skipCacheUpdate) { + tracked = await tracked; + + return tracked; + } + + this._trackedCache.set(cacheKey, tracked); + tracked = await tracked; + this._trackedCache.set(cacheKey, tracked); + + return tracked; + } + finally { + Logger.log(`isTracked('${fileName}', '${repoPath}'${ref !== undefined ? `, '${ref}'` : ''}) = ${tracked}`); + } + } + + private async isTrackedCore(fileName: string, repoPath: string, ref?: string) { + if (ref === GitService.deletedSha) return false; + + try { + // Even if we have a sha, check first to see if the file exists (that way the cache will be better reused) + let tracked = !!(await Git.ls_files(repoPath === undefined ? '' : repoPath, fileName)); + if (!tracked && ref !== undefined) { + tracked = !!(await Git.ls_files(repoPath === undefined ? '' : repoPath, fileName, { ref: ref })); + // If we still haven't found this file, make sure it wasn't deleted in that sha (i.e. check the previous) + if (!tracked) { + tracked = !!(await Git.ls_files(repoPath === undefined ? '' : repoPath, fileName, { + ref: `${ref}^` + })); + } + } + return tracked; + } + catch (ex) { + Logger.error(ex, 'GitService.isTrackedCore'); + return false; + } + } + + async getDiffTool(repoPath?: string) { + return (await Git.config_get('diff.guitool', repoPath)) || (await Git.config_get('diff.tool', repoPath)); + } + + async openDiffTool(repoPath: string, uri: Uri, staged: boolean, tool?: string) { + if (!tool) { + tool = await this.getDiffTool(repoPath); + if (tool === undefined) throw new Error('No diff tool found'); + } + + Logger.log(`openDiffTool('${repoPath}', '${uri.fsPath}', ${staged}, '${tool}')`); + + return Git.difftool_fileDiff(repoPath, uri.fsPath, tool, staged); + } + + async openDirectoryDiff(repoPath: string, ref1: string, ref2?: string, tool?: string) { + if (!tool) { + tool = await this.getDiffTool(repoPath); + if (tool === undefined) throw new Error('No diff tool found'); + } + + Logger.log(`openDirectoryDiff('${repoPath}', '${ref1}', '${ref2}', '${tool}')`); + + return Git.difftool_dirDiff(repoPath, tool, ref1, ref2); + } + + async resolveReference(repoPath: string, ref: string, uri?: Uri) { + if (!GitService.isResolveRequired(ref) || ref.endsWith('^3')) return ref; + + Logger.log(`resolveReference('${repoPath}', '${ref}', '${uri && uri.toString(true)}')`); + + if (uri == null) return (await Git.revparse(repoPath, ref)) || ref; + + return ( + (await Git.log_resolve(repoPath, Strings.normalizePath(path.relative(repoPath, uri.fsPath)), ref)) || ref + ); + } + + stopWatchingFileSystem() { + this._repositoryTree.forEach(r => r.stopWatchingFileSystem()); + } + + stashApply(repoPath: string, stashName: string, deleteAfter: boolean = false) { + Logger.log(`stashApply('${repoPath}', '${stashName}', ${deleteAfter})`); + + return Git.stash_apply(repoPath, stashName, deleteAfter); + } + + stashDelete(repoPath: string, stashName: string) { + Logger.log(`stashDelete('${repoPath}', '${stashName}')`); + + return Git.stash_delete(repoPath, stashName); + } + + stashSave(repoPath: string, message?: string, uris?: Uri[]) { + Logger.log(`stashSave('${repoPath}', '${message}', ${uris})`); + + if (uris === undefined) return Git.stash_save(repoPath, message); + + GitService.ensureGitVersion('2.13.2', 'Stashing individual files'); + + const pathspecs = uris.map(u => Git.splitPath(u.fsPath, repoPath)[0]); + return Git.stash_push(repoPath, pathspecs, message); + } + + static getEncoding(repoPath: string, fileName: string): string; + static getEncoding(uri: Uri): string; + static getEncoding(repoPathOrUri: string | Uri, fileName?: string): string { + const uri = typeof repoPathOrUri === 'string' ? Uri.file(path.join(repoPathOrUri, fileName!)) : repoPathOrUri; + return Git.getEncoding(workspace.getConfiguration('files', uri).get('encoding')); + } + + static async initialize(): Promise { + // Try to use the same git as the built-in vscode git extension + let gitPath; + try { + const gitExtension = extensions.getExtension('vscode.git'); + if (gitExtension !== undefined) { + const gitApi = ((await gitExtension.activate()) as GitExtension).getAPI(1); + gitPath = gitApi.git.path; + } + } + catch {} + + await Git.setOrFindGitPath(gitPath || workspace.getConfiguration('git').get('path')); + } + + static getGitPath(): string { + return Git.getGitPath(); + } + + static getGitVersion(): string { + return Git.getGitVersion(); + } + + static isResolveRequired(sha: string): boolean { + return Git.isResolveRequired(sha); + } + + static isSha(sha: string): boolean { + return Git.isSha(sha); + } + + static isStagedUncommitted(sha: string | undefined): boolean { + return Git.isStagedUncommitted(sha); + } + + static isUncommitted(sha: string | undefined): boolean { + return Git.isUncommitted(sha); + } + + static shortenSha( + sha: string | undefined, + strings: { deleted?: string; stagedUncommitted?: string; uncommitted?: string; working?: string } = {} + ) { + if (sha === undefined) return undefined; + + strings = { deleted: '(deleted)', working: '', ...strings }; + + if (sha === '') return strings.working; + if (sha === GitService.deletedSha) return strings.deleted; + + return Git.isSha(sha) || Git.isStagedUncommitted(sha) ? Git.shortenSha(sha, strings) : sha; + } + + static compareGitVersion(version: string, throwIfLessThan?: Error) { + return Versions.compare(Versions.fromString(this.getGitVersion()), Versions.fromString(version)); + } + + static ensureGitVersion(version: string, feature: string): void { + const gitVersion = this.getGitVersion(); + if (Versions.compare(Versions.fromString(gitVersion), Versions.fromString(version)) === -1) { + throw new Error( + `${feature} requires a newer version of Git (>= ${version}) than is currently installed (${gitVersion}). Please install a more recent version of Git to use this GitLens feature.` + ); + } + } +} diff --git a/src/git/gitUri.ts b/src/git/gitUri.ts index 5fcb105..b89ee35 100644 --- a/src/git/gitUri.ts +++ b/src/git/gitUri.ts @@ -4,7 +4,7 @@ import { Uri } from 'vscode'; import { UriComparer } from '../comparers'; import { DocumentSchemes, GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitCommit, GitService, IGitStatusFile } from '../gitService'; +import { GitCommit, GitService, IGitStatusFile } from '../git/gitService'; import { Strings } from '../system'; export interface IGitCommitInfo { diff --git a/src/git/locator.ts b/src/git/locator.ts new file mode 100644 index 0000000..715aa44 --- /dev/null +++ b/src/git/locator.ts @@ -0,0 +1,83 @@ +'use strict'; +import * as path from 'path'; +import { findExecutable, run } from './shell'; + +export interface IGitInfo { + path: string; + version: string; +} + +function parseVersion(raw: string): string { + return raw.replace(/^git version /, ''); +} + +async function findSpecificGit(path: string): Promise { + const version = await run(path, ['--version'], 'utf8'); + // If needed, let's update our path to avoid the search on every command + if (!path || path === 'git') { + path = findExecutable(path, ['--version']).cmd; + } + + return { + path, + version: parseVersion(version.trim()) + }; +} + +async function findGitDarwin(): Promise { + try { + let path = await run('which', ['git'], 'utf8'); + path = path.replace(/^\s+|\s+$/g, ''); + + if (path !== '/usr/bin/git') { + return findSpecificGit(path); + } + + try { + await run('xcode-select', ['-p'], 'utf8'); + return findSpecificGit(path); + } + catch (ex) { + if (ex.code === 2) { + return Promise.reject(new Error('Unable to find git')); + } + return findSpecificGit(path); + } + } + catch (ex) { + return Promise.reject(new Error('Unable to find git')); + } +} + +function findSystemGitWin32(basePath: string): Promise { + if (!basePath) return Promise.reject(new Error('Unable to find git')); + return findSpecificGit(path.join(basePath, 'Git', 'cmd', 'git.exe')); +} + +function findGitWin32(): Promise { + return findSystemGitWin32(process.env['ProgramW6432']!) + .then(null, () => findSystemGitWin32(process.env['ProgramFiles(x86)']!)) + .then(null, () => findSystemGitWin32(process.env['ProgramFiles']!)) + .then(null, () => findSpecificGit('git')); +} + +export async function findGitPath(path?: string): Promise { + try { + return await findSpecificGit(path || 'git'); + } + catch (ex) { + try { + switch (process.platform) { + case 'darwin': + return await findGitDarwin(); + case 'win32': + return await findGitWin32(); + default: + return Promise.reject('Unable to find git'); + } + } + catch (ex) { + return Promise.reject(new Error('Unable to find git')); + } + } +} diff --git a/src/gitService.ts b/src/gitService.ts deleted file mode 100644 index 4d51d53..0000000 --- a/src/gitService.ts +++ /dev/null @@ -1,1946 +0,0 @@ -'use strict'; -import * as fs from 'fs'; -import * as path from 'path'; -import { - ConfigurationChangeEvent, - Disposable, - Event, - EventEmitter, - extensions, - Range, - TextEditor, - Uri, - window, - WindowState, - workspace, - WorkspaceFolder, - WorkspaceFoldersChangeEvent -} from 'vscode'; -import { GitExtension } from './@types/git'; -import { configuration, IRemotesConfig } from './configuration'; -import { CommandContext, DocumentSchemes, GlyphChars, setCommandContext } from './constants'; -import { Container } from './container'; -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, - GitTree, - GitTreeParser, - Repository, - RepositoryChange -} from './git/git'; -import { GitUri, IGitCommitInfo } from './git/gitUri'; -import { RemoteProviderFactory, RemoteProviderMap } from './git/remotes/factory'; -import { Logger } from './logger'; -import { Iterables, Objects, Strings, TernarySearchTree, Versions } from './system'; -import { CachedBlame, CachedDiff, CachedLog, GitDocumentState, TrackedDocument } from './trackers/gitDocumentTracker'; - -export { GitUri, IGitCommitInfo }; -export * from './git/models/models'; -export * from './git/formatters/formatters'; -export { getNameFromRemoteResource, RemoteProvider, RemoteResource, RemoteResourceType } from './git/remotes/provider'; -export { RemoteProviderFactory } from './git/remotes/factory'; - -const RepoSearchWarnings = { - doesNotExist: /no such file or directory/i -}; - -const userConfigRegex = /^user\.(name|email) (.*)$/gm; -const mappedAuthorRegex = /(.+)\s<(.+)>/; - -export enum GitRepoSearchBy { - Author = 'author', - ChangedLines = 'changed-lines', - Changes = 'changes', - Files = 'files', - Message = 'message', - Sha = 'sha' -} - -export class GitService implements Disposable { - static emptyPromise: Promise = Promise.resolve(undefined); - static deletedSha = 'ffffffffffffffffffffffffffffffffffffffff'; - static stagedUncommittedSha = Git.stagedUncommittedSha; - static uncommittedSha = Git.uncommittedSha; - - private _onDidChangeRepositories = new EventEmitter(); - get onDidChangeRepositories(): Event { - return this._onDidChangeRepositories.event; - } - - private readonly _disposable: Disposable; - private readonly _repositoryTree: TernarySearchTree; - private _repositoriesLoadingPromise: Promise | undefined; - private _suspended: boolean = false; - private readonly _trackedCache: Map>; - private _versionedUriCache: Map; - - constructor() { - this._repositoryTree = TernarySearchTree.forPaths(); - this._trackedCache = new Map(); - this._versionedUriCache = new Map(); - - this._disposable = Disposable.from( - window.onDidChangeWindowState(this.onWindowStateChanged, this), - workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this), - configuration.onDidChange(this.onConfigurationChanged, this) - ); - this.onConfigurationChanged(configuration.initializingChangeEvent); - - this._repositoriesLoadingPromise = this.onWorkspaceFoldersChanged(); - } - - dispose() { - this._repositoryTree.forEach(r => r.dispose()); - this._trackedCache.clear(); - this._versionedUriCache.clear(); - - this._disposable && this._disposable.dispose(); - } - - get UseCaching() { - return Container.config.advanced.caching.enabled; - } - - private onAnyRepositoryChanged(repo: Repository, reason: RepositoryChange) { - this._trackedCache.clear(); - - if (reason === RepositoryChange.Config) { - this._userMapCache.delete(repo.path); - } - - if (reason === RepositoryChange.Closed) { - // Send a notification that the repositories changed - setImmediate(async () => { - await this.updateContext(this._repositoryTree); - - this.fireRepositoriesChanged(); - }); - } - } - - private onConfigurationChanged(e: ConfigurationChangeEvent) { - const initializing = configuration.initializing(e); - - if ( - initializing || - configuration.changed(e, configuration.name('defaultDateStyle').value) || - configuration.changed(e, configuration.name('defaultDateFormat').value) - ) { - CommitFormatting.reset(); - } - } - - private onWindowStateChanged(e: WindowState) { - if (e.focused) { - this._repositoryTree.forEach(r => r.resume()); - } - else { - this._repositoryTree.forEach(r => r.suspend()); - } - - this._suspended = !e.focused; - } - - private async onWorkspaceFoldersChanged(e?: WorkspaceFoldersChangeEvent) { - let initializing = false; - if (e === undefined) { - initializing = true; - e = { - added: workspace.workspaceFolders || [], - removed: [] - } as WorkspaceFoldersChangeEvent; - - Logger.log(`Starting repository search in ${e.added.length} folders`); - } - - for (const f of e.added) { - if (f.uri.scheme !== DocumentSchemes.File) continue; - - // Search for and add all repositories (nested and/or submodules) - const repositories = await this.repositorySearch(f); - for (const r of repositories) { - this._repositoryTree.set(r.path, r); - } - } - - for (const f of e.removed) { - if (f.uri.scheme !== DocumentSchemes.File) continue; - - const fsPath = f.uri.fsPath; - const repos = this._repositoryTree.findSuperstr(fsPath); - const reposToDelete = - repos !== undefined - ? // Since the filtered tree will have keys that are relative to the fsPath, normalize to the full path - [...Iterables.map(repos, r => [r, r.path])] - : []; - - // const filteredTree = this._repositoryTree.findSuperstr(fsPath); - // const reposToDelete = - // filteredTree !== undefined - // ? // Since the filtered tree will have keys that are relative to the fsPath, normalize to the full path - // [ - // ...Iterables.map<[Repository, string], [Repository, string]>( - // filteredTree.entries(), - // ([r, k]) => [r, path.join(fsPath, k)] - // ) - // ] - // : []; - - const repo = this._repositoryTree.get(fsPath); - if (repo !== undefined) { - reposToDelete.push([repo, fsPath]); - } - - for (const [r, k] of reposToDelete) { - this._repositoryTree.delete(k); - r.dispose(); - } - } - - await this.updateContext(this._repositoryTree); - - if (!initializing) { - // Defer the event trigger enough to let everything unwind - setImmediate(() => this.fireRepositoriesChanged()); - } - } - - private async repositorySearch(folder: WorkspaceFolder): Promise { - const folderUri = folder.uri; - - const depth = configuration.get( - configuration.name('advanced')('repositorySearchDepth').value, - folderUri - ); - - Logger.log(`Searching for repositories (depth=${depth}) in '${folderUri.fsPath}' ...`); - - const start = process.hrtime(); - - const repositories: Repository[] = []; - const anyRepoChangedFn = this.onAnyRepositoryChanged.bind(this); - - const rootPath = await this.getRepoPathCore(folderUri.fsPath, true); - if (rootPath !== undefined) { - Logger.log(`Repository found in '${rootPath}'`); - repositories.push(new Repository(folder, rootPath, true, anyRepoChangedFn, this._suspended)); - } - - if (depth <= 0) { - Logger.log( - `Completed repository search (depth=${depth}) in '${folderUri.fsPath}' ${ - GlyphChars.Dot - } ${Strings.getDurationMilliseconds(start)} ms` - ); - - return repositories; - } - - // Get any specified excludes -- this is a total hack, but works for some simple cases and something is better than nothing :) - let excludes = { - ...workspace.getConfiguration('files', folderUri).get<{ [key: string]: boolean }>('exclude', {}), - ...workspace.getConfiguration('search', folderUri).get<{ [key: string]: boolean }>('exclude', {}) - }; - - const excludedPaths = [ - ...Iterables.filterMap(Objects.entries(excludes), ([key, value]) => { - if (!value) return undefined; - if (key.startsWith('**/')) return key.substring(3); - return key; - }) - ]; - - excludes = excludedPaths.reduce( - (accumulator, current) => { - accumulator[current] = true; - return accumulator; - }, - Object.create(null) as any - ); - - let paths; - try { - paths = await this.repositorySearchCore(folderUri.fsPath, depth, excludes); - } - catch (ex) { - if (RepoSearchWarnings.doesNotExist.test(ex.message || '')) { - Logger.log( - `Repository search (depth=${depth}) in '${folderUri.fsPath}' FAILED${ - ex.message ? `(${ex.message})` : '' - }` - ); - } - else { - Logger.error(ex, `Repository search (depth=${depth}) in '${folderUri.fsPath}' FAILED`); - } - - return repositories; - } - - for (let p of paths) { - p = path.dirname(p); - // If we are the same as the root, skip it - if (Strings.normalizePath(p) === rootPath) continue; - - const rp = await this.getRepoPathCore(p, true); - if (rp === undefined) continue; - - Logger.log(`Repository found in '${rp}'`); - repositories.push(new Repository(folder, rp, false, anyRepoChangedFn, this._suspended)); - } - - Logger.log( - `Completed repository search (depth=${depth}) in '${folderUri.fsPath}' ${ - GlyphChars.Dot - } ${Strings.getDurationMilliseconds(start)} ms` - ); - - return repositories; - } - - private async repositorySearchCore( - root: string, - depth: number, - excludes: { [key: string]: boolean }, - repositories: string[] = [] - ): Promise { - return new Promise((resolve, reject) => { - fs.readdir(root, async (err, files) => { - if (err != null) { - reject(err); - return; - } - - if (files.length === 0) { - resolve(repositories); - return; - } - - const folders: string[] = []; - - const promises = files.map(file => { - const fullPath = path.resolve(root, file); - - return new Promise((res, rej) => { - fs.stat(fullPath, (err, stat) => { - if (file === '.git') { - repositories.push(fullPath); - } - else if (err == null && excludes[file] !== true && stat != null && stat.isDirectory()) { - folders.push(fullPath); - } - - res(); - }); - }); - }); - - await Promise.all(promises); - - if (depth-- > 0) { - for (const folder of folders) { - await this.repositorySearchCore(folder, depth, excludes, repositories); - } - } - - resolve(repositories); - }); - }); - } - - private async updateContext(repositoryTree: TernarySearchTree) { - const hasRepository = repositoryTree.any(); - await setCommandContext(CommandContext.Enabled, hasRepository); - - let hasRemotes = false; - if (hasRepository) { - for (const repo of repositoryTree.values()) { - hasRemotes = await repo.hasRemotes(); - if (hasRemotes) break; - } - } - - await setCommandContext(CommandContext.HasRemotes, hasRemotes); - - // If we have no repositories setup a watcher in case one is initialized - if (!hasRepository) { - const watcher = workspace.createFileSystemWatcher('**/.git', false, true, true); - const disposable = Disposable.from( - watcher, - watcher.onDidCreate(async uri => { - const f = workspace.getWorkspaceFolder(uri); - if (f === undefined) return; - - // Search for and add all repositories (nested and/or submodules) - const repositories = await this.repositorySearch(f); - if (repositories.length === 0) return; - - disposable.dispose(); - - for (const r of repositories) { - this._repositoryTree.set(r.path, r); - } - - await this.updateContext(this._repositoryTree); - - // Defer the event trigger enough to let everything unwind - setImmediate(() => this.fireRepositoriesChanged()); - }, this) - ); - } - } - - private fireRepositoriesChanged() { - this._onDidChangeRepositories.fire(); - } - - checkoutFile(uri: GitUri, ref?: string) { - ref = ref || uri.sha; - Logger.log(`checkoutFile('${uri.repoPath}', '${uri.fsPath}', '${ref}')`); - - return Git.checkout(uri.repoPath!, uri.fsPath, ref!); - } - - private async fileExists( - repoPath: string, - fileName: string, - options: { ensureCase: boolean } = { ensureCase: false } - ): Promise { - const filePath = path.resolve(repoPath, fileName); - const exists = await new Promise((resolve, reject) => fs.exists(filePath, resolve)); - if (!options.ensureCase || !exists) return exists; - - // Deal with renames in case only on case-insensative file systems - const normalizedRepoPath = path.normalize(repoPath); - return this.fileExistsWithCase(filePath, normalizedRepoPath, normalizedRepoPath.length); - } - - private async fileExistsWithCase(filePath: string, repoPath: string, repoPathLength: number): Promise { - const dir = path.dirname(filePath); - if (dir.length < repoPathLength) return false; - if (dir === repoPath) return true; - - const filenames = await new Promise((resolve, reject) => - fs.readdir(dir, (err: NodeJS.ErrnoException, files: string[]) => { - if (err) { - reject(err); - } - else { - resolve(files); - } - }) - ); - if (filenames.indexOf(path.basename(filePath)) === -1) { - return false; - } - return this.fileExistsWithCase(dir, repoPath, repoPathLength); - } - - async findNextCommit(repoPath: string, fileName: string, ref?: string): Promise { - let log = await this.getLogForFile(repoPath, fileName, { maxCount: 1, ref: ref, renames: true, reverse: true }); - let commit = log && Iterables.first(log.commits.values()); - if (commit) return commit; - - const nextFileName = await this.findNextFileName(repoPath, fileName, ref); - if (nextFileName) { - log = await this.getLogForFile(repoPath, nextFileName, { - maxCount: 1, - ref: ref, - renames: true, - reverse: true - }); - commit = log && Iterables.first(log.commits.values()); - } - - return commit; - } - - async findNextFileName(repoPath: string | undefined, fileName: string, ref?: string): Promise { - [fileName, repoPath] = Git.splitPath(fileName, repoPath); - - return (await this.fileExists(repoPath, fileName, { ensureCase: true })) - ? fileName - : await this.findNextFileNameCore(repoPath, fileName, ref); - } - - private async findNextFileNameCore(repoPath: string, fileName: string, ref?: string): Promise { - if (ref === undefined) { - // Get the most recent commit for this file name - ref = await this.getRecentShaForFile(repoPath, fileName); - if (ref === undefined) return undefined; - } - - // Get the full commit (so we can see if there are any matching renames in the file statuses) - const log = await this.getLog(repoPath, { maxCount: 1, ref: ref }); - if (log === undefined) return undefined; - - const c = Iterables.first(log.commits.values()); - const status = c.fileStatuses.find(f => f.originalFileName === fileName); - if (status === undefined) return undefined; - - return status.fileName; - } - - async findWorkingFileName(commit: GitCommit): Promise<[string | undefined, string | undefined]>; - async findWorkingFileName( - fileName: string, - repoPath?: string, - ref?: string - ): Promise<[string | undefined, string | undefined]>; - async findWorkingFileName( - commitOrFileName: GitCommit | string, - repoPath?: string, - ref?: string - ): Promise<[string | undefined, string | undefined]> { - let fileName; - if (typeof commitOrFileName === 'string') { - fileName = commitOrFileName; - if (repoPath === undefined) { - repoPath = await this.getRepoPath(fileName, { ref: ref }); - [fileName, repoPath] = Git.splitPath(fileName, repoPath); - } - else { - fileName = Strings.normalizePath(path.relative(repoPath, fileName)); - } - } - else { - const c = commitOrFileName; - repoPath = c.repoPath; - if (c.workingFileName && (await this.fileExists(repoPath, c.workingFileName, { ensureCase: true }))) { - return [c.workingFileName, repoPath]; - } - fileName = c.fileName; - } - - // Keep walking up to the most recent commit for a given filename, until it exists on disk - while (true) { - if (await this.fileExists(repoPath, fileName, { ensureCase: true })) return [fileName, repoPath]; - - fileName = await this.findNextFileNameCore(repoPath, fileName); - if (fileName === undefined) return [undefined, undefined]; - } - } - - async getActiveRepoPath(editor?: TextEditor): Promise { - editor = editor || window.activeTextEditor; - - let repoPath; - if (editor != null) { - const doc = await Container.tracker.getOrAdd(editor.document.uri); - if (doc !== undefined) { - repoPath = doc.uri.repoPath; - } - } - - if (repoPath != null) return repoPath; - - return this.getHighlanderRepoPath(); - } - - getHighlanderRepoPath(): string | undefined { - const entry = this._repositoryTree.highlander(); - if (entry === undefined) return undefined; - - const [repo] = entry; - return repo.path; - } - - async getBlameForFile(uri: GitUri): Promise { - let key = 'blame'; - if (uri.sha !== undefined) { - key += `:${uri.sha}`; - } - - const doc = await Container.tracker.getOrAdd(uri); - if (this.UseCaching) { - if (doc.state !== undefined) { - const cachedBlame = doc.state.get(key); - if (cachedBlame !== undefined) { - Logger.log(`getBlameForFile[Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`); - return cachedBlame.item; - } - } - - Logger.log(`getBlameForFile[Not Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`); - - if (doc.state === undefined) { - doc.state = new GitDocumentState(doc.key); - } - } - else { - Logger.log(`getBlameForFile('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`); - } - - const promise = this.getBlameForFileCore(uri, doc, key); - - if (doc.state !== undefined) { - Logger.log(`Add blame cache for '${doc.state.key}:${key}'`); - - doc.state.set(key, { - item: promise - } as CachedBlame); - } - - return promise; - } - - private async getBlameForFileCore( - uri: GitUri, - document: TrackedDocument, - key: string - ): Promise { - if (!(await this.isTracked(uri))) { - Logger.log(`Skipping blame; '${uri.fsPath}' is not tracked`); - return GitService.emptyPromise as Promise; - } - - const [file, root] = Git.splitPath(uri.fsPath, uri.repoPath, false); - - try { - const data = await Git.blame(root, file, uri.sha, { - args: Container.config.advanced.blame.customArguments, - ignoreWhitespace: Container.config.blame.ignoreWhitespace - }); - const blame = GitBlameParser.parse(data, root, file, await this.getCurrentUser(root)); - return blame; - } - catch (ex) { - // Trap and cache expected blame errors - if (document.state !== undefined) { - const msg = ex && ex.toString(); - Logger.log(`Replace blame cache with empty promise for '${document.state.key}:${key}'`); - - document.state.set(key, { - item: GitService.emptyPromise, - errorMessage: msg - } as CachedBlame); - - document.setBlameFailure(); - - return GitService.emptyPromise as Promise; - } - - return undefined; - } - } - - async getBlameForFileContents(uri: GitUri, contents: string): Promise { - const key = `blame:${Strings.sha1(contents)}`; - - const doc = await Container.tracker.getOrAdd(uri); - if (this.UseCaching) { - if (doc.state !== undefined) { - const cachedBlame = doc.state.get(key); - if (cachedBlame !== undefined) { - Logger.log( - `getBlameForFileContents[Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')` - ); - return cachedBlame.item; - } - } - - Logger.log(`getBlameForFileContents[Not Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`); - - if (doc.state === undefined) { - doc.state = new GitDocumentState(doc.key); - } - } - else { - Logger.log(`getBlameForFileContents('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`); - } - - const promise = this.getBlameForFileContentsCore(uri, contents, doc, key); - - if (doc.state !== undefined) { - Logger.log(`Add blame cache for '${doc.state.key}:${key}'`); - - doc.state.set(key, { - item: promise - } as CachedBlame); - } - - return promise; - } - - async getBlameForFileContentsCore( - uri: GitUri, - contents: string, - document: TrackedDocument, - key: string - ): Promise { - if (!(await this.isTracked(uri))) { - Logger.log(`Skipping blame; '${uri.fsPath}' is not tracked`); - return GitService.emptyPromise as Promise; - } - - const [file, root] = Git.splitPath(uri.fsPath, uri.repoPath, false); - - try { - const data = await Git.blame_contents(root, file, contents, { - args: Container.config.advanced.blame.customArguments, - correlationKey: `:${key}`, - ignoreWhitespace: Container.config.blame.ignoreWhitespace - }); - const blame = GitBlameParser.parse(data, root, file, await this.getCurrentUser(root)); - return blame; - } - catch (ex) { - // Trap and cache expected blame errors - if (document.state !== undefined) { - const msg = ex && ex.toString(); - Logger.log(`Replace blame cache with empty promise for '${document.state.key}:${key}'`); - - document.state.set(key, { - item: GitService.emptyPromise, - errorMessage: msg - } as CachedBlame); - - document.setBlameFailure(); - return GitService.emptyPromise as Promise; - } - - return undefined; - } - } - - async getBlameForLine( - uri: GitUri, - line: number, - options: { skipCache?: boolean } = {} - ): Promise { - Logger.log(`getBlameForLine('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}', ${line})`); - - if (!options.skipCache && this.UseCaching) { - const blame = await this.getBlameForFile(uri); - if (blame === undefined) return undefined; - - let blameLine = blame.lines[line]; - if (blameLine === undefined) { - if (blame.lines.length !== line) return undefined; - blameLine = blame.lines[line - 1]; - } - - const commit = blame.commits.get(blameLine.sha); - if (commit === undefined) return undefined; - - return { - author: { ...blame.authors.get(commit.author), lineCount: commit.lines.length }, - commit: commit, - line: blameLine - } as GitBlameLine; - } - - const lineToBlame = line + 1; - const fileName = uri.fsPath; - - try { - const data = await Git.blame(uri.repoPath, fileName, uri.sha, { - args: Container.config.advanced.blame.customArguments, - ignoreWhitespace: Container.config.blame.ignoreWhitespace, - startLine: lineToBlame, - endLine: lineToBlame - }); - const blame = GitBlameParser.parse(data, uri.repoPath, fileName, await this.getCurrentUser(uri.repoPath!)); - if (blame === undefined) return undefined; - - return { - author: Iterables.first(blame.authors.values()), - commit: Iterables.first(blame.commits.values()), - line: blame.lines[line] - } as GitBlameLine; - } - catch { - return undefined; - } - } - - async getBlameForLineContents( - uri: GitUri, - line: number, - contents: string, - options: { skipCache?: boolean } = {} - ): Promise { - Logger.log(`getBlameForLineContents('${uri.repoPath}', '${uri.fsPath}', ${line})`); - - if (!options.skipCache && this.UseCaching) { - const blame = await this.getBlameForFileContents(uri, contents); - if (blame === undefined) return undefined; - - let blameLine = blame.lines[line]; - if (blameLine === undefined) { - if (blame.lines.length !== line) return undefined; - blameLine = blame.lines[line - 1]; - } - - const commit = blame.commits.get(blameLine.sha); - if (commit === undefined) return undefined; - - return { - author: { ...blame.authors.get(commit.author), lineCount: commit.lines.length }, - commit: commit, - line: blameLine - } as GitBlameLine; - } - - const lineToBlame = line + 1; - const fileName = uri.fsPath; - - try { - const data = await Git.blame_contents(uri.repoPath, fileName, contents, { - args: Container.config.advanced.blame.customArguments, - ignoreWhitespace: Container.config.blame.ignoreWhitespace, - startLine: lineToBlame, - endLine: lineToBlame - }); - const currentUser = await this.getCurrentUser(uri.repoPath!); - const blame = GitBlameParser.parse(data, uri.repoPath, fileName, currentUser); - if (blame === undefined) return undefined; - - return { - author: Iterables.first(blame.authors.values()), - commit: Iterables.first(blame.commits.values()), - line: blame.lines[line] - } as GitBlameLine; - } - catch { - return undefined; - } - } - - async getBlameForRange(uri: GitUri, range: Range): Promise { - Logger.log( - `getBlameForRange('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}', [${range.start.line}, ${ - range.end.line - }])` - ); - - const blame = await this.getBlameForFile(uri); - if (blame === undefined) return undefined; - - return this.getBlameForRangeSync(blame, uri, range); - } - - getBlameForRangeSync(blame: GitBlame, uri: GitUri, range: Range): GitBlameLines | undefined { - Logger.log( - `getBlameForRangeSync('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}', [${range.start.line}, ${ - range.end.line - }])` - ); - - if (blame.lines.length === 0) return { allLines: blame.lines, ...blame }; - - if (range.start.line === 0 && range.end.line === blame.lines.length - 1) { - return { allLines: blame.lines, ...blame }; - } - - const lines = blame.lines.slice(range.start.line, range.end.line + 1); - const shas = new Set(lines.map(l => l.sha)); - - const authors: Map = new Map(); - const commits: Map = new Map(); - for (const c of blame.commits.values()) { - if (!shas.has(c.sha)) continue; - - const commit = c.with({ - lines: c.lines.filter(l => l.line >= range.start.line && l.line <= range.end.line) - }); - commits.set(c.sha, commit); - - let author = authors.get(commit.author); - if (author === undefined) { - author = { - name: commit.author, - lineCount: 0 - }; - authors.set(author.name, author); - } - - author.lineCount += commit.lines.length; - } - - const sortedAuthors = new Map([...authors.entries()].sort((a, b) => b[1].lineCount - a[1].lineCount)); - - return { - authors: sortedAuthors, - commits: commits, - lines: lines, - allLines: blame.lines - } as GitBlameLines; - } - - async getBranch(repoPath: string | undefined): Promise { - if (repoPath === undefined) return undefined; - - Logger.log(`getBranch('${repoPath}')`); - - const data = await Git.revparse_currentBranch(repoPath); - if (data === undefined) return undefined; - - const branch = data[0].split('\n'); - return new GitBranch(repoPath, branch[0], true, data[1], branch[1]); - } - - async getBranches(repoPath: string | undefined): Promise { - if (repoPath === undefined) return []; - - Logger.log(`getBranches('${repoPath}')`); - - const data = await Git.branch(repoPath, { all: true }); - // If we don't get any data, assume the repo doesn't have any commits yet so check if we have a current branch - if (data === '') { - const current = await this.getBranch(repoPath); - return current !== undefined ? [current] : []; - } - - return GitBranchParser.parse(data, repoPath) || []; - } - - async getChangedFilesCount(repoPath: string, sha?: string): Promise { - Logger.log(`getChangedFilesCount('${repoPath}', '${sha}')`); - - const data = await Git.diff_shortstat(repoPath, sha); - return GitDiffParser.parseShortStat(data); - } - - async getConfig(key: string, repoPath?: string): Promise { - Logger.log(`getConfig('${key}', '${repoPath}')`); - - return await Git.config_get(key, repoPath); - } - - private _userMapCache = new Map(); - - async getCurrentUser(repoPath: string) { - let user = this._userMapCache.get(repoPath); - if (user != null) return user; - // If we found the repo, but no user data was found just return - if (user === null) return undefined; - - const data = await Git.config_getRegex('user.(name|email)', repoPath); - if (!data) { - // If we found no user data, mark it so we won't bother trying again - this._userMapCache.set(repoPath, null); - return undefined; - } - - user = { name: undefined, email: undefined }; - - let match: RegExpExecArray | null = null; - do { - match = userConfigRegex.exec(data); - if (match == null) break; - - // Stops excessive memory usage -- https://bugs.chromium.org/p/v8/issues/detail?id=2869 - user[match[1] as 'name' | 'email'] = (' ' + match[2]).substr(1); - } while (match != null); - - const author = `${user.name} <${user.email}>`; - // Check if there is a mailmap for the current user - const mappedAuthor = await Git.check_mailmap(repoPath, author); - if (author !== mappedAuthor) { - match = mappedAuthorRegex.exec(mappedAuthor); - if (match != null) { - [, user.name, user.email] = match; - } - } - - this._userMapCache.set(repoPath, user); - return user; - } - - async getDiffForFile(uri: GitUri, sha1?: string, sha2?: string): Promise { - if (sha1 !== undefined && sha2 === undefined && uri.sha !== undefined) { - sha2 = uri.sha; - } - - let key = 'diff'; - if (sha1 !== undefined) { - key += `:${sha1}`; - } - if (sha2 !== undefined) { - key += `:${sha2}`; - } - - const doc = await Container.tracker.getOrAdd(uri); - if (this.UseCaching) { - if (doc.state !== undefined) { - const cachedDiff = doc.state.get(key); - if (cachedDiff !== undefined) { - Logger.log( - `getDiffForFile[Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${sha1}', '${sha2}')` - ); - return cachedDiff.item; - } - } - - Logger.log(`getDiffForFile[Not Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${sha1}', '${sha2}')`); - - if (doc.state === undefined) { - doc.state = new GitDocumentState(doc.key); - } - } - else { - Logger.log(`getDiffForFile('${uri.repoPath}', '${uri.fsPath}', '${sha1}', '${sha2}')`); - } - - const promise = this.getDiffForFileCore( - uri.repoPath, - uri.fsPath, - sha1, - sha2, - { encoding: GitService.getEncoding(uri) }, - doc, - key - ); - - if (doc.state !== undefined) { - Logger.log(`Add log cache for '${doc.state.key}:${key}'`); - - doc.state.set(key, { - item: promise - } as CachedDiff); - } - - return promise; - } - - private async getDiffForFileCore( - repoPath: string | undefined, - fileName: string, - sha1: string | undefined, - sha2: string | undefined, - options: { encoding?: string }, - document: TrackedDocument, - key: string - ): Promise { - const [file, root] = Git.splitPath(fileName, repoPath, false); - - try { - const data = await Git.diff(root, file, sha1, sha2, options); - const diff = GitDiffParser.parse(data); - return diff; - } - catch (ex) { - // Trap and cache expected diff errors - if (document.state !== undefined) { - const msg = ex && ex.toString(); - Logger.log(`Replace diff cache with empty promise for '${document.state.key}:${key}'`); - - document.state.set(key, { - item: GitService.emptyPromise, - errorMessage: msg - } as CachedDiff); - - return GitService.emptyPromise as Promise; - } - - return undefined; - } - } - - async getDiffForLine( - uri: GitUri, - line: number, - sha1?: string, - sha2?: string - ): Promise { - Logger.log(`getDiffForLine('${uri.repoPath}', '${uri.fsPath}', ${line}, '${sha1}', '${sha2}')`); - - try { - const diff = await this.getDiffForFile(uri, sha1, sha2); - if (diff === undefined) return undefined; - - const chunk = diff.chunks.find(c => c.currentPosition.start <= line && c.currentPosition.end >= line); - if (chunk === undefined) return undefined; - - return chunk.lines[line - chunk.currentPosition.start + 1]; - } - catch (ex) { - return undefined; - } - } - - async getDiffStatus( - repoPath: string, - sha1?: string, - sha2?: string, - options: { filter?: string } = {} - ): Promise { - Logger.log(`getDiffStatus('${repoPath}', '${sha1}', '${sha2}', ${options.filter})`); - - try { - const data = await Git.diff_nameStatus(repoPath, sha1, sha2, options); - const diff = GitDiffParser.parseNameStatus(data, repoPath); - return diff; - } - catch (ex) { - return undefined; - } - } - - async getRecentLogCommitForFile(repoPath: string | undefined, fileName: string): Promise { - return this.getLogCommitForFile(repoPath, fileName, undefined); - } - - async getRecentShaForFile(repoPath: string, fileName: string) { - return await Git.log_recent(repoPath, fileName); - } - - async getLogCommit(repoPath: string, ref: string): Promise { - Logger.log(`getLogCommit('${repoPath}', '${ref}'`); - - const log = await this.getLog(repoPath, { maxCount: 2, ref: ref }); - if (log === undefined) return undefined; - - return log.commits.get(ref); - } - - async getLogCommitForFile( - repoPath: string | undefined, - fileName: string, - options: { ref?: string; firstIfNotFound?: boolean } = {} - ): Promise { - Logger.log(`getFileLogCommit('${repoPath}', '${fileName}', '${options.ref}', ${options.firstIfNotFound})`); - - const log = await this.getLogForFile(repoPath, fileName, { maxCount: 2, ref: options.ref }); - if (log === undefined) return undefined; - - const commit = options.ref && log.commits.get(options.ref); - if (commit === undefined && !options.firstIfNotFound && options.ref) { - // If the sha isn't resolved we will never find it, so let it fall through so we return the first - if (!Git.isResolveRequired(options.ref)) return undefined; - } - - return commit || Iterables.first(log.commits.values()); - } - - async getLog( - repoPath: string, - options: { author?: string; maxCount?: number; ref?: string; reverse?: boolean } = {} - ): Promise { - options = { reverse: false, ...options }; - - Logger.log(`getLog('${repoPath}', '${options.ref}', ${options.maxCount}, ${options.reverse})`); - - const maxCount = options.maxCount == null ? Container.config.advanced.maxListItems || 0 : options.maxCount; - - try { - const data = await Git.log(repoPath, { - author: options.author, - maxCount: maxCount, - ref: options.ref, - reverse: options.reverse - }); - const log = GitLogParser.parse( - data, - GitCommitType.Branch, - repoPath, - undefined, - options.ref, - await this.getCurrentUser(repoPath), - maxCount, - options.reverse!, - undefined - ); - - if (log !== undefined) { - const opts = { ...options }; - log.query = (maxCount: number | undefined) => this.getLog(repoPath, { ...opts, maxCount: maxCount }); - } - - return log; - } - catch (ex) { - return undefined; - } - } - - async getLogForSearch( - repoPath: string, - search: string, - searchBy: GitRepoSearchBy, - options: { maxCount?: number } = {} - ): Promise { - Logger.log(`getLogForSearch('${repoPath}', '${search}', '${searchBy}', ${options.maxCount})`); - - let maxCount = options.maxCount == null ? Container.config.advanced.maxListItems || 0 : options.maxCount; - - let searchArgs: string[] | undefined = undefined; - switch (searchBy) { - case GitRepoSearchBy.Author: - searchArgs = ['-m', '-M', '--all', '--full-history', '-i', `--author=${search}`]; - break; - case GitRepoSearchBy.ChangedLines: - searchArgs = ['-M', '--all', '--full-history', '-i', `-G${search}`]; - break; - case GitRepoSearchBy.Changes: - searchArgs = ['-M', '--all', '--full-history', '-i', '--pickaxe-regex', `-S${search}`]; - break; - case GitRepoSearchBy.Files: - searchArgs = ['-M', '--all', '--full-history', '-i', `--`, `${search}`]; - break; - case GitRepoSearchBy.Message: - searchArgs = ['-m', '-M', '--all', '--full-history']; - if (search) { - searchArgs.push(`--grep=${search}`); - } - break; - case GitRepoSearchBy.Sha: - searchArgs = [`-m`, '-M', search]; - maxCount = 1; - break; - } - - try { - const data = await Git.log_search(repoPath, searchArgs, { maxCount: maxCount }); - const log = GitLogParser.parse( - data, - GitCommitType.Branch, - repoPath, - undefined, - undefined, - await this.getCurrentUser(repoPath), - maxCount, - false, - undefined - ); - - if (log !== undefined) { - const opts = { ...options }; - log.query = (maxCount: number | undefined) => - this.getLogForSearch(repoPath, search, searchBy, { ...opts, maxCount: maxCount }); - } - - return log; - } - catch (ex) { - return undefined; - } - } - - async getLogForFile( - repoPath: string | undefined, - fileName: string, - options: { maxCount?: number; range?: Range; ref?: string; renames?: boolean; reverse?: boolean } = {} - ): Promise { - if (repoPath !== undefined && repoPath === Strings.normalizePath(fileName)) { - throw new Error(`File name cannot match the repository path; fileName=${fileName}`); - } - - options = { reverse: false, ...options }; - - if (options.renames === undefined) { - options.renames = Container.config.advanced.fileHistoryFollowsRenames; - } - - let key = 'log'; - if (options.ref !== undefined) { - key += `:${options.ref}`; - } - if (options.maxCount !== undefined) { - key += `:n${options.maxCount}`; - } - if (options.renames) { - key += `:follow`; - } - if (options.reverse) { - key += `:reverse`; - } - - const doc = await Container.tracker.getOrAdd( - new GitUri(Uri.file(fileName), { repoPath: repoPath!, sha: options.ref }) - ); - if (this.UseCaching && options.range === undefined) { - if (doc.state !== undefined) { - const cachedLog = doc.state.get(key); - if (cachedLog !== undefined) { - Logger.log( - `getLogForFile[Cached(${key})]('${repoPath}', '${fileName}', '${options.ref}', ${ - options.maxCount - }, undefined, ${options.renames}, ${options.reverse})` - ); - return cachedLog.item; - } - - if (options.ref !== undefined || options.maxCount !== undefined) { - // Since we are looking for partial log, see if we have the log of the whole file - const cachedLog = doc.state.get( - `log${options.renames ? ':follow' : ''}${options.reverse ? ':reverse' : ''}` - ); - if (cachedLog !== undefined) { - if (options.ref === undefined) { - Logger.log( - `getLogForFile[Cached(~${key})]('${repoPath}', '${fileName}', '', ${ - options.maxCount - }, undefined, ${options.renames}, ${options.reverse})` - ); - return cachedLog.item; - } - - Logger.log( - `getLogForFile[? Cache(${key})]('${repoPath}', '${fileName}', '${options.ref}', ${ - options.maxCount - }, undefined, ${options.renames}, ${options.reverse})` - ); - const log = await cachedLog.item; - if (log !== undefined && log.commits.has(options.ref)) { - Logger.log( - `getLogForFile[Cached(${key})]('${repoPath}', '${fileName}', '${options.ref}', ${ - options.maxCount - }, undefined, ${options.renames}, ${options.reverse})` - ); - return cachedLog.item; - } - } - } - } - - Logger.log( - `getLogForFile[Not Cached(${key})]('${repoPath}', '${fileName}', ${options.ref}, ${ - options.maxCount - }, undefined, ${options.reverse})` - ); - - if (doc.state === undefined) { - doc.state = new GitDocumentState(doc.key); - } - } - else { - Logger.log( - `getLogForFile('${repoPath}', '${fileName}', ${options.ref}, ${options.maxCount}, ${options.range && - `[${options.range.start.line}, ${options.range.end.line}]`}, ${options.reverse})` - ); - } - - const promise = this.getLogForFileCore(repoPath, fileName, options, doc, key); - - if (doc.state !== undefined && options.range === undefined) { - Logger.log(`Add log cache for '${doc.state.key}:${key}'`); - - doc.state.set(key, { - item: promise - } as CachedLog); - } - - return promise; - } - - private async getLogForFileCore( - repoPath: string | undefined, - fileName: string, - options: { maxCount?: number; range?: Range; ref?: string; renames?: boolean; reverse?: boolean }, - document: TrackedDocument, - key: string - ): Promise { - if (!(await this.isTracked(fileName, repoPath, { ref: options.ref }))) { - Logger.log(`Skipping log; '${fileName}' is not tracked`); - return GitService.emptyPromise as Promise; - } - - const [file, root] = Git.splitPath(fileName, repoPath, false); - - try { - const { range, ...opts } = options; - - const maxCount = options.maxCount == null ? Container.config.advanced.maxListItems || 0 : options.maxCount; - - const data = await Git.log_file(root, file, { - ...opts, - maxCount: maxCount, - startLine: range && range.start.line + 1, - endLine: range && range.end.line + 1 - }); - const log = GitLogParser.parse( - data, - GitCommitType.File, - root, - file, - opts.ref, - await this.getCurrentUser(root), - maxCount, - opts.reverse!, - range - ); - - if (log !== undefined) { - const opts = { ...options }; - log.query = (maxCount: number | undefined) => - this.getLogForFile(repoPath, fileName, { ...opts, maxCount: maxCount }); - } - - return log; - } - catch (ex) { - // Trap and cache expected log errors - if (document.state !== undefined && options.range === undefined && !options.reverse) { - const msg = ex && ex.toString(); - Logger.log(`Replace log cache with empty promise for '${document.state.key}:${key}'`); - - document.state.set(key, { - item: GitService.emptyPromise, - errorMessage: msg - } as CachedLog); - - return GitService.emptyPromise as Promise; - } - - return undefined; - } - } - - async hasRemotes(repoPath: string | undefined): Promise { - if (repoPath === undefined) return false; - - const repository = await this.getRepository(repoPath); - if (repository === undefined) return false; - - return repository.hasRemotes(); - } - - async hasTrackingBranch(repoPath: string | undefined): Promise { - if (repoPath === undefined) return false; - - const repository = await this.getRepository(repoPath); - if (repository === undefined) return false; - - return repository.hasTrackingBranch(); - } - - async getMergeBase(repoPath: string, ref1: string, ref2: string, options: { forkPoint?: boolean } = {}) { - try { - const data = await Git.merge_base(repoPath, ref1, ref2, options); - if (data === undefined) return undefined; - - return data.split('\n')[0]; - } - catch (ex) { - Logger.error(ex, 'GitService.getMergeBase'); - return undefined; - } - } - - async getRemotes(repoPath: string | undefined, options: { includeAll?: boolean } = {}): Promise { - if (repoPath === undefined) return []; - - Logger.log(`getRemotes('${repoPath}')`); - - const repository = await this.getRepository(repoPath); - const remotes = repository !== undefined ? repository.getRemotes() : this.getRemotesCore(repoPath); - - if (options.includeAll) return remotes; - - return (await remotes).filter(r => r.provider !== undefined); - } - - async getRemotesCore(repoPath: string | undefined, providerMap?: RemoteProviderMap): Promise { - if (repoPath === undefined) return []; - - Logger.log(`getRemotesCore('${repoPath}')`); - - providerMap = - providerMap || - RemoteProviderFactory.createMap( - configuration.get(configuration.name('remotes').value, null) - ); - - try { - const data = await Git.remote(repoPath); - return GitRemoteParser.parse(data, repoPath, RemoteProviderFactory.factory(providerMap)); - } - catch (ex) { - Logger.error(ex, 'GitService.getRemotesCore'); - return []; - } - } - - async getRepoPath(filePath: string, options?: { ref?: string }): Promise; - async getRepoPath(uri: Uri | undefined, options?: { ref?: string }): Promise; - async getRepoPath( - filePathOrUri: string | Uri | undefined, - options: { ref?: string } = {} - ): Promise { - if (filePathOrUri == null) return await this.getActiveRepoPath(); - if (filePathOrUri instanceof GitUri) return filePathOrUri.repoPath; - - // Don't save the tracking info to the cache, because we could be looking in the wrong place (e.g. looking in the root when the file is in a submodule) - let repo = await this.getRepository(filePathOrUri, { ...options, skipCacheUpdate: true }); - if (repo !== undefined) return repo.path; - - if (typeof filePathOrUri !== 'string') { - const versionedUri = await Container.git.getVersionedUri(filePathOrUri); - if (versionedUri !== undefined) return versionedUri.repoPath; - } - - const rp = await this.getRepoPathCore( - typeof filePathOrUri === 'string' ? filePathOrUri : filePathOrUri.fsPath, - false - ); - if (rp === undefined) return undefined; - - // Recheck this._repositoryTree.get(rp) to make sure we haven't already tried adding this due to awaits - if (this._repositoryTree.get(rp) !== undefined) return rp; - - // If this new repo is inside one of our known roots and we we don't already know about, add it - const root = this._repositoryTree.findSubstr(rp); - let folder = root === undefined ? workspace.getWorkspaceFolder(Uri.file(rp)) : root.folder; - - if (folder === undefined) { - const parts = rp.split('/'); - folder = { uri: Uri.file(rp), name: parts[parts.length - 1], index: this._repositoryTree.count() }; - } - - Logger.log(`Repository found in '${rp}'`); - repo = new Repository(folder, rp, false, this.onAnyRepositoryChanged.bind(this), this._suspended); - this._repositoryTree.set(rp, repo); - - // Send a notification that the repositories changed - setImmediate(async () => { - await this.updateContext(this._repositoryTree); - - this.fireRepositoriesChanged(); - }); - - return rp; - } - - private async getRepoPathCore(filePath: string, isDirectory: boolean): Promise { - try { - return await Git.revparse_toplevel(isDirectory ? filePath : path.dirname(filePath)); - } - catch (ex) { - Logger.error(ex, 'GitService.getRepoPathCore'); - return undefined; - } - } - - async getRepoPathOrActive(uri: Uri | undefined, editor: TextEditor | undefined) { - const repoPath = await Container.git.getRepoPath(uri); - if (repoPath) return repoPath; - - return Container.git.getActiveRepoPath(editor); - } - - async getRepositories(predicate?: (repo: Repository) => boolean): Promise> { - const repositoryTree = await this.getRepositoryTree(); - - const values = repositoryTree.values(); - return predicate !== undefined ? Iterables.filter(values, predicate) : values; - } - - private async getRepositoryTree(): Promise> { - if (this._repositoriesLoadingPromise !== undefined) { - await this._repositoriesLoadingPromise; - this._repositoriesLoadingPromise = undefined; - } - - return this._repositoryTree; - } - - async getRepository( - repoPath: string, - options?: { ref?: string; skipCacheUpdate?: boolean } - ): Promise; - async getRepository( - uri: Uri, - options?: { ref?: string; skipCacheUpdate?: boolean } - ): Promise; - async getRepository( - repoPathOrUri: string | Uri, - options?: { ref?: string; skipCacheUpdate?: boolean } - ): Promise; - async getRepository( - repoPathOrUri: string | Uri, - options: { ref?: string; skipCacheUpdate?: boolean } = {} - ): Promise { - const repositoryTree = await this.getRepositoryTree(); - - let path: string; - if (typeof repoPathOrUri === 'string') { - const repo = repositoryTree.get(repoPathOrUri); - if (repo !== undefined) return repo; - - path = repoPathOrUri; - } - else { - if (repoPathOrUri instanceof GitUri) { - if (repoPathOrUri.repoPath) { - const repo = repositoryTree.get(repoPathOrUri.repoPath); - if (repo !== undefined) return repo; - } - - path = repoPathOrUri.fsPath; - } - else { - path = repoPathOrUri.fsPath; - } - } - - const repo = repositoryTree.findSubstr(path); - if (repo === undefined) return undefined; - - // Make sure the file is tracked in this repo before returning -- it could be from a submodule - if (!(await this.isTracked(path, repo.path, options))) return undefined; - return repo; - } - - async getRepositoryCount(): Promise { - const repositoryTree = await this.getRepositoryTree(); - return repositoryTree.count(); - } - - async getStashList(repoPath: string | undefined): Promise { - if (repoPath === undefined) return undefined; - - Logger.log(`getStashList('${repoPath}')`); - - const data = await Git.stash_list(repoPath); - const stash = GitStashParser.parse(data, repoPath); - return stash; - } - - async getStatusForFile(repoPath: string, fileName: string): Promise { - Logger.log(`getStatusForFile('${repoPath}', '${fileName}')`); - - const porcelainVersion = Git.validateVersion(2, 11) ? 2 : 1; - - const data = await Git.status_file(repoPath, fileName, porcelainVersion); - const status = GitStatusParser.parse(data, repoPath, porcelainVersion); - if (status === undefined || !status.files.length) return undefined; - - return status.files[0]; - } - - async getStatusForRepo(repoPath: string | undefined): Promise { - if (repoPath === undefined) return undefined; - - Logger.log(`getStatusForRepo('${repoPath}')`); - - const porcelainVersion = Git.validateVersion(2, 11) ? 2 : 1; - - const data = await Git.status(repoPath, porcelainVersion); - const status = GitStatusParser.parse(data, repoPath, porcelainVersion); - return status; - } - - async getTags(repoPath: string | undefined): Promise { - if (repoPath === undefined) return []; - - Logger.log(`getTags('${repoPath}')`); - - const data = await Git.tag(repoPath); - return GitTagParser.parse(data, repoPath) || []; - } - - async getTreeFileForRevision(repoPath: string, fileName: string, ref: string): Promise { - if (repoPath === undefined) return undefined; - - Logger.log(`getTreeFileForRevision('${repoPath}', '${fileName}', '${ref}')`); - - const data = await Git.ls_tree(repoPath, ref, { fileName: fileName }); - const trees = GitTreeParser.parse(data); - return trees === undefined || trees.length === 0 ? undefined : trees[0]; - } - - async getTreeForRevision(repoPath: string, ref: string): Promise { - if (repoPath === undefined) return []; - - Logger.log(`getTreeForRevision('${repoPath}', '${ref}')`); - - const data = await Git.ls_tree(repoPath, ref); - return GitTreeParser.parse(data) || []; - } - - async getVersionedFile( - repoPath: string | undefined, - fileName: string, - sha: string | undefined - ): Promise { - Logger.log(`getVersionedFile('${repoPath}', '${fileName}', '${sha}')`); - - if (sha === GitService.deletedSha) return undefined; - - if (!sha || (Git.isUncommitted(sha) && !Git.isStagedUncommitted(sha))) { - if (await this.fileExists(repoPath!, fileName)) return Uri.file(fileName); - - return undefined; - } - - return GitUri.toRevisionUri(sha, fileName, repoPath!); - } - - getVersionedFileBuffer(repoPath: string, fileName: string, sha: string) { - Logger.log(`getVersionedFileBuffer('${repoPath}', '${fileName}', ${sha})`); - - return Git.show(repoPath, fileName, sha, { encoding: 'buffer' }); - } - - // getVersionedFileText(repoPath: string, fileName: string, sha: string) { - // Logger.log(`getVersionedFileText('${repoPath}', '${fileName}', ${sha})`); - - // return Git.show(repoPath, fileName, sha, { encoding: GitService.getEncoding(repoPath, fileName) }); - // } - - getVersionedUri(uri: Uri) { - return this._versionedUriCache.get(GitUri.toKey(uri)); - } - - isTrackable(scheme: string): boolean; - isTrackable(uri: Uri): boolean; - isTrackable(schemeOruri: string | Uri): boolean { - let scheme: string; - if (typeof schemeOruri === 'string') { - scheme = schemeOruri; - } - else { - scheme = schemeOruri.scheme; - } - - return scheme === DocumentSchemes.File || scheme === DocumentSchemes.Git || scheme === DocumentSchemes.GitLens; - } - - async isTracked( - fileName: string, - repoPath?: string, - options?: { ref?: string; skipCacheUpdate?: boolean } - ): Promise; - async isTracked(uri: GitUri): Promise; - async isTracked( - fileNameOrUri: string | GitUri, - repoPath?: string, - options: { ref?: string; skipCacheUpdate?: boolean } = {} - ): Promise { - if (options.ref === GitService.deletedSha) return false; - - let ref = options.ref; - let cacheKey: string; - let fileName: string; - if (typeof fileNameOrUri === 'string') { - [fileName, repoPath] = Git.splitPath(fileNameOrUri, repoPath); - cacheKey = GitUri.toKey(fileNameOrUri); - } - else { - if (!this.isTrackable(fileNameOrUri)) return false; - - fileName = fileNameOrUri.fsPath; - repoPath = fileNameOrUri.repoPath; - ref = fileNameOrUri.sha; - cacheKey = GitUri.toKey(fileName); - } - - if (ref !== undefined) { - cacheKey += `:${ref}`; - } - - let tracked = this._trackedCache.get(cacheKey); - try { - if (tracked !== undefined) { - tracked = await tracked; - - return tracked; - } - - tracked = this.isTrackedCore(fileName, repoPath === undefined ? '' : repoPath, ref); - if (options.skipCacheUpdate) { - tracked = await tracked; - - return tracked; - } - - this._trackedCache.set(cacheKey, tracked); - tracked = await tracked; - this._trackedCache.set(cacheKey, tracked); - - return tracked; - } - finally { - Logger.log(`isTracked('${fileName}', '${repoPath}'${ref !== undefined ? `, '${ref}'` : ''}) = ${tracked}`); - } - } - - private async isTrackedCore(fileName: string, repoPath: string, ref?: string) { - if (ref === GitService.deletedSha) return false; - - try { - // Even if we have a sha, check first to see if the file exists (that way the cache will be better reused) - let tracked = !!(await Git.ls_files(repoPath === undefined ? '' : repoPath, fileName)); - if (!tracked && ref !== undefined) { - tracked = !!(await Git.ls_files(repoPath === undefined ? '' : repoPath, fileName, { ref: ref })); - // If we still haven't found this file, make sure it wasn't deleted in that sha (i.e. check the previous) - if (!tracked) { - tracked = !!(await Git.ls_files(repoPath === undefined ? '' : repoPath, fileName, { - ref: `${ref}^` - })); - } - } - return tracked; - } - catch (ex) { - Logger.error(ex, 'GitService.isTrackedCore'); - return false; - } - } - - async getDiffTool(repoPath?: string) { - return (await Git.config_get('diff.guitool', repoPath)) || (await Git.config_get('diff.tool', repoPath)); - } - - async openDiffTool(repoPath: string, uri: Uri, staged: boolean, tool?: string) { - if (!tool) { - tool = await this.getDiffTool(repoPath); - if (tool === undefined) throw new Error('No diff tool found'); - } - - Logger.log(`openDiffTool('${repoPath}', '${uri.fsPath}', ${staged}, '${tool}')`); - - return Git.difftool_fileDiff(repoPath, uri.fsPath, tool, staged); - } - - async openDirectoryDiff(repoPath: string, ref1: string, ref2?: string, tool?: string) { - if (!tool) { - tool = await this.getDiffTool(repoPath); - if (tool === undefined) throw new Error('No diff tool found'); - } - - Logger.log(`openDirectoryDiff('${repoPath}', '${ref1}', '${ref2}', '${tool}')`); - - return Git.difftool_dirDiff(repoPath, tool, ref1, ref2); - } - - async resolveReference(repoPath: string, ref: string, uri?: Uri) { - if (!GitService.isResolveRequired(ref) || ref.endsWith('^3')) return ref; - - Logger.log(`resolveReference('${repoPath}', '${ref}', '${uri && uri.toString(true)}')`); - - if (uri == null) return (await Git.revparse(repoPath, ref)) || ref; - - return ( - (await Git.log_resolve(repoPath, Strings.normalizePath(path.relative(repoPath, uri.fsPath)), ref)) || ref - ); - } - - stopWatchingFileSystem() { - this._repositoryTree.forEach(r => r.stopWatchingFileSystem()); - } - - stashApply(repoPath: string, stashName: string, deleteAfter: boolean = false) { - Logger.log(`stashApply('${repoPath}', '${stashName}', ${deleteAfter})`); - - return Git.stash_apply(repoPath, stashName, deleteAfter); - } - - stashDelete(repoPath: string, stashName: string) { - Logger.log(`stashDelete('${repoPath}', '${stashName}')`); - - return Git.stash_delete(repoPath, stashName); - } - - stashSave(repoPath: string, message?: string, uris?: Uri[]) { - Logger.log(`stashSave('${repoPath}', '${message}', ${uris})`); - - if (uris === undefined) return Git.stash_save(repoPath, message); - - GitService.ensureGitVersion('2.13.2', 'Stashing individual files'); - - const pathspecs = uris.map(u => Git.splitPath(u.fsPath, repoPath)[0]); - return Git.stash_push(repoPath, pathspecs, message); - } - - static getEncoding(repoPath: string, fileName: string): string; - static getEncoding(uri: Uri): string; - static getEncoding(repoPathOrUri: string | Uri, fileName?: string): string { - const uri = typeof repoPathOrUri === 'string' ? Uri.file(path.join(repoPathOrUri, fileName!)) : repoPathOrUri; - return Git.getEncoding(workspace.getConfiguration('files', uri).get('encoding')); - } - - static async initialize(): Promise { - // Try to use the same git as the built-in vscode git extension - let gitPath; - try { - const gitExtension = extensions.getExtension('vscode.git'); - if (gitExtension !== undefined) { - const gitApi = ((await gitExtension.activate()) as GitExtension).getAPI(1); - gitPath = gitApi.git.path; - } - } - catch {} - - await Git.setOrFindGitPath(gitPath || workspace.getConfiguration('git').get('path')); - } - - static getGitPath(): string { - return Git.getGitPath(); - } - - static getGitVersion(): string { - return Git.getGitVersion(); - } - - static isResolveRequired(sha: string): boolean { - return Git.isResolveRequired(sha); - } - - static isSha(sha: string): boolean { - return Git.isSha(sha); - } - - static isStagedUncommitted(sha: string | undefined): boolean { - return Git.isStagedUncommitted(sha); - } - - static isUncommitted(sha: string | undefined): boolean { - return Git.isUncommitted(sha); - } - - static shortenSha( - sha: string | undefined, - strings: { deleted?: string; stagedUncommitted?: string; uncommitted?: string; working?: string } = {} - ) { - if (sha === undefined) return undefined; - - strings = { deleted: '(deleted)', working: '', ...strings }; - - if (sha === '') return strings.working; - if (sha === GitService.deletedSha) return strings.deleted; - - return Git.isSha(sha) || Git.isStagedUncommitted(sha) ? Git.shortenSha(sha, strings) : sha; - } - - static compareGitVersion(version: string, throwIfLessThan?: Error) { - return Versions.compare(Versions.fromString(this.getGitVersion()), Versions.fromString(version)); - } - - static ensureGitVersion(version: string, feature: string): void { - const gitVersion = this.getGitVersion(); - if (Versions.compare(Versions.fromString(gitVersion), Versions.fromString(version)) === -1) { - throw new Error( - `${feature} requires a newer version of Git (>= ${version}) than is currently installed (${gitVersion}). Please install a more recent version of Git to use this GitLens feature.` - ); - } - } -} diff --git a/src/messages.ts b/src/messages.ts index 137a642..efc8a1c 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -2,7 +2,7 @@ import { ConfigurationTarget, MessageItem, window } from 'vscode'; import { configuration, KeyMap } from './configuration'; import { Container } from './container'; -import { GitCommit } from './gitService'; +import { GitCommit } from './git/gitService'; import { Logger } from './logger'; export enum SuppressedMessages { diff --git a/src/quickpicks/branchHistoryQuickPick.ts b/src/quickpicks/branchHistoryQuickPick.ts index 0ab0cf0..b826aea 100644 --- a/src/quickpicks/branchHistoryQuickPick.ts +++ b/src/quickpicks/branchHistoryQuickPick.ts @@ -3,7 +3,7 @@ import { CancellationTokenSource, QuickPickOptions, window } from 'vscode'; import { Commands, ShowCommitSearchCommandArgs, ShowQuickBranchHistoryCommandArgs } from '../commands'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitLog, GitUri, RemoteResource } from '../gitService'; +import { GitLog, GitUri, RemoteResource } from '../git/gitService'; import { KeyNoopCommand } from '../keyboard'; import { Iterables, Strings } from '../system'; import { diff --git a/src/quickpicks/branchesAndTagsQuickPick.ts b/src/quickpicks/branchesAndTagsQuickPick.ts index 03002ed..b517d7c 100644 --- a/src/quickpicks/branchesAndTagsQuickPick.ts +++ b/src/quickpicks/branchesAndTagsQuickPick.ts @@ -2,7 +2,7 @@ import { CancellationTokenSource, QuickPickItem, QuickPickOptions, window } from 'vscode'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitBranch, GitTag } from '../gitService'; +import { GitBranch, GitTag } from '../git/gitService'; import { KeyNoopCommand } from '../keyboard'; import { CommandQuickPickItem, getQuickPickIgnoreFocusOut, showQuickPickProgress } from './commonQuickPicks'; diff --git a/src/quickpicks/branchesQuickPick.ts b/src/quickpicks/branchesQuickPick.ts index 9e98a16..1ba16ff 100644 --- a/src/quickpicks/branchesQuickPick.ts +++ b/src/quickpicks/branchesQuickPick.ts @@ -1,7 +1,7 @@ 'use strict'; import { QuickPickItem, QuickPickOptions, window } from 'vscode'; import { GlyphChars } from '../constants'; -import { GitBranch } from '../gitService'; +import { GitBranch } from '../git/gitService'; import { CommandQuickPickItem, getQuickPickIgnoreFocusOut } from './commonQuickPicks'; export class BranchQuickPickItem implements QuickPickItem { diff --git a/src/quickpicks/commitFileQuickPick.ts b/src/quickpicks/commitFileQuickPick.ts index ebe4e30..d6b1a01 100644 --- a/src/quickpicks/commitFileQuickPick.ts +++ b/src/quickpicks/commitFileQuickPick.ts @@ -14,7 +14,7 @@ import { } from '../commands'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitLog, GitLogCommit, GitUri, RemoteResource } from '../gitService'; +import { GitLog, GitLogCommit, GitUri, RemoteResource } from '../git/gitService'; import { KeyCommand, KeyNoopCommand } from '../keyboard'; import { Iterables, Strings } from '../system'; import { diff --git a/src/quickpicks/commitQuickPick.ts b/src/quickpicks/commitQuickPick.ts index 5e8cdbd..300811d 100644 --- a/src/quickpicks/commitQuickPick.ts +++ b/src/quickpicks/commitQuickPick.ts @@ -23,7 +23,7 @@ import { GitUri, IGitStatusFile, RemoteResource -} from '../gitService'; +} from '../git/gitService'; import { KeyCommand, KeyNoopCommand, Keys } from '../keyboard'; import { Arrays, Iterables, Strings } from '../system'; import { diff --git a/src/quickpicks/commitsQuickPick.ts b/src/quickpicks/commitsQuickPick.ts index 33bcb48..7914037 100644 --- a/src/quickpicks/commitsQuickPick.ts +++ b/src/quickpicks/commitsQuickPick.ts @@ -1,7 +1,7 @@ 'use strict'; import { CancellationTokenSource, QuickPickOptions, window } from 'vscode'; import { Container } from '../container'; -import { GitLog } from '../gitService'; +import { GitLog } from '../git/gitService'; import { KeyNoopCommand } from '../keyboard'; import { Iterables } from '../system'; import { diff --git a/src/quickpicks/commonQuickPicks.ts b/src/quickpicks/commonQuickPicks.ts index 2310661..36798c6 100644 --- a/src/quickpicks/commonQuickPicks.ts +++ b/src/quickpicks/commonQuickPicks.ts @@ -13,7 +13,7 @@ import { Commands, openEditor } from '../commands'; import { configuration } from '../configuration'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitLog, GitLogCommit, GitStashCommit } from '../gitService'; +import { GitLog, GitLogCommit, GitStashCommit } from '../git/gitService'; import { KeyMapping, Keys } from '../keyboard'; import { Strings } from '../system'; import { BranchesAndTagsQuickPick, BranchOrTagQuickPickItem } from './branchesAndTagsQuickPick'; diff --git a/src/quickpicks/fileHistoryQuickPick.ts b/src/quickpicks/fileHistoryQuickPick.ts index c6c6340..ba052ce 100644 --- a/src/quickpicks/fileHistoryQuickPick.ts +++ b/src/quickpicks/fileHistoryQuickPick.ts @@ -4,7 +4,7 @@ import { CancellationTokenSource, QuickPickOptions, Uri, window } from 'vscode'; import { Commands, ShowQuickCurrentBranchHistoryCommandArgs, ShowQuickFileHistoryCommandArgs } from '../commands'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitLog, GitUri, RemoteResource } from '../gitService'; +import { GitLog, GitUri, RemoteResource } from '../git/gitService'; import { KeyNoopCommand } from '../keyboard'; import { Iterables, Strings } from '../system'; import { diff --git a/src/quickpicks/remotesQuickPick.ts b/src/quickpicks/remotesQuickPick.ts index fda166a..ca60fd7 100644 --- a/src/quickpicks/remotesQuickPick.ts +++ b/src/quickpicks/remotesQuickPick.ts @@ -10,7 +10,7 @@ import { GitService, RemoteResource, RemoteResourceType -} from '../gitService'; +} from '../git/gitService'; import { Strings } from '../system'; import { CommandQuickPickItem, getQuickPickIgnoreFocusOut } from './commonQuickPicks'; diff --git a/src/quickpicks/repoStatusQuickPick.ts b/src/quickpicks/repoStatusQuickPick.ts index f7bfacf..1aeb1e2 100644 --- a/src/quickpicks/repoStatusQuickPick.ts +++ b/src/quickpicks/repoStatusQuickPick.ts @@ -19,7 +19,7 @@ import { GitStatusFile, GitStatusFileStatus, GitUri -} from '../gitService'; +} from '../git/gitService'; import { Keys } from '../keyboard'; import { Iterables, Strings } from '../system'; import { diff --git a/src/quickpicks/repositoriesQuickPick.ts b/src/quickpicks/repositoriesQuickPick.ts index e4077f0..6f3737a 100644 --- a/src/quickpicks/repositoriesQuickPick.ts +++ b/src/quickpicks/repositoriesQuickPick.ts @@ -1,7 +1,7 @@ 'use strict'; import { QuickPickItem, QuickPickOptions, window } from 'vscode'; import { Container } from '../container'; -import { Repository } from '../gitService'; +import { Repository } from '../git/gitService'; import { Iterables } from '../system'; import { CommandQuickPickItem, getQuickPickIgnoreFocusOut } from './commonQuickPicks'; diff --git a/src/quickpicks/stashListQuickPick.ts b/src/quickpicks/stashListQuickPick.ts index 3c40d13..42ecc8e 100644 --- a/src/quickpicks/stashListQuickPick.ts +++ b/src/quickpicks/stashListQuickPick.ts @@ -3,7 +3,7 @@ import { CancellationTokenSource, QuickPickOptions, window } from 'vscode'; import { Commands, StashSaveCommandArgs } from '../commands'; import { GlyphChars } from '../constants'; import { Container } from '../container'; -import { GitStash } from '../gitService'; +import { GitStash } from '../git/gitService'; import { KeyNoopCommand } from '../keyboard'; import { Iterables, Strings } from '../system'; import { diff --git a/src/statusbar/statusBarController.ts b/src/statusbar/statusBarController.ts index 09230f8..105e2fb 100644 --- a/src/statusbar/statusBarController.ts +++ b/src/statusbar/statusBarController.ts @@ -4,7 +4,7 @@ import { Commands } from '../commands'; import { configuration, StatusBarCommand } from '../configuration'; import { isTextEditor } from '../constants'; import { Container } from '../container'; -import { CommitFormatter, GitCommit, ICommitFormatOptions } from '../gitService'; +import { CommitFormatter, GitCommit, ICommitFormatOptions } from '../git/gitService'; import { LinesChangeEvent } from '../trackers/gitLineTracker'; export class StatusBarController implements Disposable { diff --git a/src/trackers/documentTracker.ts b/src/trackers/documentTracker.ts index 05eeeb4..2ee267c 100644 --- a/src/trackers/documentTracker.ts +++ b/src/trackers/documentTracker.ts @@ -17,7 +17,7 @@ import { } from 'vscode'; import { configuration } from '../configuration'; import { CommandContext, DocumentSchemes, isActiveDocument, isTextEditor, setCommandContext } from '../constants'; -import { GitUri } from '../gitService'; +import { GitUri } from '../git/gitService'; import { Functions, IDeferrable } from '../system'; import { DocumentBlameStateChangeEvent, TrackedDocument } from './trackedDocument'; diff --git a/src/trackers/gitLineTracker.ts b/src/trackers/gitLineTracker.ts index 8746266..d7319c7 100644 --- a/src/trackers/gitLineTracker.ts +++ b/src/trackers/gitLineTracker.ts @@ -1,7 +1,7 @@ 'use strict'; import { Disposable, TextEditor } from 'vscode'; import { Container } from '../container'; -import { GitBlameCommit, GitLogCommit } from '../gitService'; +import { GitBlameCommit, GitLogCommit } from '../git/gitService'; import { DocumentBlameStateChangeEvent, DocumentDirtyIdleTriggerEvent, diff --git a/src/trackers/trackedDocument.ts b/src/trackers/trackedDocument.ts index a25c40f..6da4c0a 100644 --- a/src/trackers/trackedDocument.ts +++ b/src/trackers/trackedDocument.ts @@ -2,7 +2,7 @@ import { Disposable, Event, EventEmitter, TextDocument, TextEditor, Uri } from 'vscode'; import { CommandContext, getEditorIfActive, isActiveDocument, setCommandContext } from '../constants'; import { Container } from '../container'; -import { GitUri, Repository, RepositoryChange, RepositoryChangeEvent } from '../gitService'; +import { GitUri, Repository, RepositoryChange, RepositoryChangeEvent } from '../git/gitService'; import { Logger } from '../logger'; import { Functions } from '../system'; diff --git a/src/views/explorerCommands.ts b/src/views/explorerCommands.ts index f60de28..fe1679c 100644 --- a/src/views/explorerCommands.ts +++ b/src/views/explorerCommands.ts @@ -15,7 +15,7 @@ import { import { CommandContext, extensionTerminalName, setCommandContext } from '../constants'; import { Container } from '../container'; import { toGitLensFSUri } from '../git/fsProvider'; -import { GitService, GitUri } from '../gitService'; +import { GitService, GitUri } from '../git/gitService'; import { Arrays } from '../system'; import { BranchNode, diff --git a/src/views/gitExplorer.ts b/src/views/gitExplorer.ts index 64c507c..9d4556c 100644 --- a/src/views/gitExplorer.ts +++ b/src/views/gitExplorer.ts @@ -22,7 +22,7 @@ import { } from '../configuration'; import { CommandContext, GlyphChars, setCommandContext, WorkspaceState } from '../constants'; import { Container } from '../container'; -import { GitUri } from '../gitService'; +import { GitUri } from '../git/gitService'; import { Logger } from '../logger'; import { Functions } from '../system'; import { RefreshNodeCommandArgs } from '../views/explorerCommands'; diff --git a/src/views/nodes/activeRepositoryNode.ts b/src/views/nodes/activeRepositoryNode.ts index 71b7ad2..cabdf57 100644 --- a/src/views/nodes/activeRepositoryNode.ts +++ b/src/views/nodes/activeRepositoryNode.ts @@ -2,7 +2,7 @@ import { TextEditor, TreeItem, TreeItemCollapsibleState, window } from 'vscode'; import { isTextEditor } from '../../constants'; import { Container } from '../../container'; -import { GitUri } from '../../gitService'; +import { GitUri } from '../../git/gitService'; import { Functions } from '../../system'; import { GitExplorer } from '../gitExplorer'; import { ExplorerNode } from './explorerNode'; diff --git a/src/views/nodes/branchNode.ts b/src/views/nodes/branchNode.ts index 134f54e..8fc0452 100644 --- a/src/views/nodes/branchNode.ts +++ b/src/views/nodes/branchNode.ts @@ -3,7 +3,7 @@ import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExplorerBranchesLayout } from '../../configuration'; import { GlyphChars } from '../../constants'; import { Container } from '../../container'; -import { GitBranch, GitUri } from '../../gitService'; +import { GitBranch, GitUri } from '../../git/gitService'; import { Arrays, Iterables } from '../../system'; import { GitExplorer } from '../gitExplorer'; import { CommitNode } from './commitNode'; diff --git a/src/views/nodes/branchOrTagFolderNode.ts b/src/views/nodes/branchOrTagFolderNode.ts index d8b169b..36e61c3 100644 --- a/src/views/nodes/branchOrTagFolderNode.ts +++ b/src/views/nodes/branchOrTagFolderNode.ts @@ -1,6 +1,6 @@ 'use strict'; import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { GitUri } from '../../gitService'; +import { GitUri } from '../../git/gitService'; import { Arrays, Objects } from '../../system'; import { BranchNode } from './branchNode'; // import { Container } from '../../container'; diff --git a/src/views/nodes/branchesNode.ts b/src/views/nodes/branchesNode.ts index caa8f31..43d1646 100644 --- a/src/views/nodes/branchesNode.ts +++ b/src/views/nodes/branchesNode.ts @@ -2,7 +2,7 @@ import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExplorerBranchesLayout } from '../../configuration'; import { Container } from '../../container'; -import { GitUri, Repository } from '../../gitService'; +import { GitUri, Repository } from '../../git/gitService'; import { Arrays, Iterables } from '../../system'; import { GitExplorer } from '../gitExplorer'; import { BranchNode } from './branchNode'; diff --git a/src/views/nodes/commitFileNode.ts b/src/views/nodes/commitFileNode.ts index f7f3802..f11d931 100644 --- a/src/views/nodes/commitFileNode.ts +++ b/src/views/nodes/commitFileNode.ts @@ -13,7 +13,7 @@ import { IGitStatusFile, IStatusFormatOptions, StatusFileFormatter -} from '../../gitService'; +} from '../../git/gitService'; import { Explorer, ExplorerNode, ExplorerRefNode, ResourceType } from './explorerNode'; export enum CommitFileNodeDisplayAs { diff --git a/src/views/nodes/commitNode.ts b/src/views/nodes/commitNode.ts index 10d18db..a8709a6 100644 --- a/src/views/nodes/commitNode.ts +++ b/src/views/nodes/commitNode.ts @@ -5,7 +5,7 @@ import { Commands, DiffWithPreviousCommandArgs } from '../../commands'; import { ExplorerFilesLayout } from '../../configuration'; import { GlyphChars } from '../../constants'; import { Container } from '../../container'; -import { CommitFormatter, GitBranch, GitLogCommit, ICommitFormatOptions } from '../../gitService'; +import { CommitFormatter, GitBranch, GitLogCommit, ICommitFormatOptions } from '../../git/gitService'; import { Arrays, Iterables, Strings } from '../../system'; import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode'; import { Explorer, ExplorerNode, ExplorerRefNode, ResourceType } from './explorerNode'; diff --git a/src/views/nodes/commitResultsNode.ts b/src/views/nodes/commitResultsNode.ts index ee507ac..6473397 100644 --- a/src/views/nodes/commitResultsNode.ts +++ b/src/views/nodes/commitResultsNode.ts @@ -1,6 +1,6 @@ 'use strict'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { GitLogCommit } from '../../gitService'; +import { GitLogCommit } from '../../git/gitService'; import { ResultsExplorer } from '../resultsExplorer'; import { CommitNode } from './commitNode'; import { ExplorerNode, ResourceType } from './explorerNode'; diff --git a/src/views/nodes/commitsNode.ts b/src/views/nodes/commitsNode.ts index 46e6a94..f58a952 100644 --- a/src/views/nodes/commitsNode.ts +++ b/src/views/nodes/commitsNode.ts @@ -1,6 +1,6 @@ 'use strict'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { GitLog, GitUri } from '../../gitService'; +import { GitLog, GitUri } from '../../git/gitService'; import { Iterables } from '../../system'; import { CommitNode } from './commitNode'; import { Explorer, ExplorerNode, ResourceType, ShowAllNode } from './explorerNode'; diff --git a/src/views/nodes/commitsResultsNode.ts b/src/views/nodes/commitsResultsNode.ts index ec0bcf9..be3ebde 100644 --- a/src/views/nodes/commitsResultsNode.ts +++ b/src/views/nodes/commitsResultsNode.ts @@ -1,6 +1,6 @@ 'use strict'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { GitLog, GitUri } from '../../gitService'; +import { GitLog, GitUri } from '../../git/gitService'; import { Iterables } from '../../system'; import { CommitNode } from './commitNode'; import { Explorer, ExplorerNode, ResourceType, ShowAllNode } from './explorerNode'; diff --git a/src/views/nodes/comparisonResultsNode.ts b/src/views/nodes/comparisonResultsNode.ts index 661a0c3..a2073f9 100644 --- a/src/views/nodes/comparisonResultsNode.ts +++ b/src/views/nodes/comparisonResultsNode.ts @@ -2,7 +2,7 @@ import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { GlyphChars } from '../../constants'; import { Container } from '../../container'; -import { GitLog, GitService, GitUri } from '../../gitService'; +import { GitLog, GitService, GitUri } from '../../git/gitService'; import { Strings } from '../../system'; import { CommitsResultsNode } from './commitsResultsNode'; import { Explorer, ExplorerNode, NamedRef, ResourceType } from './explorerNode'; diff --git a/src/views/nodes/explorerNode.ts b/src/views/nodes/explorerNode.ts index aff4944..25b4c9a 100644 --- a/src/views/nodes/explorerNode.ts +++ b/src/views/nodes/explorerNode.ts @@ -2,7 +2,7 @@ import { Command, Disposable, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; import { GlyphChars } from '../../constants'; import { Container } from '../../container'; -import { GitUri } from '../../gitService'; +import { GitUri } from '../../git/gitService'; import { RefreshNodeCommandArgs } from '../explorerCommands'; import { GitExplorer } from '../gitExplorer'; import { HistoryExplorer } from '../historyExplorer'; diff --git a/src/views/nodes/fileHistoryNode.ts b/src/views/nodes/fileHistoryNode.ts index cee6ac6..deb5338 100644 --- a/src/views/nodes/fileHistoryNode.ts +++ b/src/views/nodes/fileHistoryNode.ts @@ -10,7 +10,7 @@ import { RepositoryChange, RepositoryChangeEvent, RepositoryFileSystemChangeEvent -} from '../../gitService'; +} from '../../git/gitService'; import { Logger } from '../../logger'; import { Iterables } from '../../system'; import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode'; diff --git a/src/views/nodes/folderNode.ts b/src/views/nodes/folderNode.ts index 79e3236..a60bafb 100644 --- a/src/views/nodes/folderNode.ts +++ b/src/views/nodes/folderNode.ts @@ -1,7 +1,7 @@ 'use strict'; import { ThemeIcon, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExplorerFilesLayout, IExplorersFilesConfig } from '../../configuration'; -import { GitUri } from '../../gitService'; +import { GitUri } from '../../git/gitService'; import { Arrays, Objects } from '../../system'; import { Explorer, ExplorerNode, ResourceType } from './explorerNode'; diff --git a/src/views/nodes/historyNode.ts b/src/views/nodes/historyNode.ts index b33e192..dc8218f 100644 --- a/src/views/nodes/historyNode.ts +++ b/src/views/nodes/historyNode.ts @@ -1,7 +1,7 @@ 'use strict'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { Container } from '../../container'; -import { GitUri, Repository } from '../../gitService'; +import { GitUri, Repository } from '../../git/gitService'; import { Explorer, ExplorerNode, ResourceType } from './explorerNode'; import { FileHistoryNode } from './fileHistoryNode'; diff --git a/src/views/nodes/remoteNode.ts b/src/views/nodes/remoteNode.ts index fc37232..5e3b8ee 100644 --- a/src/views/nodes/remoteNode.ts +++ b/src/views/nodes/remoteNode.ts @@ -3,7 +3,7 @@ import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExplorerBranchesLayout } from '../../configuration'; import { GlyphChars } from '../../constants'; import { Container } from '../../container'; -import { GitRemote, GitRemoteType, GitUri, Repository } from '../../gitService'; +import { GitRemote, GitRemoteType, GitUri, Repository } from '../../git/gitService'; import { Arrays, Iterables } from '../../system'; import { GitExplorer } from '../gitExplorer'; import { BranchNode } from './branchNode'; diff --git a/src/views/nodes/remotesNode.ts b/src/views/nodes/remotesNode.ts index fc27945..b33ba59 100644 --- a/src/views/nodes/remotesNode.ts +++ b/src/views/nodes/remotesNode.ts @@ -1,7 +1,7 @@ 'use strict'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { Container } from '../../container'; -import { GitUri, Repository } from '../../gitService'; +import { GitUri, Repository } from '../../git/gitService'; import { Iterables } from '../../system'; import { GitExplorer } from '../gitExplorer'; import { ExplorerNode, MessageNode, ResourceType } from './explorerNode'; diff --git a/src/views/nodes/repositoriesNode.ts b/src/views/nodes/repositoriesNode.ts index 3ba4938..6731012 100644 --- a/src/views/nodes/repositoriesNode.ts +++ b/src/views/nodes/repositoriesNode.ts @@ -1,6 +1,6 @@ 'use strict'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; -import { GitUri, Repository } from '../../gitService'; +import { GitUri, Repository } from '../../git/gitService'; import { GitExplorer } from '../gitExplorer'; import { ActiveRepositoryNode } from './activeRepositoryNode'; import { ExplorerNode, ResourceType } from './explorerNode'; diff --git a/src/views/nodes/repositoryNode.ts b/src/views/nodes/repositoryNode.ts index 8b4ebaf..294e538 100644 --- a/src/views/nodes/repositoryNode.ts +++ b/src/views/nodes/repositoryNode.ts @@ -1,7 +1,7 @@ 'use strict'; import { Disposable, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { GlyphChars } from '../../constants'; -import { GitUri, Repository, RepositoryChange, RepositoryChangeEvent } from '../../gitService'; +import { GitUri, Repository, RepositoryChange, RepositoryChangeEvent } from '../../git/gitService'; import { Logger } from '../../logger'; import { Strings } from '../../system'; import { GitExplorer } from '../gitExplorer'; diff --git a/src/views/nodes/stashFileNode.ts b/src/views/nodes/stashFileNode.ts index 7365318..6d6d53e 100644 --- a/src/views/nodes/stashFileNode.ts +++ b/src/views/nodes/stashFileNode.ts @@ -1,5 +1,5 @@ 'use strict'; -import { GitLogCommit, IGitStatusFile } from '../../gitService'; +import { GitLogCommit, IGitStatusFile } from '../../git/gitService'; import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode'; import { Explorer, ResourceType } from './explorerNode'; diff --git a/src/views/nodes/stashNode.ts b/src/views/nodes/stashNode.ts index 376ef00..0e2a70d 100644 --- a/src/views/nodes/stashNode.ts +++ b/src/views/nodes/stashNode.ts @@ -1,7 +1,7 @@ 'use strict'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { Container } from '../../container'; -import { CommitFormatter, GitStashCommit, ICommitFormatOptions } from '../../gitService'; +import { CommitFormatter, GitStashCommit, ICommitFormatOptions } from '../../git/gitService'; import { Iterables } from '../../system'; import { Explorer, ExplorerNode, ExplorerRefNode, ResourceType } from './explorerNode'; import { StashFileNode } from './stashFileNode'; diff --git a/src/views/nodes/stashesNode.ts b/src/views/nodes/stashesNode.ts index 67b71fc..7dacc12 100644 --- a/src/views/nodes/stashesNode.ts +++ b/src/views/nodes/stashesNode.ts @@ -1,7 +1,7 @@ 'use strict'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { Container } from '../../container'; -import { GitUri, Repository } from '../../gitService'; +import { GitUri, Repository } from '../../git/gitService'; import { Iterables } from '../../system'; import { Explorer, ExplorerNode, MessageNode, ResourceType } from './explorerNode'; import { StashNode } from './stashNode'; diff --git a/src/views/nodes/statusFileCommitsNode.ts b/src/views/nodes/statusFileCommitsNode.ts index 1485b2b..69d5378 100644 --- a/src/views/nodes/statusFileCommitsNode.ts +++ b/src/views/nodes/statusFileCommitsNode.ts @@ -11,7 +11,7 @@ import { IGitStatusFileWithCommit, IStatusFormatOptions, StatusFileFormatter -} from '../../gitService'; +} from '../../git/gitService'; import { Strings } from '../../system'; import { CommitFileNode, CommitFileNodeDisplayAs } from './commitFileNode'; import { Explorer, ExplorerNode, ResourceType } from './explorerNode'; diff --git a/src/views/nodes/statusFileNode.ts b/src/views/nodes/statusFileNode.ts index 3a4b95a..8b3a977 100644 --- a/src/views/nodes/statusFileNode.ts +++ b/src/views/nodes/statusFileNode.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { Command, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { Commands, DiffWithCommandArgs } from '../../commands'; import { Container } from '../../container'; -import { getGitStatusIcon, GitStatusFile, GitUri, IStatusFormatOptions, StatusFileFormatter } from '../../gitService'; +import { getGitStatusIcon, GitStatusFile, GitUri, IStatusFormatOptions, StatusFileFormatter } from '../../git/gitService'; import { Explorer, ExplorerNode, ResourceType } from './explorerNode'; export class StatusFileNode extends ExplorerNode { diff --git a/src/views/nodes/statusFilesNode.ts b/src/views/nodes/statusFilesNode.ts index 348d6b3..1c159db 100644 --- a/src/views/nodes/statusFilesNode.ts +++ b/src/views/nodes/statusFilesNode.ts @@ -11,7 +11,7 @@ import { GitStatus, GitUri, IGitStatusFileWithCommit -} from '../../gitService'; +} from '../../git/gitService'; import { Arrays, Iterables, Objects, Strings } from '../../system'; import { GitExplorer } from '../gitExplorer'; import { ExplorerNode, ResourceType, ShowAllNode } from './explorerNode'; diff --git a/src/views/nodes/statusFilesResultsNode.ts b/src/views/nodes/statusFilesResultsNode.ts index 84be151..838876e 100644 --- a/src/views/nodes/statusFilesResultsNode.ts +++ b/src/views/nodes/statusFilesResultsNode.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExplorerFilesLayout } from '../../configuration'; import { Container } from '../../container'; -import { GitStatusFile, GitUri } from '../../gitService'; +import { GitStatusFile, GitUri } from '../../git/gitService'; import { Arrays, Iterables, Strings } from '../../system'; import { Explorer, ExplorerNode, ResourceType } from './explorerNode'; import { FolderNode, IFileExplorerNode } from './folderNode'; diff --git a/src/views/nodes/statusNode.ts b/src/views/nodes/statusNode.ts index 8cd5edc..befab08 100644 --- a/src/views/nodes/statusNode.ts +++ b/src/views/nodes/statusNode.ts @@ -1,7 +1,7 @@ import { Disposable, TreeItem, TreeItemCollapsibleState } from 'vscode'; import { GlyphChars } from '../../constants'; import { Container } from '../../container'; -import { GitBranch, GitUri, Repository, RepositoryFileSystemChangeEvent } from '../../gitService'; +import { GitBranch, GitUri, Repository, RepositoryFileSystemChangeEvent } from '../../git/gitService'; import { GitExplorer } from '../gitExplorer'; import { BranchNode } from './branchNode'; import { ExplorerNode, ResourceType } from './explorerNode'; diff --git a/src/views/nodes/statusUpstreamNode.ts b/src/views/nodes/statusUpstreamNode.ts index 544f0d6..ee6da13 100644 --- a/src/views/nodes/statusUpstreamNode.ts +++ b/src/views/nodes/statusUpstreamNode.ts @@ -1,7 +1,7 @@ 'use strict'; import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { Container } from '../../container'; -import { GitStatus, GitUri } from '../../gitService'; +import { GitStatus, GitUri } from '../../git/gitService'; import { Iterables, Strings } from '../../system'; import { CommitNode } from './commitNode'; import { Explorer, ExplorerNode, ResourceType } from './explorerNode'; diff --git a/src/views/nodes/tagNode.ts b/src/views/nodes/tagNode.ts index 2c7e69f..071722c 100644 --- a/src/views/nodes/tagNode.ts +++ b/src/views/nodes/tagNode.ts @@ -2,7 +2,7 @@ import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExplorerBranchesLayout } from '../../configuration'; import { Container } from '../../container'; -import { GitTag, GitUri } from '../../gitService'; +import { GitTag, GitUri } from '../../git/gitService'; import { Iterables } from '../../system'; import { GitExplorer } from '../gitExplorer'; import { CommitNode } from './commitNode'; diff --git a/src/views/nodes/tagsNode.ts b/src/views/nodes/tagsNode.ts index aafc849..deeb9df 100644 --- a/src/views/nodes/tagsNode.ts +++ b/src/views/nodes/tagsNode.ts @@ -2,7 +2,7 @@ import { TreeItem, TreeItemCollapsibleState } from 'vscode'; import { ExplorerBranchesLayout } from '../../configuration'; import { Container } from '../../container'; -import { GitUri, Repository } from '../../gitService'; +import { GitUri, Repository } from '../../git/gitService'; import { Arrays } from '../../system'; import { GitExplorer } from '../gitExplorer'; import { BranchOrTagFolderNode } from './branchOrTagFolderNode'; diff --git a/src/views/resultsExplorer.ts b/src/views/resultsExplorer.ts index 315305f..923b48b 100644 --- a/src/views/resultsExplorer.ts +++ b/src/views/resultsExplorer.ts @@ -13,7 +13,7 @@ import { import { configuration, ExplorerFilesLayout, IExplorersConfig, IResultsExplorerConfig } from '../configuration'; import { CommandContext, GlyphChars, setCommandContext, WorkspaceState } from '../constants'; import { Container } from '../container'; -import { GitLog, GitLogCommit } from '../gitService'; +import { GitLog, GitLogCommit } from '../git/gitService'; import { Logger } from '../logger'; import { Functions, Strings } from '../system'; import { RefreshNodeCommandArgs } from './explorerCommands';