- 'use strict';
- import { Functions, Iterables, Objects, Strings, TernarySearchTree } from './system';
- import { ConfigurationChangeEvent, Disposable, Event, EventEmitter, Range, TextDocument, TextDocumentChangeEvent, TextEditor, Uri, window, WindowState, workspace, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode';
- import { configuration, IConfig, IRemotesConfig } from './configuration';
- import { CommandContext, DocumentSchemes, setCommandContext } from './constants';
- import { RemoteProviderFactory, RemoteProviderMap } from './git/remotes/factory';
- import { CommitFormatting, Git, GitAuthor, GitBlame, GitBlameCommit, GitBlameLine, GitBlameLines, GitBlameParser, GitBranch, GitBranchParser, GitCommit, GitCommitType, GitDiff, GitDiffChunkLine, GitDiffParser, GitDiffShortStat, GitLog, GitLogCommit, GitLogParser, GitRemote, GitRemoteParser, GitStash, GitStashParser, GitStatus, GitStatusFile, GitStatusParser, GitTag, GitTagParser, IGit, Repository } from './git/git';
- import { GitUri, IGitCommitInfo } from './git/gitUri';
- import { Logger } from './logger';
- import * as fs from 'fs';
- import * as path from 'path';
-
- export { GitUri, IGit, IGitCommitInfo };
- export * from './git/models/models';
- export * from './git/formatters/commit';
- export * from './git/formatters/status';
- export { getNameFromRemoteResource, RemoteProvider, RemoteResource, RemoteResourceType } from './git/remotes/provider';
- export { RemoteProviderFactory } from './git/remotes/factory';
- export * from './git/gitContextTracker';
-
- class UriCacheEntry {
-
- constructor(
- public readonly uri: GitUri
- ) { }
- }
-
- class GitCacheEntry {
-
- private cache: Map<string, CachedBlame | CachedDiff | CachedLog> = new Map();
-
- constructor(
- public readonly key: string
- ) { }
-
- get hasErrors(): boolean {
- return Iterables.every(this.cache.values(), entry => entry.errorMessage !== undefined);
- }
-
- get<T extends CachedBlame | CachedDiff | CachedLog>(key: string): T | undefined {
- return this.cache.get(key) as T;
- }
-
- set<T extends CachedBlame | CachedDiff | CachedLog>(key: string, value: T) {
- this.cache.set(key, value);
- }
- }
-
- interface CachedItem<T> {
- item: Promise<T>;
- errorMessage?: string;
- }
-
- interface CachedBlame extends CachedItem<GitBlame> { }
- interface CachedDiff extends CachedItem<GitDiff> { }
- interface CachedLog extends CachedItem<GitLog> { }
-
- enum RemoveCacheReason {
- DocumentChanged,
- DocumentClosed
- }
-
- export enum GitRepoSearchBy {
- Author = 'author',
- ChangedOccurrences = 'changed-occurrences',
- Changes = 'changes',
- Files = 'files',
- Message = 'message',
- Sha = 'sha'
- }
-
- export enum GitChangeReason {
- GitCache = 'git-cache',
- Repositories = 'repositories'
- }
-
- export interface GitChangeEvent {
- reason: GitChangeReason;
- }
-
- export class GitService extends Disposable {
-
- static emptyPromise: Promise<GitBlame | GitDiff | GitLog | undefined> = Promise.resolve(undefined);
- static deletedSha = 'ffffffffffffffffffffffffffffffffffffffff';
- static stagedUncommittedSha = Git.stagedUncommittedSha;
- static uncommittedSha = Git.uncommittedSha;
-
- config: IConfig;
-
- private _onDidBlameFail = new EventEmitter<string>();
- get onDidBlameFail(): Event<string> {
- return this._onDidBlameFail.event;
- }
-
- private _onDidChange = new EventEmitter<GitChangeEvent>();
- get onDidChange(): Event<GitChangeEvent> {
- return this._onDidChange.event;
- }
-
- private _cacheDisposable: Disposable | undefined;
- private _disposable: Disposable | undefined;
- private _documentKeyMap: Map<TextDocument, string>;
- private _gitCache: Map<string, GitCacheEntry>;
- private _repositoryTree: TernarySearchTree<Repository>;
- private _repositoriesLoadingPromise: Promise<void> | undefined;
- private _suspended: boolean = false;
- private _trackedCache: Map<string, boolean | Promise<boolean>>;
- private _versionedUriCache: Map<string, UriCacheEntry>;
-
- constructor() {
- super(() => this.dispose());
-
- this._documentKeyMap = new Map();
- this._gitCache = new Map();
- 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._disposable && this._disposable.dispose();
-
- this._cacheDisposable && this._cacheDisposable.dispose();
- this._cacheDisposable = undefined;
-
- this._documentKeyMap.clear();
- this._gitCache.clear();
- this._trackedCache.clear();
- this._versionedUriCache.clear();
- }
-
- get UseCaching() {
- return this.config.advanced.caching.enabled;
- }
-
- private onAnyRepositoryChanged() {
- this._gitCache.clear();
- this._trackedCache.clear();
- }
-
- private onConfigurationChanged(e: ConfigurationChangeEvent) {
- const initializing = configuration.initializing(e);
-
- const cfg = configuration.get<IConfig>();
-
- if (initializing || configuration.changed(e, configuration.name('keymap').value)) {
- setCommandContext(CommandContext.KeyMap, cfg.keymap);
- }
-
- if (initializing || configuration.changed(e, configuration.name('advanced')('caching')('enabled').value)) {
- if (cfg.advanced.caching.enabled) {
- this._cacheDisposable && this._cacheDisposable.dispose();
-
- this._cacheDisposable = Disposable.from(
- workspace.onDidChangeTextDocument(Functions.debounce(this.onTextDocumentChanged, 50), this),
- workspace.onDidCloseTextDocument(this.onTextDocumentClosed, this)
- );
- }
- else {
- this._cacheDisposable && this._cacheDisposable.dispose();
- this._cacheDisposable = undefined;
-
- this._documentKeyMap.clear();
- this._gitCache.clear();
- }
- }
-
- if (initializing || configuration.changed(e, configuration.name('defaultDateStyle').value) ||
- configuration.changed(e, configuration.name('defaultDateFormat').value)) {
- CommitFormatting.reset();
- }
-
- this.config = cfg;
-
- // Only count the change if we aren't initializing
- if (!initializing && configuration.changed(e, configuration.name('blame')('ignoreWhitespace').value, null)) {
- this._gitCache.clear();
- this.fireChange(GitChangeReason.GitCache);
- }
- }
-
- private onTextDocumentChanged(e: TextDocumentChangeEvent) {
- let key = this._documentKeyMap.get(e.document);
- if (key === undefined) {
- key = this.getCacheEntryKey(e.document.uri);
- this._documentKeyMap.set(e.document, key);
- }
-
- // Don't remove broken blame on change (since otherwise we'll have to run the broken blame again)
- const entry = this._gitCache.get(key);
- if (entry === undefined || entry.hasErrors) return;
-
- if (this._gitCache.delete(key)) {
- Logger.log(`Clear cache entry for '${key}', reason=${RemoveCacheReason[RemoveCacheReason.DocumentChanged]}`);
- }
- }
-
- private onTextDocumentClosed(document: TextDocument) {
- this._documentKeyMap.delete(document);
-
- const key = this.getCacheEntryKey(document.uri);
- if (this._gitCache.delete(key)) {
- Logger.log(`Clear cache entry for '${key}', reason=${RemoveCacheReason[RemoveCacheReason.DocumentClosed]}`);
- }
- }
-
- 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;
- }
-
- 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 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 setCommandContext(CommandContext.HasRepository, this._repositoryTree.any());
-
- if (!initializing) {
- // Defer the event trigger enough to let everything unwind
- setImmediate(() => this.fireChange(GitChangeReason.Repositories));
- }
- }
-
- private async repositorySearch(folder: WorkspaceFolder): Promise<Repository[]> {
- const folderUri = folder.uri;
-
- const repositories: Repository[] = [];
- const anyRepoChangedFn = this.onAnyRepositoryChanged.bind(this);
-
- const rootPath = await this.getRepoPathCore(folderUri.fsPath, true);
- if (rootPath !== undefined) {
- repositories.push(new Repository(folder, rootPath, true, this, anyRepoChangedFn, this._suspended));
- }
-
- const depth = configuration.get<number>(configuration.name('advanced')('repositorySearchDepth').value, folderUri);
- if (depth <= 0) 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);
-
- const start = process.hrtime();
-
- const paths = await this.repositorySearchCore(folderUri.fsPath, depth, excludes);
-
- const duration = process.hrtime(start);
- Logger.log(`Searching (depth=${depth}) for repositories in ${folderUri.fsPath} took ${(duration[0] * 1000) + Math.floor(duration[1] / 1000000)} ms`);
-
- for (let p of paths) {
- p = path.dirname(p);
- // If we are the same as the root, skip it
- if (Git.normalizePath(p) === rootPath) continue;
-
- const rp = await this.getRepoPathCore(p, true);
- if (rp === undefined) continue;
-
- repositories.push(new Repository(folder, rp, false, this, anyRepoChangedFn, this._suspended));
- }
-
- // const uris = await workspace.findFiles(new RelativePattern(folder, '**/.git/HEAD'));
- // for (const uri of uris) {
- // const rp = await this.getRepoPathCore(path.resolve(path.dirname(uri.fsPath), '../'), true);
- // if (rp !== undefined && rp !== rootPath) {
- // repositories.push(new Repository(folder, rp, false, this, anyRepoChangedFn, this._suspended));
- // }
- // }
-
- return repositories;
- }
-
- private async repositorySearchCore(root: string, depth: number, excludes: { [key: string]: boolean }, repositories: string[] = []): Promise<string[]> {
- return new Promise<string[]>((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<void>((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 fireChange(reason: GitChangeReason) {
- this._onDidChange.fire({ reason: reason });
- }
-
- checkoutFile(uri: GitUri, sha?: string) {
- sha = sha || uri.sha;
- Logger.log(`checkoutFile('${uri.repoPath}', '${uri.fsPath}', '${sha}')`);
-
- return Git.checkout(uri.repoPath!, uri.fsPath, sha!);
- }
-
- private async fileExists(repoPath: string, fileName: string): Promise<boolean> {
- return await new Promise<boolean>((resolve, reject) => fs.exists(path.resolve(repoPath, fileName), resolve));
- }
-
- async findNextCommit(repoPath: string, fileName: string, sha?: string): Promise<GitLogCommit | undefined> {
- let log = await this.getLogForFile(repoPath, fileName, { maxCount: 1, ref: sha, reverse: true });
- let commit = log && Iterables.first(log.commits.values());
- if (commit) return commit;
-
- const nextFileName = await this.findNextFileName(repoPath, fileName, sha);
- if (nextFileName) {
- log = await this.getLogForFile(repoPath, nextFileName, { maxCount: 1, ref: sha, reverse: true });
- commit = log && Iterables.first(log.commits.values());
- }
-
- return commit;
- }
-
- async findNextFileName(repoPath: string | undefined, fileName: string, sha?: string): Promise<string | undefined> {
- [fileName, repoPath] = Git.splitPath(fileName, repoPath);
-
- return (await this.fileExists(repoPath, fileName))
- ? fileName
- : await this.findNextFileNameCore(repoPath, fileName, sha);
- }
-
- private async findNextFileNameCore(repoPath: string, fileName: string, sha?: string): Promise<string | undefined> {
- if (sha === undefined) {
- // Get the most recent commit for this file name
- const c = await this.getLogCommit(repoPath, fileName);
- if (c === undefined) return undefined;
-
- sha = c.sha;
- }
-
- // Get the full commit (so we can see if there are any matching renames in the file statuses)
- const log = await this.getLogForRepo(repoPath, { maxCount: 1, ref: sha });
- 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>;
- async findWorkingFileName(repoPath: string | undefined, fileName: string): Promise<string | undefined>;
- async findWorkingFileName(commitOrRepoPath: GitCommit | string | undefined, fileName?: string): Promise<string | undefined> {
- let repoPath: string | undefined;
- if (commitOrRepoPath === undefined || typeof commitOrRepoPath === 'string') {
- repoPath = commitOrRepoPath;
- if (fileName === undefined) throw new Error('Invalid fileName');
-
- [fileName] = Git.splitPath(fileName, repoPath);
- }
- else {
- const c = commitOrRepoPath;
- repoPath = c.repoPath;
- if (c.workingFileName && await this.fileExists(repoPath, c.workingFileName)) return c.workingFileName;
- fileName = c.fileName;
- }
-
- while (true) {
- if (await this.fileExists(repoPath!, fileName)) return fileName;
-
- fileName = await this.findNextFileNameCore(repoPath!, fileName);
- if (fileName === undefined) return undefined;
- }
- }
-
- async getActiveRepoPath(editor?: TextEditor): Promise<string | undefined> {
- if (editor === undefined) {
- const repoPath = this.getHighlanderRepoPath();
- if (repoPath !== undefined) return repoPath;
- }
-
- editor = editor || window.activeTextEditor;
- if (editor === undefined) return undefined;
-
- return this.getRepoPath(editor.document.uri);
- }
-
- getHighlanderRepoPath(): string | undefined {
- const entry = this._repositoryTree.highlander();
- if (entry === undefined) return undefined;
-
- const [repo] = entry;
- return repo.path;
- }
-
- public async getBlameability(uri: GitUri): Promise<boolean> {
- if (!this.UseCaching) return await this.isTracked(uri);
-
- const cacheKey = this.getCacheEntryKey(uri);
- const entry = this._gitCache.get(cacheKey);
- if (entry === undefined) return await this.isTracked(uri);
-
- return !entry.hasErrors;
- }
-
- async getBlameForFile(uri: GitUri): Promise<GitBlame | undefined> {
- let key = 'blame';
- if (uri.sha !== undefined) {
- key += `:${uri.sha}`;
- }
-
- let entry: GitCacheEntry | undefined;
- if (this.UseCaching) {
- const cacheKey = this.getCacheEntryKey(uri);
- entry = this._gitCache.get(cacheKey);
-
- if (entry !== undefined) {
- const cachedBlame = entry.get<CachedBlame>(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 (entry === undefined) {
- entry = new GitCacheEntry(cacheKey);
- this._gitCache.set(entry.key, entry);
- }
- }
- else {
- Logger.log(`getBlameForFile('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`);
- }
-
- const promise = this.getBlameForFileCore(uri, entry, key);
-
- if (entry) {
- Logger.log(`Add blame cache for '${entry.key}:${key}'`);
-
- entry.set<CachedBlame>(key, {
- item: promise
- } as CachedBlame);
- }
-
- return promise;
- }
-
- private async getBlameForFileCore(uri: GitUri, entry: GitCacheEntry | undefined, key: string): Promise<GitBlame | undefined> {
- if (!(await this.isTracked(uri))) {
- Logger.log(`Skipping blame; '${uri.fsPath}' is not tracked`);
- if (entry && entry.key) {
- this._onDidBlameFail.fire(entry.key);
- }
- return GitService.emptyPromise as Promise<GitBlame>;
- }
-
- const [file, root] = Git.splitPath(uri.fsPath, uri.repoPath, false);
-
- try {
- const data = await Git.blame(root, file, uri.sha, { ignoreWhitespace: this.config.blame.ignoreWhitespace });
- const blame = GitBlameParser.parse(data, root, file);
- return blame;
- }
- catch (ex) {
- // Trap and cache expected blame errors
- if (entry) {
- const msg = ex && ex.toString();
- Logger.log(`Replace blame cache with empty promise for '${entry.key}:${key}'`);
-
- entry.set<CachedBlame>(key, {
- item: GitService.emptyPromise,
- errorMessage: msg
- } as CachedBlame);
-
- this._onDidBlameFail.fire(entry.key);
- return GitService.emptyPromise as Promise<GitBlame>;
- }
-
- return undefined;
- }
- }
-
- async getBlameForFileContents(uri: GitUri, contents: string): Promise<GitBlame | undefined> {
- const key = `blame:${Strings.sha1(contents)}`;
-
- let entry: GitCacheEntry | undefined;
- if (this.UseCaching) {
- const cacheKey = this.getCacheEntryKey(uri);
- entry = this._gitCache.get(cacheKey);
-
- if (entry !== undefined) {
- const cachedBlame = entry.get<CachedBlame>(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 (entry === undefined) {
- entry = new GitCacheEntry(cacheKey);
- this._gitCache.set(entry.key, entry);
- }
- }
- else {
- Logger.log(`getBlameForFileContents('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`);
- }
-
- const promise = this.getBlameForFileContentsCore(uri, contents, entry, key);
-
- if (entry) {
- Logger.log(`Add blame cache for '${entry.key}:${key}'`);
-
- entry.set<CachedBlame>(key, {
- item: promise
- } as CachedBlame);
- }
-
- return promise;
- }
-
- async getBlameForFileContentsCore(uri: GitUri, contents: string, entry: GitCacheEntry | undefined, key: string): Promise<GitBlame | undefined> {
- if (!(await this.isTracked(uri))) {
- Logger.log(`Skipping blame; '${uri.fsPath}' is not tracked`);
- if (entry && entry.key) {
- this._onDidBlameFail.fire(entry.key);
- }
- return GitService.emptyPromise as Promise<GitBlame>;
- }
-
- const [file, root] = Git.splitPath(uri.fsPath, uri.repoPath, false);
-
- try {
- const data = await Git.blame_contents(root, file, contents, { ignoreWhitespace: this.config.blame.ignoreWhitespace });
- const blame = GitBlameParser.parse(data, root, file);
- return blame;
- }
- catch (ex) {
- // Trap and cache expected blame errors
- if (entry) {
- const msg = ex && ex.toString();
- Logger.log(`Replace blame cache with empty promise for '${entry.key}:${key}'`);
-
- entry.set<CachedBlame>(key, {
- item: GitService.emptyPromise,
- errorMessage: msg
- } as CachedBlame);
-
- this._onDidBlameFail.fire(entry.key);
- return GitService.emptyPromise as Promise<GitBlame>;
- }
-
- return undefined;
- }
- }
-
- async getBlameForLine(uri: GitUri, line: number): Promise<GitBlameLine | undefined> {
- Logger.log(`getBlameForLine('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}', ${line})`);
-
- if (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, { ignoreWhitespace: this.config.blame.ignoreWhitespace, startLine: lineToBlame, endLine: lineToBlame });
- const blame = GitBlameParser.parse(data, uri.repoPath, fileName);
- 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<GitBlameLine | undefined> {
- 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, { ignoreWhitespace: this.config.blame.ignoreWhitespace, startLine: lineToBlame, endLine: lineToBlame });
- const blame = GitBlameParser.parse(data, uri.repoPath, fileName);
- 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<GitBlameLines | undefined> {
- 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<string, GitAuthor> = new Map();
- const commits: Map<string, GitBlameCommit> = 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<GitBranch | undefined> {
- if (repoPath === undefined) return undefined;
-
- Logger.log(`getBranch('${repoPath}')`);
-
- const data = await Git.revparse_currentBranch(repoPath);
- if (data === undefined) return undefined;
-
- const branch = data.split('\n');
- return new GitBranch(repoPath, branch[0], true, branch[1]);
- }
-
- async getBranches(repoPath: string | undefined): Promise<GitBranch[]> {
- 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) || [];
- }
-
- getCacheEntryKey(fileName: string): string;
- getCacheEntryKey(uri: Uri): string;
- getCacheEntryKey(fileNameOrUri: string | Uri): string {
- return Git.normalizePath(typeof fileNameOrUri === 'string' ? fileNameOrUri : fileNameOrUri.fsPath).toLowerCase();
- }
-
- async getChangedFilesCount(repoPath: string, sha?: string): Promise<GitDiffShortStat | undefined> {
- Logger.log(`getChangedFilesCount('${repoPath}', '${sha}')`);
-
- const data = await Git.diff_shortstat(repoPath, sha);
- return GitDiffParser.parseShortStat(data);
- }
-
- async getConfig(key: string, repoPath?: string): Promise<string | undefined> {
- Logger.log(`getConfig('${key}', '${repoPath}')`);
-
- return await Git.config_get(key, repoPath);
- }
-
- getGitUriForVersionedFile(uri: Uri) {
- const cacheKey = this.getCacheEntryKey(uri);
- const entry = this._versionedUriCache.get(cacheKey);
- return entry && entry.uri;
- }
-
- async getDiffForFile(uri: GitUri, sha1?: string, sha2?: string): Promise<GitDiff | undefined> {
- if (sha1 !== undefined && sha2 === undefined && uri.sha !== undefined) {
- sha2 = uri.sha;
- }
-
- let key = 'diff';
- if (sha1 !== undefined) {
- key += `:${sha1}`;
- }
- if (sha2 !== undefined) {
- key += `:${sha2}`;
- }
-
- let entry: GitCacheEntry | undefined;
- if (this.UseCaching) {
- const cacheKey = this.getCacheEntryKey(uri);
- entry = this._gitCache.get(cacheKey);
-
- if (entry !== undefined) {
- const cachedDiff = entry.get<CachedDiff>(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 (entry === undefined) {
- entry = new GitCacheEntry(cacheKey);
- this._gitCache.set(entry.key, entry);
- }
- }
- else {
- Logger.log(`getDiffForFile('${uri.repoPath}', '${uri.fsPath}', '${sha1}', '${sha2}')`);
- }
-
- const promise = this.getDiffForFileCore(uri.repoPath, uri.fsPath, sha1, sha2, { encoding: GitService.getEncoding(uri) }, entry, key);
-
- if (entry) {
- Logger.log(`Add log cache for '${entry.key}:${key}'`);
-
- entry.set<CachedDiff>(key, {
- item: promise
- } as CachedDiff);
- }
-
- return promise;
- }
-
- private async getDiffForFileCore(repoPath: string | undefined, fileName: string, sha1: string | undefined, sha2: string | undefined, options: { encoding?: string }, entry: GitCacheEntry | undefined, key: string): Promise<GitDiff | undefined> {
- 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 (entry) {
- const msg = ex && ex.toString();
- Logger.log(`Replace diff cache with empty promise for '${entry.key}:${key}'`);
-
- entry.set<CachedDiff>(key, {
- item: GitService.emptyPromise,
- errorMessage: msg
- } as CachedDiff);
-
- return GitService.emptyPromise as Promise<GitDiff>;
- }
-
- return undefined;
- }
- }
-
- async getDiffForLine(uri: GitUri, line: number, sha1?: string, sha2?: string): Promise<GitDiffChunkLine | undefined> {
- 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<GitStatusFile[] | undefined> {
- 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 getLogCommit(repoPath: string | undefined, fileName: string, options?: { firstIfMissing?: boolean, previous?: boolean }): Promise<GitLogCommit | undefined>;
- async getLogCommit(repoPath: string | undefined, fileName: string, sha: string | undefined, options?: { firstIfMissing?: boolean, previous?: boolean }): Promise<GitLogCommit | undefined>;
- async getLogCommit(repoPath: string | undefined, fileName: string, shaOrOptions?: string | undefined | { firstIfMissing?: boolean, previous?: boolean }, options?: { firstIfMissing?: boolean, previous?: boolean }): Promise<GitLogCommit | undefined> {
- let sha: string | undefined = undefined;
- if (typeof shaOrOptions === 'string') {
- sha = shaOrOptions;
- }
- else if (options === undefined) {
- options = shaOrOptions;
- }
-
- options = options || {};
-
- Logger.log(`getLogCommit('${repoPath}', '${fileName}', '${sha}', ${options.firstIfMissing}, ${options.previous})`);
-
- const log = await this.getLogForFile(repoPath, fileName, { maxCount: options.previous ? 2 : 1, ref: sha });
- if (log === undefined) return undefined;
-
- const commit = sha && log.commits.get(sha);
- if (commit === undefined && sha && !options.firstIfMissing) {
- // If the sha isn't resolved we will never find it, so don't kick out
- if (!Git.isResolveRequired(sha)) return undefined;
- }
-
- return commit || Iterables.first(log.commits.values());
- }
-
- async getLogForRepo(repoPath: string, options: { maxCount?: number, ref?: string, reverse?: boolean } = {}): Promise<GitLog | undefined> {
- options = { reverse: false, ...options };
-
- Logger.log(`getLogForRepo('${repoPath}', '${options.ref}', ${options.maxCount}, ${options.reverse})`);
-
- const maxCount = options.maxCount == null
- ? this.config.advanced.maxQuickHistory || 0
- : options.maxCount;
-
- try {
- const data = await Git.log(repoPath, { maxCount: maxCount, ref: options.ref, reverse: options.reverse });
- const log = GitLogParser.parse(data, GitCommitType.Branch, repoPath, undefined, options.ref, maxCount, options.reverse!, undefined);
-
- if (log !== undefined) {
- const opts = { ...options };
- log.query = (maxCount: number | undefined) => this.getLogForRepo(repoPath, { ...opts, maxCount: maxCount });
- }
-
- return log;
- }
- catch (ex) {
- return undefined;
- }
- }
-
- async getLogForRepoSearch(repoPath: string, search: string, searchBy: GitRepoSearchBy, options: { maxCount?: number } = {}): Promise<GitLog | undefined> {
- Logger.log(`getLogForRepoSearch('${repoPath}', '${search}', '${searchBy}', ${options.maxCount})`);
-
- let maxCount = options.maxCount == null
- ? this.config.advanced.maxQuickHistory || 0
- : options.maxCount;
-
- let searchArgs: string[] | undefined = undefined;
- switch (searchBy) {
- case GitRepoSearchBy.Author:
- searchArgs = [`--author=${search}`];
- break;
- case GitRepoSearchBy.ChangedOccurrences:
- searchArgs = [`-S${search}`, '--pickaxe-regex'];
- break;
- case GitRepoSearchBy.Changes:
- searchArgs = [`-G${search}`];
- break;
- case GitRepoSearchBy.Files:
- searchArgs = [`--`, `${search}`];
- break;
- case GitRepoSearchBy.Message:
- searchArgs = [`--grep=${search}`];
- break;
- case GitRepoSearchBy.Sha:
- searchArgs = [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, maxCount, false, undefined);
-
- if (log !== undefined) {
- const opts = { ...options };
- log.query = (maxCount: number | undefined) => this.getLogForRepoSearch(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, reverse?: boolean, skipMerges?: boolean } = {}): Promise<GitLog | undefined> {
- options = { reverse: false, skipMerges: false, ...options };
-
- let key = 'log';
- if (options.ref !== undefined) {
- key += `:${options.ref}`;
- }
- if (options.maxCount !== undefined) {
- key += `:n${options.maxCount}`;
- }
-
- let entry: GitCacheEntry | undefined;
- if (this.UseCaching && options.range === undefined && !options.reverse) {
- const cacheKey = this.getCacheEntryKey(fileName);
- entry = this._gitCache.get(cacheKey);
-
- if (entry !== undefined) {
- const cachedLog = entry.get<CachedLog>(key);
- if (cachedLog !== undefined) {
- Logger.log(`getLogForFile[Cached(${key})]('${repoPath}', '${fileName}', '${options.ref}', ${options.maxCount}, undefined, ${options.reverse}, ${options.skipMerges})`);
- return cachedLog.item;
- }
-
- if (key !== 'log') {
- // Since we are looking for partial log, see if we have the log of the whole file
- const cachedLog = entry.get<CachedLog>('log');
- if (cachedLog !== undefined) {
- if (options.ref === undefined) {
- Logger.log(`getLogForFile[Cached(~${key})]('${repoPath}', '${fileName}', '', ${options.maxCount}, undefined, ${options.reverse}, ${options.skipMerges})`);
- return cachedLog.item;
- }
-
- Logger.log(`getLogForFile[? Cache(${key})]('${repoPath}', '${fileName}', '${options.ref}', ${options.maxCount}, undefined, ${options.reverse}, ${options.skipMerges})`);
- 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.reverse}, ${options.skipMerges})`);
- return cachedLog.item;
- }
- }
- }
- }
-
- Logger.log(`getLogForFile[Not Cached(${key})]('${repoPath}', '${fileName}', ${options.ref}, ${options.maxCount}, undefined, ${options.reverse}, ${options.skipMerges})`);
-
- if (entry === undefined) {
- entry = new GitCacheEntry(cacheKey);
- this._gitCache.set(entry.key, entry);
- }
- }
- else {
- Logger.log(`getLogForFile('${repoPath}', '${fileName}', ${options.ref}, ${options.maxCount}, ${options.range && `[${options.range.start.line}, ${options.range.end.line}]`}, ${options.reverse}, ${options.skipMerges})`);
- }
-
- const promise = this.getLogForFileCore(repoPath, fileName, options, entry, key);
-
- if (entry) {
- Logger.log(`Add log cache for '${entry.key}:${key}'`);
-
- entry.set<CachedLog>(key, {
- item: promise
- } as CachedLog);
- }
-
- return promise;
- }
-
- private async getLogForFileCore(repoPath: string | undefined, fileName: string, options: { maxCount?: number, range?: Range, ref?: string, reverse?: boolean, skipMerges?: boolean }, entry: GitCacheEntry | undefined, key: string): Promise<GitLog | undefined> {
- if (!(await this.isTracked(fileName, repoPath, options.ref))) {
- Logger.log(`Skipping log; '${fileName}' is not tracked`);
- return GitService.emptyPromise as Promise<GitLog>;
- }
-
- const [file, root] = Git.splitPath(fileName, repoPath, false);
-
- try {
- const { range, ...opts } = options;
-
- const maxCount = options.maxCount == null
- ? this.config.advanced.maxQuickHistory || 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, 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 (entry) {
- const msg = ex && ex.toString();
- Logger.log(`Replace log cache with empty promise for '${entry.key}:${key}'`);
-
- entry.set<CachedLog>(key, {
- item: GitService.emptyPromise,
- errorMessage: msg
- } as CachedLog);
-
- return GitService.emptyPromise as Promise<GitLog>;
- }
-
- return undefined;
- }
- }
-
- async hasRemote(repoPath: string | undefined): Promise<boolean> {
- if (repoPath === undefined) return false;
-
- const repository = await this.getRepository(repoPath);
- if (repository === undefined) return false;
-
- return repository.hasRemote();
- }
-
- async hasRemotes(repoPath: string | undefined): Promise<boolean> {
- if (repoPath === undefined) return false;
-
- const repository = await this.getRepository(repoPath);
- if (repository === undefined) return false;
-
- return repository.hasRemotes();
- }
-
- 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): Promise<GitRemote[]> {
- if (repoPath === undefined) return [];
-
- Logger.log(`getRemotes('${repoPath}')`);
-
- const repository = await this.getRepository(repoPath);
- if (repository !== undefined) return repository.getRemotes();
-
- return this.getRemotesCore(repoPath);
- }
-
- async getRemotesCore(repoPath: string | undefined, providerMap?: RemoteProviderMap): Promise<GitRemote[]> {
- if (repoPath === undefined) return [];
-
- Logger.log(`getRemotesCore('${repoPath}')`);
-
- providerMap = providerMap || RemoteProviderFactory.createMap(configuration.get<IRemotesConfig[] | null | undefined>(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): Promise<string | undefined>;
- async getRepoPath(uri: Uri | undefined): Promise<string | undefined>;
- async getRepoPath(filePathOrUri: string | Uri | undefined): Promise<string | undefined> {
- if (filePathOrUri === undefined) return await this.getActiveRepoPath();
- if (filePathOrUri instanceof GitUri) return filePathOrUri.repoPath;
-
- const repo = await this.getRepository(filePathOrUri);
- if (repo !== undefined) return repo.path;
-
- 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);
- const folder = root === undefined
- ? workspace.getWorkspaceFolder(Uri.file(rp))
- : root.folder;
-
- if (folder !== undefined) {
- const repo = new Repository(folder, rp, false, this, this.onAnyRepositoryChanged.bind(this), this._suspended);
- this._repositoryTree.set(rp, repo);
-
- // Send a notification that the repositories changed
- setImmediate(async () => {
- await setCommandContext(CommandContext.HasRepository, this._repositoryTree.any());
-
- this.fireChange(GitChangeReason.Repositories);
- });
- }
-
- return rp;
- }
-
- private async getRepoPathCore(filePath: string, isDirectory: boolean): Promise<string | undefined> {
- try {
- return await Git.revparse_toplevel(isDirectory ? filePath : path.dirname(filePath));
- }
- catch (ex) {
- Logger.error(ex, 'GitService.getRepoPathCore');
- return undefined;
- }
- }
-
- async getRepositories(): Promise<Iterable<Repository>> {
- const repositoryTree = await this.getRepositoryTree();
- return repositoryTree.values();
- }
-
- private async getRepositoryTree(): Promise<TernarySearchTree<Repository>> {
- if (this._repositoriesLoadingPromise !== undefined) {
- await this._repositoriesLoadingPromise;
- this._repositoriesLoadingPromise = undefined;
- }
-
- return this._repositoryTree;
- }
-
- async getRepository(repoPath: string): Promise<Repository | undefined>;
- async getRepository(uri: Uri): Promise<Repository | undefined>;
- async getRepository(repoPathOrUri: string | Uri): Promise<Repository | undefined>;
- async getRepository(repoPathOrUri: string | Uri): Promise<Repository | undefined> {
- 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 that repo, before returning
- if (!await this.isTrackedCore(repo.path, path)) return undefined;
- return repo;
- }
-
- async getStashList(repoPath: string | undefined): Promise<GitStash | undefined> {
- 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<GitStatusFile | undefined> {
- 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<GitStatus | undefined> {
- 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<GitTag[]> {
- if (repoPath === undefined) return [];
-
- Logger.log(`getTags('${repoPath}')`);
-
- const data = await Git.tag(repoPath);
- return GitTagParser.parse(data, repoPath) || [];
- }
-
- async getVersionedFile(repoPath: string | undefined, fileName: string, sha: string | undefined) {
- Logger.log(`getVersionedFile('${repoPath}', '${fileName}', '${sha}')`);
-
- if (!sha || (Git.isUncommitted(sha) && !Git.isStagedUncommitted(sha))) {
- if (await this.fileExists(repoPath!, fileName)) return fileName;
-
- return undefined;
- }
-
- const file = await Git.getVersionedFile(repoPath, fileName, sha);
- if (file === undefined) return undefined;
-
- const cacheKey = this.getCacheEntryKey(file);
- const entry = new UriCacheEntry(new GitUri(Uri.file(fileName), { sha: sha, repoPath: repoPath! }));
- this._versionedUriCache.set(cacheKey, entry);
- return file;
- }
-
- getVersionedFileText(repoPath: string, fileName: string, sha: string) {
- Logger.log(`getVersionedFileText('${repoPath}', '${fileName}', ${sha})`);
-
- return Git.show(repoPath, fileName, sha, { encoding: GitService.getEncoding(repoPath, fileName) });
- }
-
- hasGitUriForFile(editor: TextEditor): boolean {
- if (editor === undefined || editor.document === undefined || editor.document.uri === undefined) return false;
-
- const cacheKey = this.getCacheEntryKey(editor.document.uri);
- return this._versionedUriCache.has(cacheKey);
- }
-
- isEditorBlameable(editor: TextEditor): boolean {
- return (editor.viewColumn !== undefined || this.isTrackable(editor.document.uri) || this.hasGitUriForFile(editor));
- }
-
- 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.GitLensGit;
- }
-
- async isTracked(fileName: string, repoPath?: string, sha?: string): Promise<boolean>;
- async isTracked(uri: GitUri): Promise<boolean>;
- async isTracked(fileNameOrUri: string | GitUri, repoPath?: string, sha?: string): Promise<boolean> {
- if (sha === GitService.deletedSha) return false;
-
- let cacheKey: string;
- let fileName: string;
- if (typeof fileNameOrUri === 'string') {
- [fileName, repoPath] = Git.splitPath(fileNameOrUri, repoPath);
- cacheKey = this.getCacheEntryKey(fileNameOrUri);
- }
- else {
- if (!this.isTrackable(fileNameOrUri)) return false;
-
- fileName = fileNameOrUri.fsPath;
- repoPath = fileNameOrUri.repoPath;
- sha = fileNameOrUri.sha;
- cacheKey = this.getCacheEntryKey(fileName);
- }
-
- if (sha !== undefined) {
- cacheKey += `:${sha}`;
- }
-
- Logger.log(`isTracked('${fileName}', '${repoPath}', '${sha}')`);
-
- let tracked = this._trackedCache.get(cacheKey);
- if (tracked !== undefined) {
- if (typeof tracked === 'boolean') return tracked;
- return await tracked;
- }
-
- tracked = this.isTrackedCore(repoPath === undefined ? '' : repoPath, fileName, sha);
- this._trackedCache.set(cacheKey, tracked);
-
- tracked = await tracked;
- this._trackedCache.set(cacheKey, tracked);
-
- return tracked;
- }
-
- private async isTrackedCore(repoPath: string, fileName: string, sha?: string) {
- if (sha === 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 && sha !== undefined) {
- tracked = !!await Git.ls_files(repoPath === undefined ? '' : repoPath, fileName, { ref: sha });
- // 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: `${sha}^` });
- }
- }
- 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)) return ref;
-
- Logger.log(`resolveReference('${repoPath}', '${ref}', '${uri && uri.toString()}')`);
-
- if (uri === undefined) return (await Git.revparse(repoPath, ref)) || ref;
-
- return (await Git.log_resolve(repoPath, Git.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);
- 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<string>('encoding'));
- }
-
- static initialize(gitPath?: string): Promise<IGit> {
- return Git.getGitInfo(gitPath);
- }
-
- static getGitPath(): string {
- return Git.gitInfo().path;
- }
-
- static getGitVersion(): string {
- return Git.gitInfo().version;
- }
-
- 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 normalizePath(fileName: string): string {
- return Git.normalizePath(fileName);
- }
-
- static shortenSha(sha: string | undefined) {
- if (sha === undefined) return undefined;
- if (sha === GitService.deletedSha) return '(deleted)';
-
- return Git.isSha(sha) || Git.isStagedUncommitted(sha)
- ? Git.shortenSha(sha)
- : sha;
- }
-
- static validateGitVersion(major: number, minor: number): boolean {
- const [gitMajor, gitMinor] = this.getGitVersion().split('.');
- return (parseInt(gitMajor, 10) >= major && parseInt(gitMinor, 10) >= minor);
- }
- }
|