You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1569 lines
60 KiB

7 years ago
  1. 'use strict';
  2. import { Functions, Iterables, Objects, Strings, TernarySearchTree } from './system';
  3. import { ConfigurationChangeEvent, Disposable, Event, EventEmitter, Range, TextDocument, TextDocumentChangeEvent, TextEditor, Uri, window, WindowState, workspace, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode';
  4. import { configuration, IConfig, IRemotesConfig } from './configuration';
  5. import { CommandContext, DocumentSchemes, setCommandContext } from './constants';
  6. import { RemoteProviderFactory, RemoteProviderMap } from './git/remotes/factory';
  7. 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';
  8. import { GitUri, IGitCommitInfo } from './git/gitUri';
  9. import { Logger } from './logger';
  10. import * as fs from 'fs';
  11. import * as path from 'path';
  12. export { GitUri, IGit, IGitCommitInfo };
  13. export * from './git/models/models';
  14. export * from './git/formatters/commit';
  15. export * from './git/formatters/status';
  16. export { getNameFromRemoteResource, RemoteProvider, RemoteResource, RemoteResourceType } from './git/remotes/provider';
  17. export { RemoteProviderFactory } from './git/remotes/factory';
  18. export * from './git/gitContextTracker';
  19. class UriCacheEntry {
  20. constructor(
  21. public readonly uri: GitUri
  22. ) { }
  23. }
  24. class GitCacheEntry {
  25. private cache: Map<string, CachedBlame | CachedDiff | CachedLog> = new Map();
  26. constructor(
  27. public readonly key: string
  28. ) { }
  29. get hasErrors(): boolean {
  30. return Iterables.every(this.cache.values(), entry => entry.errorMessage !== undefined);
  31. }
  32. get<T extends CachedBlame | CachedDiff | CachedLog>(key: string): T | undefined {
  33. return this.cache.get(key) as T;
  34. }
  35. set<T extends CachedBlame | CachedDiff | CachedLog>(key: string, value: T) {
  36. this.cache.set(key, value);
  37. }
  38. }
  39. interface CachedItem<T> {
  40. item: Promise<T>;
  41. errorMessage?: string;
  42. }
  43. interface CachedBlame extends CachedItem<GitBlame> { }
  44. interface CachedDiff extends CachedItem<GitDiff> { }
  45. interface CachedLog extends CachedItem<GitLog> { }
  46. enum RemoveCacheReason {
  47. DocumentChanged,
  48. DocumentClosed
  49. }
  50. export enum GitRepoSearchBy {
  51. Author = 'author',
  52. ChangedOccurrences = 'changed-occurrences',
  53. Changes = 'changes',
  54. Files = 'files',
  55. Message = 'message',
  56. Sha = 'sha'
  57. }
  58. export enum GitChangeReason {
  59. GitCache = 'git-cache',
  60. Repositories = 'repositories'
  61. }
  62. export interface GitChangeEvent {
  63. reason: GitChangeReason;
  64. }
  65. export class GitService extends Disposable {
  66. static emptyPromise: Promise<GitBlame | GitDiff | GitLog | undefined> = Promise.resolve(undefined);
  67. static deletedSha = 'ffffffffffffffffffffffffffffffffffffffff';
  68. static stagedUncommittedSha = Git.stagedUncommittedSha;
  69. static uncommittedSha = Git.uncommittedSha;
  70. config: IConfig;
  71. private _onDidBlameFail = new EventEmitter<string>();
  72. get onDidBlameFail(): Event<string> {
  73. return this._onDidBlameFail.event;
  74. }
  75. private _onDidChange = new EventEmitter<GitChangeEvent>();
  76. get onDidChange(): Event<GitChangeEvent> {
  77. return this._onDidChange.event;
  78. }
  79. private _cacheDisposable: Disposable | undefined;
  80. private _disposable: Disposable | undefined;
  81. private _documentKeyMap: Map<TextDocument, string>;
  82. private _gitCache: Map<string, GitCacheEntry>;
  83. private _repositoryTree: TernarySearchTree<Repository>;
  84. private _repositoriesLoadingPromise: Promise<void> | undefined;
  85. private _suspended: boolean = false;
  86. private _trackedCache: Map<string, boolean | Promise<boolean>>;
  87. private _versionedUriCache: Map<string, UriCacheEntry>;
  88. constructor() {
  89. super(() => this.dispose());
  90. this._documentKeyMap = new Map();
  91. this._gitCache = new Map();
  92. this._repositoryTree = TernarySearchTree.forPaths();
  93. this._trackedCache = new Map();
  94. this._versionedUriCache = new Map();
  95. this._disposable = Disposable.from(
  96. window.onDidChangeWindowState(this.onWindowStateChanged, this),
  97. workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this),
  98. configuration.onDidChange(this.onConfigurationChanged, this)
  99. );
  100. this.onConfigurationChanged(configuration.initializingChangeEvent);
  101. this._repositoriesLoadingPromise = this.onWorkspaceFoldersChanged();
  102. }
  103. dispose() {
  104. this._repositoryTree.forEach(r => r.dispose());
  105. this._disposable && this._disposable.dispose();
  106. this._cacheDisposable && this._cacheDisposable.dispose();
  107. this._cacheDisposable = undefined;
  108. this._documentKeyMap.clear();
  109. this._gitCache.clear();
  110. this._trackedCache.clear();
  111. this._versionedUriCache.clear();
  112. }
  113. get UseCaching() {
  114. return this.config.advanced.caching.enabled;
  115. }
  116. private onAnyRepositoryChanged() {
  117. this._gitCache.clear();
  118. this._trackedCache.clear();
  119. }
  120. private onConfigurationChanged(e: ConfigurationChangeEvent) {
  121. const initializing = configuration.initializing(e);
  122. const cfg = configuration.get<IConfig>();
  123. if (initializing || configuration.changed(e, configuration.name('keymap').value)) {
  124. setCommandContext(CommandContext.KeyMap, cfg.keymap);
  125. }
  126. if (initializing || configuration.changed(e, configuration.name('advanced')('caching')('enabled').value)) {
  127. if (cfg.advanced.caching.enabled) {
  128. this._cacheDisposable && this._cacheDisposable.dispose();
  129. this._cacheDisposable = Disposable.from(
  130. workspace.onDidChangeTextDocument(Functions.debounce(this.onTextDocumentChanged, 50), this),
  131. workspace.onDidCloseTextDocument(this.onTextDocumentClosed, this)
  132. );
  133. }
  134. else {
  135. this._cacheDisposable && this._cacheDisposable.dispose();
  136. this._cacheDisposable = undefined;
  137. this._documentKeyMap.clear();
  138. this._gitCache.clear();
  139. }
  140. }
  141. if (initializing || configuration.changed(e, configuration.name('defaultDateStyle').value) ||
  142. configuration.changed(e, configuration.name('defaultDateFormat').value)) {
  143. CommitFormatting.reset();
  144. }
  145. this.config = cfg;
  146. // Only count the change if we aren't initializing
  147. if (!initializing && configuration.changed(e, configuration.name('blame')('ignoreWhitespace').value, null)) {
  148. this._gitCache.clear();
  149. this.fireChange(GitChangeReason.GitCache);
  150. }
  151. }
  152. private onTextDocumentChanged(e: TextDocumentChangeEvent) {
  153. let key = this._documentKeyMap.get(e.document);
  154. if (key === undefined) {
  155. key = this.getCacheEntryKey(e.document.uri);
  156. this._documentKeyMap.set(e.document, key);
  157. }
  158. // Don't remove broken blame on change (since otherwise we'll have to run the broken blame again)
  159. const entry = this._gitCache.get(key);
  160. if (entry === undefined || entry.hasErrors) return;
  161. if (this._gitCache.delete(key)) {
  162. Logger.log(`Clear cache entry for '${key}', reason=${RemoveCacheReason[RemoveCacheReason.DocumentChanged]}`);
  163. }
  164. }
  165. private onTextDocumentClosed(document: TextDocument) {
  166. this._documentKeyMap.delete(document);
  167. const key = this.getCacheEntryKey(document.uri);
  168. if (this._gitCache.delete(key)) {
  169. Logger.log(`Clear cache entry for '${key}', reason=${RemoveCacheReason[RemoveCacheReason.DocumentClosed]}`);
  170. }
  171. }
  172. private onWindowStateChanged(e: WindowState) {
  173. if (e.focused) {
  174. this._repositoryTree.forEach(r => r.resume());
  175. }
  176. else {
  177. this._repositoryTree.forEach(r => r.suspend());
  178. }
  179. this._suspended = !e.focused;
  180. }
  181. private async onWorkspaceFoldersChanged(e?: WorkspaceFoldersChangeEvent) {
  182. let initializing = false;
  183. if (e === undefined) {
  184. initializing = true;
  185. e = {
  186. added: workspace.workspaceFolders || [],
  187. removed: []
  188. } as WorkspaceFoldersChangeEvent;
  189. }
  190. for (const f of e.added) {
  191. if (f.uri.scheme !== DocumentSchemes.File) continue;
  192. // Search for and add all repositories (nested and/or submodules)
  193. const repositories = await this.repositorySearch(f);
  194. for (const r of repositories) {
  195. this._repositoryTree.set(r.path, r);
  196. }
  197. }
  198. for (const f of e.removed) {
  199. if (f.uri.scheme !== DocumentSchemes.File) continue;
  200. const fsPath = f.uri.fsPath;
  201. const filteredTree = this._repositoryTree.findSuperstr(fsPath);
  202. const reposToDelete = filteredTree !== undefined
  203. // Since the filtered tree will have keys that are relative to the fsPath, normalize to the full path
  204. ? [...Iterables.map<[Repository, string], [Repository, string]>(filteredTree.entries(), ([r, k]) => [r, path.join(fsPath, k)])]
  205. : [];
  206. const repo = this._repositoryTree.get(fsPath);
  207. if (repo !== undefined) {
  208. reposToDelete.push([repo, fsPath]);
  209. }
  210. for (const [r, k] of reposToDelete) {
  211. this._repositoryTree.delete(k);
  212. r.dispose();
  213. }
  214. }
  215. await setCommandContext(CommandContext.HasRepository, this._repositoryTree.any());
  216. if (!initializing) {
  217. // Defer the event trigger enough to let everything unwind
  218. setImmediate(() => this.fireChange(GitChangeReason.Repositories));
  219. }
  220. }
  221. private async repositorySearch(folder: WorkspaceFolder): Promise<Repository[]> {
  222. const folderUri = folder.uri;
  223. const repositories: Repository[] = [];
  224. const anyRepoChangedFn = this.onAnyRepositoryChanged.bind(this);
  225. const rootPath = await this.getRepoPathCore(folderUri.fsPath, true);
  226. if (rootPath !== undefined) {
  227. repositories.push(new Repository(folder, rootPath, true, this, anyRepoChangedFn, this._suspended));
  228. }
  229. const depth = configuration.get<number>(configuration.name('advanced')('repositorySearchDepth').value, folderUri);
  230. if (depth <= 0) return repositories;
  231. // Get any specified excludes -- this is a total hack, but works for some simple cases and something is better than nothing :)
  232. let excludes = {
  233. ...workspace.getConfiguration('files', folderUri).get<{ [key: string]: boolean }>('exclude', {}),
  234. ...workspace.getConfiguration('search', folderUri).get<{ [key: string]: boolean }>('exclude', {})
  235. };
  236. const excludedPaths = [...Iterables.filterMap(Objects.entries(excludes), ([key, value]) => {
  237. if (!value) return undefined;
  238. if (key.startsWith('**/')) return key.substring(3);
  239. return key;
  240. })];
  241. excludes = excludedPaths.reduce((accumulator, current) => {
  242. accumulator[current] = true;
  243. return accumulator;
  244. }, Object.create(null) as any);
  245. const start = process.hrtime();
  246. const paths = await this.repositorySearchCore(folderUri.fsPath, depth, excludes);
  247. const duration = process.hrtime(start);
  248. Logger.log(`Searching (depth=${depth}) for repositories in ${folderUri.fsPath} took ${(duration[0] * 1000) + Math.floor(duration[1] / 1000000)} ms`);
  249. for (let p of paths) {
  250. p = path.dirname(p);
  251. // If we are the same as the root, skip it
  252. if (Git.normalizePath(p) === rootPath) continue;
  253. const rp = await this.getRepoPathCore(p, true);
  254. if (rp === undefined) continue;
  255. repositories.push(new Repository(folder, rp, false, this, anyRepoChangedFn, this._suspended));
  256. }
  257. // const uris = await workspace.findFiles(new RelativePattern(folder, '**/.git/HEAD'));
  258. // for (const uri of uris) {
  259. // const rp = await this.getRepoPathCore(path.resolve(path.dirname(uri.fsPath), '../'), true);
  260. // if (rp !== undefined && rp !== rootPath) {
  261. // repositories.push(new Repository(folder, rp, false, this, anyRepoChangedFn, this._suspended));
  262. // }
  263. // }
  264. return repositories;
  265. }
  266. private async repositorySearchCore(root: string, depth: number, excludes: { [key: string]: boolean }, repositories: string[] = []): Promise<string[]> {
  267. return new Promise<string[]>((resolve, reject) => {
  268. fs.readdir(root, async (err, files) => {
  269. if (err != null) {
  270. reject(err);
  271. return;
  272. }
  273. if (files.length === 0) {
  274. resolve(repositories);
  275. return;
  276. }
  277. const folders: string[] = [];
  278. const promises = files.map(file => {
  279. const fullPath = path.resolve(root, file);
  280. return new Promise<void>((res, rej) => {
  281. fs.stat(fullPath, (err, stat) => {
  282. if (file === '.git') {
  283. repositories.push(fullPath);
  284. }
  285. else if (err == null && excludes[file] !== true && stat != null && stat.isDirectory()) {
  286. folders.push(fullPath);
  287. }
  288. res();
  289. });
  290. });
  291. });
  292. await Promise.all(promises);
  293. if (depth-- > 0) {
  294. for (const folder of folders) {
  295. await this.repositorySearchCore(folder, depth, excludes, repositories);
  296. }
  297. }
  298. resolve(repositories);
  299. });
  300. });
  301. }
  302. private fireChange(reason: GitChangeReason) {
  303. this._onDidChange.fire({ reason: reason });
  304. }
  305. checkoutFile(uri: GitUri, sha?: string) {
  306. sha = sha || uri.sha;
  307. Logger.log(`checkoutFile('${uri.repoPath}', '${uri.fsPath}', '${sha}')`);
  308. return Git.checkout(uri.repoPath!, uri.fsPath, sha!);
  309. }
  310. private async fileExists(repoPath: string, fileName: string): Promise<boolean> {
  311. return await new Promise<boolean>((resolve, reject) => fs.exists(path.resolve(repoPath, fileName), resolve));
  312. }
  313. async findNextCommit(repoPath: string, fileName: string, sha?: string): Promise<GitLogCommit | undefined> {
  314. let log = await this.getLogForFile(repoPath, fileName, { maxCount: 1, ref: sha, reverse: true });
  315. let commit = log && Iterables.first(log.commits.values());
  316. if (commit) return commit;
  317. const nextFileName = await this.findNextFileName(repoPath, fileName, sha);
  318. if (nextFileName) {
  319. log = await this.getLogForFile(repoPath, nextFileName, { maxCount: 1, ref: sha, reverse: true });
  320. commit = log && Iterables.first(log.commits.values());
  321. }
  322. return commit;
  323. }
  324. async findNextFileName(repoPath: string | undefined, fileName: string, sha?: string): Promise<string | undefined> {
  325. [fileName, repoPath] = Git.splitPath(fileName, repoPath);
  326. return (await this.fileExists(repoPath, fileName))
  327. ? fileName
  328. : await this.findNextFileNameCore(repoPath, fileName, sha);
  329. }
  330. private async findNextFileNameCore(repoPath: string, fileName: string, sha?: string): Promise<string | undefined> {
  331. if (sha === undefined) {
  332. // Get the most recent commit for this file name
  333. const c = await this.getLogCommit(repoPath, fileName);
  334. if (c === undefined) return undefined;
  335. sha = c.sha;
  336. }
  337. // Get the full commit (so we can see if there are any matching renames in the file statuses)
  338. const log = await this.getLogForRepo(repoPath, { maxCount: 1, ref: sha });
  339. if (log === undefined) return undefined;
  340. const c = Iterables.first(log.commits.values());
  341. const status = c.fileStatuses.find(f => f.originalFileName === fileName);
  342. if (status === undefined) return undefined;
  343. return status.fileName;
  344. }
  345. async findWorkingFileName(commit: GitCommit): Promise<string | undefined>;
  346. async findWorkingFileName(repoPath: string | undefined, fileName: string): Promise<string | undefined>;
  347. async findWorkingFileName(commitOrRepoPath: GitCommit | string | undefined, fileName?: string): Promise<string | undefined> {
  348. let repoPath: string | undefined;
  349. if (commitOrRepoPath === undefined || typeof commitOrRepoPath === 'string') {
  350. repoPath = commitOrRepoPath;
  351. if (fileName === undefined) throw new Error('Invalid fileName');
  352. [fileName] = Git.splitPath(fileName, repoPath);
  353. }
  354. else {
  355. const c = commitOrRepoPath;
  356. repoPath = c.repoPath;
  357. if (c.workingFileName && await this.fileExists(repoPath, c.workingFileName)) return c.workingFileName;
  358. fileName = c.fileName;
  359. }
  360. while (true) {
  361. if (await this.fileExists(repoPath!, fileName)) return fileName;
  362. fileName = await this.findNextFileNameCore(repoPath!, fileName);
  363. if (fileName === undefined) return undefined;
  364. }
  365. }
  366. async getActiveRepoPath(editor?: TextEditor): Promise<string | undefined> {
  367. if (editor === undefined) {
  368. const repoPath = this.getHighlanderRepoPath();
  369. if (repoPath !== undefined) return repoPath;
  370. }
  371. editor = editor || window.activeTextEditor;
  372. if (editor === undefined) return undefined;
  373. return this.getRepoPath(editor.document.uri);
  374. }
  375. getHighlanderRepoPath(): string | undefined {
  376. const entry = this._repositoryTree.highlander();
  377. if (entry === undefined) return undefined;
  378. const [repo] = entry;
  379. return repo.path;
  380. }
  381. public async getBlameability(uri: GitUri): Promise<boolean> {
  382. if (!this.UseCaching) return await this.isTracked(uri);
  383. const cacheKey = this.getCacheEntryKey(uri);
  384. const entry = this._gitCache.get(cacheKey);
  385. if (entry === undefined) return await this.isTracked(uri);
  386. return !entry.hasErrors;
  387. }
  388. async getBlameForFile(uri: GitUri): Promise<GitBlame | undefined> {
  389. let key = 'blame';
  390. if (uri.sha !== undefined) {
  391. key += `:${uri.sha}`;
  392. }
  393. let entry: GitCacheEntry | undefined;
  394. if (this.UseCaching) {
  395. const cacheKey = this.getCacheEntryKey(uri);
  396. entry = this._gitCache.get(cacheKey);
  397. if (entry !== undefined) {
  398. const cachedBlame = entry.get<CachedBlame>(key);
  399. if (cachedBlame !== undefined) {
  400. Logger.log(`getBlameForFile[Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`);
  401. return cachedBlame.item;
  402. }
  403. }
  404. Logger.log(`getBlameForFile[Not Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`);
  405. if (entry === undefined) {
  406. entry = new GitCacheEntry(cacheKey);
  407. this._gitCache.set(entry.key, entry);
  408. }
  409. }
  410. else {
  411. Logger.log(`getBlameForFile('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`);
  412. }
  413. const promise = this.getBlameForFileCore(uri, entry, key);
  414. if (entry) {
  415. Logger.log(`Add blame cache for '${entry.key}:${key}'`);
  416. entry.set<CachedBlame>(key, {
  417. item: promise
  418. } as CachedBlame);
  419. }
  420. return promise;
  421. }
  422. private async getBlameForFileCore(uri: GitUri, entry: GitCacheEntry | undefined, key: string): Promise<GitBlame | undefined> {
  423. if (!(await this.isTracked(uri))) {
  424. Logger.log(`Skipping blame; '${uri.fsPath}' is not tracked`);
  425. if (entry && entry.key) {
  426. this._onDidBlameFail.fire(entry.key);
  427. }
  428. return GitService.emptyPromise as Promise<GitBlame>;
  429. }
  430. const [file, root] = Git.splitPath(uri.fsPath, uri.repoPath, false);
  431. try {
  432. const data = await Git.blame(root, file, uri.sha, { ignoreWhitespace: this.config.blame.ignoreWhitespace });
  433. const blame = GitBlameParser.parse(data, root, file);
  434. return blame;
  435. }
  436. catch (ex) {
  437. // Trap and cache expected blame errors
  438. if (entry) {
  439. const msg = ex && ex.toString();
  440. Logger.log(`Replace blame cache with empty promise for '${entry.key}:${key}'`);
  441. entry.set<CachedBlame>(key, {
  442. item: GitService.emptyPromise,
  443. errorMessage: msg
  444. } as CachedBlame);
  445. this._onDidBlameFail.fire(entry.key);
  446. return GitService.emptyPromise as Promise<GitBlame>;
  447. }
  448. return undefined;
  449. }
  450. }
  451. async getBlameForFileContents(uri: GitUri, contents: string): Promise<GitBlame | undefined> {
  452. const key = `blame:${Strings.sha1(contents)}`;
  453. let entry: GitCacheEntry | undefined;
  454. if (this.UseCaching) {
  455. const cacheKey = this.getCacheEntryKey(uri);
  456. entry = this._gitCache.get(cacheKey);
  457. if (entry !== undefined) {
  458. const cachedBlame = entry.get<CachedBlame>(key);
  459. if (cachedBlame !== undefined) {
  460. Logger.log(`getBlameForFileContents[Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`);
  461. return cachedBlame.item;
  462. }
  463. }
  464. Logger.log(`getBlameForFileContents[Not Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`);
  465. if (entry === undefined) {
  466. entry = new GitCacheEntry(cacheKey);
  467. this._gitCache.set(entry.key, entry);
  468. }
  469. }
  470. else {
  471. Logger.log(`getBlameForFileContents('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}')`);
  472. }
  473. const promise = this.getBlameForFileContentsCore(uri, contents, entry, key);
  474. if (entry) {
  475. Logger.log(`Add blame cache for '${entry.key}:${key}'`);
  476. entry.set<CachedBlame>(key, {
  477. item: promise
  478. } as CachedBlame);
  479. }
  480. return promise;
  481. }
  482. async getBlameForFileContentsCore(uri: GitUri, contents: string, entry: GitCacheEntry | undefined, key: string): Promise<GitBlame | undefined> {
  483. if (!(await this.isTracked(uri))) {
  484. Logger.log(`Skipping blame; '${uri.fsPath}' is not tracked`);
  485. if (entry && entry.key) {
  486. this._onDidBlameFail.fire(entry.key);
  487. }
  488. return GitService.emptyPromise as Promise<GitBlame>;
  489. }
  490. const [file, root] = Git.splitPath(uri.fsPath, uri.repoPath, false);
  491. try {
  492. const data = await Git.blame_contents(root, file, contents, { ignoreWhitespace: this.config.blame.ignoreWhitespace });
  493. const blame = GitBlameParser.parse(data, root, file);
  494. return blame;
  495. }
  496. catch (ex) {
  497. // Trap and cache expected blame errors
  498. if (entry) {
  499. const msg = ex && ex.toString();
  500. Logger.log(`Replace blame cache with empty promise for '${entry.key}:${key}'`);
  501. entry.set<CachedBlame>(key, {
  502. item: GitService.emptyPromise,
  503. errorMessage: msg
  504. } as CachedBlame);
  505. this._onDidBlameFail.fire(entry.key);
  506. return GitService.emptyPromise as Promise<GitBlame>;
  507. }
  508. return undefined;
  509. }
  510. }
  511. async getBlameForLine(uri: GitUri, line: number): Promise<GitBlameLine | undefined> {
  512. Logger.log(`getBlameForLine('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}', ${line})`);
  513. if (this.UseCaching) {
  514. const blame = await this.getBlameForFile(uri);
  515. if (blame === undefined) return undefined;
  516. let blameLine = blame.lines[line];
  517. if (blameLine === undefined) {
  518. if (blame.lines.length !== line) return undefined;
  519. blameLine = blame.lines[line - 1];
  520. }
  521. const commit = blame.commits.get(blameLine.sha);
  522. if (commit === undefined) return undefined;
  523. return {
  524. author: { ...blame.authors.get(commit.author), lineCount: commit.lines.length },
  525. commit: commit,
  526. line: blameLine
  527. } as GitBlameLine;
  528. }
  529. const lineToBlame = line + 1;
  530. const fileName = uri.fsPath;
  531. try {
  532. const data = await Git.blame(uri.repoPath, fileName, uri.sha, { ignoreWhitespace: this.config.blame.ignoreWhitespace, startLine: lineToBlame, endLine: lineToBlame });
  533. const blame = GitBlameParser.parse(data, uri.repoPath, fileName);
  534. if (blame === undefined) return undefined;
  535. return {
  536. author: Iterables.first(blame.authors.values()),
  537. commit: Iterables.first(blame.commits.values()),
  538. line: blame.lines[line]
  539. } as GitBlameLine;
  540. }
  541. catch {
  542. return undefined;
  543. }
  544. }
  545. async getBlameForLineContents(uri: GitUri, line: number, contents: string, options: { skipCache?: boolean } = {}): Promise<GitBlameLine | undefined> {
  546. Logger.log(`getBlameForLineContents('${uri.repoPath}', '${uri.fsPath}', ${line})`);
  547. if (!options.skipCache && this.UseCaching) {
  548. const blame = await this.getBlameForFileContents(uri, contents);
  549. if (blame === undefined) return undefined;
  550. let blameLine = blame.lines[line];
  551. if (blameLine === undefined) {
  552. if (blame.lines.length !== line) return undefined;
  553. blameLine = blame.lines[line - 1];
  554. }
  555. const commit = blame.commits.get(blameLine.sha);
  556. if (commit === undefined) return undefined;
  557. return {
  558. author: { ...blame.authors.get(commit.author), lineCount: commit.lines.length },
  559. commit: commit,
  560. line: blameLine
  561. } as GitBlameLine;
  562. }
  563. const lineToBlame = line + 1;
  564. const fileName = uri.fsPath;
  565. try {
  566. const data = await Git.blame_contents(uri.repoPath, fileName, contents, { ignoreWhitespace: this.config.blame.ignoreWhitespace, startLine: lineToBlame, endLine: lineToBlame });
  567. const blame = GitBlameParser.parse(data, uri.repoPath, fileName);
  568. if (blame === undefined) return undefined;
  569. return {
  570. author: Iterables.first(blame.authors.values()),
  571. commit: Iterables.first(blame.commits.values()),
  572. line: blame.lines[line]
  573. } as GitBlameLine;
  574. }
  575. catch {
  576. return undefined;
  577. }
  578. }
  579. async getBlameForRange(uri: GitUri, range: Range): Promise<GitBlameLines | undefined> {
  580. Logger.log(`getBlameForRange('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}', [${range.start.line}, ${range.end.line}])`);
  581. const blame = await this.getBlameForFile(uri);
  582. if (blame === undefined) return undefined;
  583. return this.getBlameForRangeSync(blame, uri, range);
  584. }
  585. getBlameForRangeSync(blame: GitBlame, uri: GitUri, range: Range): GitBlameLines | undefined {
  586. Logger.log(`getBlameForRangeSync('${uri.repoPath}', '${uri.fsPath}', '${uri.sha}', [${range.start.line}, ${range.end.line}])`);
  587. if (blame.lines.length === 0) return { allLines: blame.lines, ...blame };
  588. if (range.start.line === 0 && range.end.line === blame.lines.length - 1) {
  589. return { allLines: blame.lines, ...blame };
  590. }
  591. const lines = blame.lines.slice(range.start.line, range.end.line + 1);
  592. const shas = new Set(lines.map(l => l.sha));
  593. const authors: Map<string, GitAuthor> = new Map();
  594. const commits: Map<string, GitBlameCommit> = new Map();
  595. for (const c of blame.commits.values()) {
  596. if (!shas.has(c.sha)) continue;
  597. const commit = c.with({ lines: c.lines.filter(l => l.line >= range.start.line && l.line <= range.end.line) });
  598. commits.set(c.sha, commit);
  599. let author = authors.get(commit.author);
  600. if (author === undefined) {
  601. author = {
  602. name: commit.author,
  603. lineCount: 0
  604. };
  605. authors.set(author.name, author);
  606. }
  607. author.lineCount += commit.lines.length;
  608. }
  609. const sortedAuthors = new Map([...authors.entries()].sort((a, b) => b[1].lineCount - a[1].lineCount));
  610. return {
  611. authors: sortedAuthors,
  612. commits: commits,
  613. lines: lines,
  614. allLines: blame.lines
  615. } as GitBlameLines;
  616. }
  617. async getBranch(repoPath: string | undefined): Promise<GitBranch | undefined> {
  618. if (repoPath === undefined) return undefined;
  619. Logger.log(`getBranch('${repoPath}')`);
  620. const data = await Git.revparse_currentBranch(repoPath);
  621. if (data === undefined) return undefined;
  622. const branch = data.split('\n');
  623. return new GitBranch(repoPath, branch[0], true, branch[1]);
  624. }
  625. async getBranches(repoPath: string | undefined): Promise<GitBranch[]> {
  626. if (repoPath === undefined) return [];
  627. Logger.log(`getBranches('${repoPath}')`);
  628. const data = await Git.branch(repoPath, { all: true });
  629. // If we don't get any data, assume the repo doesn't have any commits yet so check if we have a current branch
  630. if (data === '') {
  631. const current = await this.getBranch(repoPath);
  632. return current !== undefined ? [current] : [];
  633. }
  634. return GitBranchParser.parse(data, repoPath) || [];
  635. }
  636. getCacheEntryKey(fileName: string): string;
  637. getCacheEntryKey(uri: Uri): string;
  638. getCacheEntryKey(fileNameOrUri: string | Uri): string {
  639. return Git.normalizePath(typeof fileNameOrUri === 'string' ? fileNameOrUri : fileNameOrUri.fsPath).toLowerCase();
  640. }
  641. async getChangedFilesCount(repoPath: string, sha?: string): Promise<GitDiffShortStat | undefined> {
  642. Logger.log(`getChangedFilesCount('${repoPath}', '${sha}')`);
  643. const data = await Git.diff_shortstat(repoPath, sha);
  644. return GitDiffParser.parseShortStat(data);
  645. }
  646. async getConfig(key: string, repoPath?: string): Promise<string | undefined> {
  647. Logger.log(`getConfig('${key}', '${repoPath}')`);
  648. return await Git.config_get(key, repoPath);
  649. }
  650. getGitUriForVersionedFile(uri: Uri) {
  651. const cacheKey = this.getCacheEntryKey(uri);
  652. const entry = this._versionedUriCache.get(cacheKey);
  653. return entry && entry.uri;
  654. }
  655. async getDiffForFile(uri: GitUri, sha1?: string, sha2?: string): Promise<GitDiff | undefined> {
  656. if (sha1 !== undefined && sha2 === undefined && uri.sha !== undefined) {
  657. sha2 = uri.sha;
  658. }
  659. let key = 'diff';
  660. if (sha1 !== undefined) {
  661. key += `:${sha1}`;
  662. }
  663. if (sha2 !== undefined) {
  664. key += `:${sha2}`;
  665. }
  666. let entry: GitCacheEntry | undefined;
  667. if (this.UseCaching) {
  668. const cacheKey = this.getCacheEntryKey(uri);
  669. entry = this._gitCache.get(cacheKey);
  670. if (entry !== undefined) {
  671. const cachedDiff = entry.get<CachedDiff>(key);
  672. if (cachedDiff !== undefined) {
  673. Logger.log(`getDiffForFile[Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${sha1}', '${sha2}')`);
  674. return cachedDiff.item;
  675. }
  676. }
  677. Logger.log(`getDiffForFile[Not Cached(${key})]('${uri.repoPath}', '${uri.fsPath}', '${sha1}', '${sha2}')`);
  678. if (entry === undefined) {
  679. entry = new GitCacheEntry(cacheKey);
  680. this._gitCache.set(entry.key, entry);
  681. }
  682. }
  683. else {
  684. Logger.log(`getDiffForFile('${uri.repoPath}', '${uri.fsPath}', '${sha1}', '${sha2}')`);
  685. }
  686. const promise = this.getDiffForFileCore(uri.repoPath, uri.fsPath, sha1, sha2, { encoding: GitService.getEncoding(uri) }, entry, key);
  687. if (entry) {
  688. Logger.log(`Add log cache for '${entry.key}:${key}'`);
  689. entry.set<CachedDiff>(key, {
  690. item: promise
  691. } as CachedDiff);
  692. }
  693. return promise;
  694. }
  695. 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> {
  696. const [file, root] = Git.splitPath(fileName, repoPath, false);
  697. try {
  698. const data = await Git.diff(root, file, sha1, sha2, options);
  699. const diff = GitDiffParser.parse(data);
  700. return diff;
  701. }
  702. catch (ex) {
  703. // Trap and cache expected diff errors
  704. if (entry) {
  705. const msg = ex && ex.toString();
  706. Logger.log(`Replace diff cache with empty promise for '${entry.key}:${key}'`);
  707. entry.set<CachedDiff>(key, {
  708. item: GitService.emptyPromise,
  709. errorMessage: msg
  710. } as CachedDiff);
  711. return GitService.emptyPromise as Promise<GitDiff>;
  712. }
  713. return undefined;
  714. }
  715. }
  716. async getDiffForLine(uri: GitUri, line: number, sha1?: string, sha2?: string): Promise<GitDiffChunkLine | undefined> {
  717. Logger.log(`getDiffForLine('${uri.repoPath}', '${uri.fsPath}', ${line}, '${sha1}', '${sha2}')`);
  718. try {
  719. const diff = await this.getDiffForFile(uri, sha1, sha2);
  720. if (diff === undefined) return undefined;
  721. const chunk = diff.chunks.find(c => c.currentPosition.start <= line && c.currentPosition.end >= line);
  722. if (chunk === undefined) return undefined;
  723. return chunk.lines[line - chunk.currentPosition.start + 1];
  724. }
  725. catch (ex) {
  726. return undefined;
  727. }
  728. }
  729. async getDiffStatus(repoPath: string, sha1?: string, sha2?: string, options: { filter?: string } = {}): Promise<GitStatusFile[] | undefined> {
  730. Logger.log(`getDiffStatus('${repoPath}', '${sha1}', '${sha2}', ${options.filter})`);
  731. try {
  732. const data = await Git.diff_nameStatus(repoPath, sha1, sha2, options);
  733. const diff = GitDiffParser.parseNameStatus(data, repoPath);
  734. return diff;
  735. }
  736. catch (ex) {
  737. return undefined;
  738. }
  739. }
  740. async getLogCommit(repoPath: string | undefined, fileName: string, options?: { firstIfMissing?: boolean, previous?: boolean }): Promise<GitLogCommit | undefined>;
  741. async getLogCommit(repoPath: string | undefined, fileName: string, sha: string | undefined, options?: { firstIfMissing?: boolean, previous?: boolean }): Promise<GitLogCommit | undefined>;
  742. async getLogCommit(repoPath: string | undefined, fileName: string, shaOrOptions?: string | undefined | { firstIfMissing?: boolean, previous?: boolean }, options?: { firstIfMissing?: boolean, previous?: boolean }): Promise<GitLogCommit | undefined> {
  743. let sha: string | undefined = undefined;
  744. if (typeof shaOrOptions === 'string') {
  745. sha = shaOrOptions;
  746. }
  747. else if (options === undefined) {
  748. options = shaOrOptions;
  749. }
  750. options = options || {};
  751. Logger.log(`getLogCommit('${repoPath}', '${fileName}', '${sha}', ${options.firstIfMissing}, ${options.previous})`);
  752. const log = await this.getLogForFile(repoPath, fileName, { maxCount: options.previous ? 2 : 1, ref: sha });
  753. if (log === undefined) return undefined;
  754. const commit = sha && log.commits.get(sha);
  755. if (commit === undefined && sha && !options.firstIfMissing) {
  756. // If the sha isn't resolved we will never find it, so don't kick out
  757. if (!Git.isResolveRequired(sha)) return undefined;
  758. }
  759. return commit || Iterables.first(log.commits.values());
  760. }
  761. async getLogForRepo(repoPath: string, options: { maxCount?: number, ref?: string, reverse?: boolean } = {}): Promise<GitLog | undefined> {
  762. options = { reverse: false, ...options };
  763. Logger.log(`getLogForRepo('${repoPath}', '${options.ref}', ${options.maxCount}, ${options.reverse})`);
  764. const maxCount = options.maxCount == null
  765. ? this.config.advanced.maxQuickHistory || 0
  766. : options.maxCount;
  767. try {
  768. const data = await Git.log(repoPath, { maxCount: maxCount, ref: options.ref, reverse: options.reverse });
  769. const log = GitLogParser.parse(data, GitCommitType.Branch, repoPath, undefined, options.ref, maxCount, options.reverse!, undefined);
  770. if (log !== undefined) {
  771. const opts = { ...options };
  772. log.query = (maxCount: number | undefined) => this.getLogForRepo(repoPath, { ...opts, maxCount: maxCount });
  773. }
  774. return log;
  775. }
  776. catch (ex) {
  777. return undefined;
  778. }
  779. }
  780. async getLogForRepoSearch(repoPath: string, search: string, searchBy: GitRepoSearchBy, options: { maxCount?: number } = {}): Promise<GitLog | undefined> {
  781. Logger.log(`getLogForRepoSearch('${repoPath}', '${search}', '${searchBy}', ${options.maxCount})`);
  782. let maxCount = options.maxCount == null
  783. ? this.config.advanced.maxQuickHistory || 0
  784. : options.maxCount;
  785. let searchArgs: string[] | undefined = undefined;
  786. switch (searchBy) {
  787. case GitRepoSearchBy.Author:
  788. searchArgs = [`--author=${search}`];
  789. break;
  790. case GitRepoSearchBy.ChangedOccurrences:
  791. searchArgs = [`-S${search}`, '--pickaxe-regex'];
  792. break;
  793. case GitRepoSearchBy.Changes:
  794. searchArgs = [`-G${search}`];
  795. break;
  796. case GitRepoSearchBy.Files:
  797. searchArgs = [`--`, `${search}`];
  798. break;
  799. case GitRepoSearchBy.Message:
  800. searchArgs = [`--grep=${search}`];
  801. break;
  802. case GitRepoSearchBy.Sha:
  803. searchArgs = [search];
  804. maxCount = 1;
  805. break;
  806. }
  807. try {
  808. const data = await Git.log_search(repoPath, searchArgs, { maxCount: maxCount });
  809. const log = GitLogParser.parse(data, GitCommitType.Branch, repoPath, undefined, undefined, maxCount, false, undefined);
  810. if (log !== undefined) {
  811. const opts = { ...options };
  812. log.query = (maxCount: number | undefined) => this.getLogForRepoSearch(repoPath, search, searchBy, { ...opts, maxCount: maxCount });
  813. }
  814. return log;
  815. }
  816. catch (ex) {
  817. return undefined;
  818. }
  819. }
  820. async getLogForFile(repoPath: string | undefined, fileName: string, options: { maxCount?: number, range?: Range, ref?: string, reverse?: boolean, skipMerges?: boolean } = {}): Promise<GitLog | undefined> {
  821. options = { reverse: false, skipMerges: false, ...options };
  822. let key = 'log';
  823. if (options.ref !== undefined) {
  824. key += `:${options.ref}`;
  825. }
  826. if (options.maxCount !== undefined) {
  827. key += `:n${options.maxCount}`;
  828. }
  829. let entry: GitCacheEntry | undefined;
  830. if (this.UseCaching && options.range === undefined && !options.reverse) {
  831. const cacheKey = this.getCacheEntryKey(fileName);
  832. entry = this._gitCache.get(cacheKey);
  833. if (entry !== undefined) {
  834. const cachedLog = entry.get<CachedLog>(key);
  835. if (cachedLog !== undefined) {
  836. Logger.log(`getLogForFile[Cached(${key})]('${repoPath}', '${fileName}', '${options.ref}', ${options.maxCount}, undefined, ${options.reverse}, ${options.skipMerges})`);
  837. return cachedLog.item;
  838. }
  839. if (key !== 'log') {
  840. // Since we are looking for partial log, see if we have the log of the whole file
  841. const cachedLog = entry.get<CachedLog>('log');
  842. if (cachedLog !== undefined) {
  843. if (options.ref === undefined) {
  844. Logger.log(`getLogForFile[Cached(~${key})]('${repoPath}', '${fileName}', '', ${options.maxCount}, undefined, ${options.reverse}, ${options.skipMerges})`);
  845. return cachedLog.item;
  846. }
  847. Logger.log(`getLogForFile[? Cache(${key})]('${repoPath}', '${fileName}', '${options.ref}', ${options.maxCount}, undefined, ${options.reverse}, ${options.skipMerges})`);
  848. const log = await cachedLog.item;
  849. if (log !== undefined && log.commits.has(options.ref)) {
  850. Logger.log(`getLogForFile[Cached(${key})]('${repoPath}', '${fileName}', '${options.ref}', ${options.maxCount}, undefined, ${options.reverse}, ${options.skipMerges})`);
  851. return cachedLog.item;
  852. }
  853. }
  854. }
  855. }
  856. Logger.log(`getLogForFile[Not Cached(${key})]('${repoPath}', '${fileName}', ${options.ref}, ${options.maxCount}, undefined, ${options.reverse}, ${options.skipMerges})`);
  857. if (entry === undefined) {
  858. entry = new GitCacheEntry(cacheKey);
  859. this._gitCache.set(entry.key, entry);
  860. }
  861. }
  862. else {
  863. Logger.log(`getLogForFile('${repoPath}', '${fileName}', ${options.ref}, ${options.maxCount}, ${options.range && `[${options.range.start.line}, ${options.range.end.line}]`}, ${options.reverse}, ${options.skipMerges})`);
  864. }
  865. const promise = this.getLogForFileCore(repoPath, fileName, options, entry, key);
  866. if (entry) {
  867. Logger.log(`Add log cache for '${entry.key}:${key}'`);
  868. entry.set<CachedLog>(key, {
  869. item: promise
  870. } as CachedLog);
  871. }
  872. return promise;
  873. }
  874. 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> {
  875. if (!(await this.isTracked(fileName, repoPath, options.ref))) {
  876. Logger.log(`Skipping log; '${fileName}' is not tracked`);
  877. return GitService.emptyPromise as Promise<GitLog>;
  878. }
  879. const [file, root] = Git.splitPath(fileName, repoPath, false);
  880. try {
  881. const { range, ...opts } = options;
  882. const maxCount = options.maxCount == null
  883. ? this.config.advanced.maxQuickHistory || 0
  884. : options.maxCount;
  885. const data = await Git.log_file(root, file, { ...opts, maxCount: maxCount, startLine: range && range.start.line + 1, endLine: range && range.end.line + 1 });
  886. const log = GitLogParser.parse(data, GitCommitType.File, root, file, opts.ref, maxCount, opts.reverse!, range);
  887. if (log !== undefined) {
  888. const opts = { ...options };
  889. log.query = (maxCount: number | undefined) => this.getLogForFile(repoPath, fileName, { ...opts, maxCount: maxCount });
  890. }
  891. return log;
  892. }
  893. catch (ex) {
  894. // Trap and cache expected log errors
  895. if (entry) {
  896. const msg = ex && ex.toString();
  897. Logger.log(`Replace log cache with empty promise for '${entry.key}:${key}'`);
  898. entry.set<CachedLog>(key, {
  899. item: GitService.emptyPromise,
  900. errorMessage: msg
  901. } as CachedLog);
  902. return GitService.emptyPromise as Promise<GitLog>;
  903. }
  904. return undefined;
  905. }
  906. }
  907. async hasRemote(repoPath: string | undefined): Promise<boolean> {
  908. if (repoPath === undefined) return false;
  909. const repository = await this.getRepository(repoPath);
  910. if (repository === undefined) return false;
  911. return repository.hasRemote();
  912. }
  913. async hasRemotes(repoPath: string | undefined): Promise<boolean> {
  914. if (repoPath === undefined) return false;
  915. const repository = await this.getRepository(repoPath);
  916. if (repository === undefined) return false;
  917. return repository.hasRemotes();
  918. }
  919. async getMergeBase(repoPath: string, ref1: string, ref2: string, options: { forkPoint?: boolean } = {}) {
  920. try {
  921. const data = await Git.merge_base(repoPath, ref1, ref2, options);
  922. if (data === undefined) return undefined;
  923. return data.split('\n')[0];
  924. }
  925. catch (ex) {
  926. Logger.error(ex, 'GitService.getMergeBase');
  927. return undefined;
  928. }
  929. }
  930. async getRemotes(repoPath: string | undefined): Promise<GitRemote[]> {
  931. if (repoPath === undefined) return [];
  932. Logger.log(`getRemotes('${repoPath}')`);
  933. const repository = await this.getRepository(repoPath);
  934. if (repository !== undefined) return repository.getRemotes();
  935. return this.getRemotesCore(repoPath);
  936. }
  937. async getRemotesCore(repoPath: string | undefined, providerMap?: RemoteProviderMap): Promise<GitRemote[]> {
  938. if (repoPath === undefined) return [];
  939. Logger.log(`getRemotesCore('${repoPath}')`);
  940. providerMap = providerMap || RemoteProviderFactory.createMap(configuration.get<IRemotesConfig[] | null | undefined>(configuration.name('remotes').value, null));
  941. try {
  942. const data = await Git.remote(repoPath);
  943. return GitRemoteParser.parse(data, repoPath, RemoteProviderFactory.factory(providerMap));
  944. }
  945. catch (ex) {
  946. Logger.error(ex, 'GitService.getRemotesCore');
  947. return [];
  948. }
  949. }
  950. async getRepoPath(filePath: string): Promise<string | undefined>;
  951. async getRepoPath(uri: Uri | undefined): Promise<string | undefined>;
  952. async getRepoPath(filePathOrUri: string | Uri | undefined): Promise<string | undefined> {
  953. if (filePathOrUri === undefined) return await this.getActiveRepoPath();
  954. if (filePathOrUri instanceof GitUri) return filePathOrUri.repoPath;
  955. const repo = await this.getRepository(filePathOrUri);
  956. if (repo !== undefined) return repo.path;
  957. const rp = await this.getRepoPathCore(typeof filePathOrUri === 'string' ? filePathOrUri : filePathOrUri.fsPath, false);
  958. if (rp === undefined) return undefined;
  959. // Recheck this._repositoryTree.get(rp) to make sure we haven't already tried adding this due to awaits
  960. if (this._repositoryTree.get(rp) !== undefined) return rp;
  961. // If this new repo is inside one of our known roots and we we don't already know about, add it
  962. const root = this._repositoryTree.findSubstr(rp);
  963. const folder = root === undefined
  964. ? workspace.getWorkspaceFolder(Uri.file(rp))
  965. : root.folder;
  966. if (folder !== undefined) {
  967. const repo = new Repository(folder, rp, false, this, this.onAnyRepositoryChanged.bind(this), this._suspended);
  968. this._repositoryTree.set(rp, repo);
  969. // Send a notification that the repositories changed
  970. setImmediate(async () => {
  971. await setCommandContext(CommandContext.HasRepository, this._repositoryTree.any());
  972. this.fireChange(GitChangeReason.Repositories);
  973. });
  974. }
  975. return rp;
  976. }
  977. private async getRepoPathCore(filePath: string, isDirectory: boolean): Promise<string | undefined> {
  978. try {
  979. return await Git.revparse_toplevel(isDirectory ? filePath : path.dirname(filePath));
  980. }
  981. catch (ex) {
  982. Logger.error(ex, 'GitService.getRepoPathCore');
  983. return undefined;
  984. }
  985. }
  986. async getRepositories(): Promise<Iterable<Repository>> {
  987. const repositoryTree = await this.getRepositoryTree();
  988. return repositoryTree.values();
  989. }
  990. private async getRepositoryTree(): Promise<TernarySearchTree<Repository>> {
  991. if (this._repositoriesLoadingPromise !== undefined) {
  992. await this._repositoriesLoadingPromise;
  993. this._repositoriesLoadingPromise = undefined;
  994. }
  995. return this._repositoryTree;
  996. }
  997. async getRepository(repoPath: string): Promise<Repository | undefined>;
  998. async getRepository(uri: Uri): Promise<Repository | undefined>;
  999. async getRepository(repoPathOrUri: string | Uri): Promise<Repository | undefined>;
  1000. async getRepository(repoPathOrUri: string | Uri): Promise<Repository | undefined> {
  1001. const repositoryTree = await this.getRepositoryTree();
  1002. let path: string;
  1003. if (typeof repoPathOrUri === 'string') {
  1004. const repo = repositoryTree.get(repoPathOrUri);
  1005. if (repo !== undefined) return repo;
  1006. path = repoPathOrUri;
  1007. }
  1008. else {
  1009. if (repoPathOrUri instanceof GitUri) {
  1010. if (repoPathOrUri.repoPath) {
  1011. const repo = repositoryTree.get(repoPathOrUri.repoPath);
  1012. if (repo !== undefined) return repo;
  1013. }
  1014. path = repoPathOrUri.fsPath;
  1015. }
  1016. else {
  1017. path = repoPathOrUri.fsPath;
  1018. }
  1019. }
  1020. const repo = repositoryTree.findSubstr(path);
  1021. if (repo === undefined) return undefined;
  1022. // Make sure the file is tracked in that repo, before returning
  1023. if (!await this.isTrackedCore(repo.path, path)) return undefined;
  1024. return repo;
  1025. }
  1026. async getStashList(repoPath: string | undefined): Promise<GitStash | undefined> {
  1027. if (repoPath === undefined) return undefined;
  1028. Logger.log(`getStashList('${repoPath}')`);
  1029. const data = await Git.stash_list(repoPath);
  1030. const stash = GitStashParser.parse(data, repoPath);
  1031. return stash;
  1032. }
  1033. async getStatusForFile(repoPath: string, fileName: string): Promise<GitStatusFile | undefined> {
  1034. Logger.log(`getStatusForFile('${repoPath}', '${fileName}')`);
  1035. const porcelainVersion = Git.validateVersion(2, 11) ? 2 : 1;
  1036. const data = await Git.status_file(repoPath, fileName, porcelainVersion);
  1037. const status = GitStatusParser.parse(data, repoPath, porcelainVersion);
  1038. if (status === undefined || !status.files.length) return undefined;
  1039. return status.files[0];
  1040. }
  1041. async getStatusForRepo(repoPath: string | undefined): Promise<GitStatus | undefined> {
  1042. if (repoPath === undefined) return undefined;
  1043. Logger.log(`getStatusForRepo('${repoPath}')`);
  1044. const porcelainVersion = Git.validateVersion(2, 11) ? 2 : 1;
  1045. const data = await Git.status(repoPath, porcelainVersion);
  1046. const status = GitStatusParser.parse(data, repoPath, porcelainVersion);
  1047. return status;
  1048. }
  1049. async getTags(repoPath: string | undefined): Promise<GitTag[]> {
  1050. if (repoPath === undefined) return [];
  1051. Logger.log(`getTags('${repoPath}')`);
  1052. const data = await Git.tag(repoPath);
  1053. return GitTagParser.parse(data, repoPath) || [];
  1054. }
  1055. async getVersionedFile(repoPath: string | undefined, fileName: string, sha: string | undefined) {
  1056. Logger.log(`getVersionedFile('${repoPath}', '${fileName}', '${sha}')`);
  1057. if (!sha || (Git.isUncommitted(sha) && !Git.isStagedUncommitted(sha))) {
  1058. if (await this.fileExists(repoPath!, fileName)) return fileName;
  1059. return undefined;
  1060. }
  1061. const file = await Git.getVersionedFile(repoPath, fileName, sha);
  1062. if (file === undefined) return undefined;
  1063. const cacheKey = this.getCacheEntryKey(file);
  1064. const entry = new UriCacheEntry(new GitUri(Uri.file(fileName), { sha: sha, repoPath: repoPath! }));
  1065. this._versionedUriCache.set(cacheKey, entry);
  1066. return file;
  1067. }
  1068. getVersionedFileText(repoPath: string, fileName: string, sha: string) {
  1069. Logger.log(`getVersionedFileText('${repoPath}', '${fileName}', ${sha})`);
  1070. return Git.show(repoPath, fileName, sha, { encoding: GitService.getEncoding(repoPath, fileName) });
  1071. }
  1072. hasGitUriForFile(editor: TextEditor): boolean {
  1073. if (editor === undefined || editor.document === undefined || editor.document.uri === undefined) return false;
  1074. const cacheKey = this.getCacheEntryKey(editor.document.uri);
  1075. return this._versionedUriCache.has(cacheKey);
  1076. }
  1077. isEditorBlameable(editor: TextEditor): boolean {
  1078. return (editor.viewColumn !== undefined || this.isTrackable(editor.document.uri) || this.hasGitUriForFile(editor));
  1079. }
  1080. isTrackable(scheme: string): boolean;
  1081. isTrackable(uri: Uri): boolean;
  1082. isTrackable(schemeOruri: string | Uri): boolean {
  1083. let scheme: string;
  1084. if (typeof schemeOruri === 'string') {
  1085. scheme = schemeOruri;
  1086. }
  1087. else {
  1088. scheme = schemeOruri.scheme;
  1089. }
  1090. return scheme === DocumentSchemes.File || scheme === DocumentSchemes.Git || scheme === DocumentSchemes.GitLensGit;
  1091. }
  1092. async isTracked(fileName: string, repoPath?: string, sha?: string): Promise<boolean>;
  1093. async isTracked(uri: GitUri): Promise<boolean>;
  1094. async isTracked(fileNameOrUri: string | GitUri, repoPath?: string, sha?: string): Promise<boolean> {
  1095. if (sha === GitService.deletedSha) return false;
  1096. let cacheKey: string;
  1097. let fileName: string;
  1098. if (typeof fileNameOrUri === 'string') {
  1099. [fileName, repoPath] = Git.splitPath(fileNameOrUri, repoPath);
  1100. cacheKey = this.getCacheEntryKey(fileNameOrUri);
  1101. }
  1102. else {
  1103. if (!this.isTrackable(fileNameOrUri)) return false;
  1104. fileName = fileNameOrUri.fsPath;
  1105. repoPath = fileNameOrUri.repoPath;
  1106. sha = fileNameOrUri.sha;
  1107. cacheKey = this.getCacheEntryKey(fileName);
  1108. }
  1109. if (sha !== undefined) {
  1110. cacheKey += `:${sha}`;
  1111. }
  1112. Logger.log(`isTracked('${fileName}', '${repoPath}', '${sha}')`);
  1113. let tracked = this._trackedCache.get(cacheKey);
  1114. if (tracked !== undefined) {
  1115. if (typeof tracked === 'boolean') return tracked;
  1116. return await tracked;
  1117. }
  1118. tracked = this.isTrackedCore(repoPath === undefined ? '' : repoPath, fileName, sha);
  1119. this._trackedCache.set(cacheKey, tracked);
  1120. tracked = await tracked;
  1121. this._trackedCache.set(cacheKey, tracked);
  1122. return tracked;
  1123. }
  1124. private async isTrackedCore(repoPath: string, fileName: string, sha?: string) {
  1125. if (sha === GitService.deletedSha) return false;
  1126. try {
  1127. // Even if we have a sha, check first to see if the file exists (that way the cache will be better reused)
  1128. let tracked = !!await Git.ls_files(repoPath === undefined ? '' : repoPath, fileName);
  1129. if (!tracked && sha !== undefined) {
  1130. tracked = !!await Git.ls_files(repoPath === undefined ? '' : repoPath, fileName, { ref: sha });
  1131. // If we still haven't found this file, make sure it wasn't deleted in that sha (i.e. check the previous)
  1132. if (!tracked) {
  1133. tracked = !!await Git.ls_files(repoPath === undefined ? '' : repoPath, fileName, { ref: `${sha}^` });
  1134. }
  1135. }
  1136. return tracked;
  1137. }
  1138. catch (ex) {
  1139. Logger.error(ex, 'GitService.isTrackedCore');
  1140. return false;
  1141. }
  1142. }
  1143. async getDiffTool(repoPath?: string) {
  1144. return await Git.config_get('diff.guitool', repoPath) || await Git.config_get('diff.tool', repoPath);
  1145. }
  1146. async openDiffTool(repoPath: string, uri: Uri, staged: boolean, tool?: string) {
  1147. if (!tool) {
  1148. tool = await this.getDiffTool(repoPath);
  1149. if (tool === undefined) throw new Error('No diff tool found');
  1150. }
  1151. Logger.log(`openDiffTool('${repoPath}', '${uri.fsPath}', ${staged}, '${tool}')`);
  1152. return Git.difftool_fileDiff(repoPath, uri.fsPath, tool, staged);
  1153. }
  1154. async openDirectoryDiff(repoPath: string, ref1: string, ref2?: string, tool?: string) {
  1155. if (!tool) {
  1156. tool = await this.getDiffTool(repoPath);
  1157. if (tool === undefined) throw new Error('No diff tool found');
  1158. }
  1159. Logger.log(`openDirectoryDiff('${repoPath}', '${ref1}', '${ref2}', '${tool}')`);
  1160. return Git.difftool_dirDiff(repoPath, tool, ref1, ref2);
  1161. }
  1162. async resolveReference(repoPath: string, ref: string, uri?: Uri) {
  1163. if (!GitService.isResolveRequired(ref)) return ref;
  1164. Logger.log(`resolveReference('${repoPath}', '${ref}', '${uri && uri.toString()}')`);
  1165. if (uri === undefined) return (await Git.revparse(repoPath, ref)) || ref;
  1166. return (await Git.log_resolve(repoPath, Git.normalizePath(path.relative(repoPath, uri.fsPath)), ref)) || ref;
  1167. }
  1168. stopWatchingFileSystem() {
  1169. this._repositoryTree.forEach(r => r.stopWatchingFileSystem());
  1170. }
  1171. stashApply(repoPath: string, stashName: string, deleteAfter: boolean = false) {
  1172. Logger.log(`stashApply('${repoPath}', '${stashName}', ${deleteAfter})`);
  1173. return Git.stash_apply(repoPath, stashName, deleteAfter);
  1174. }
  1175. stashDelete(repoPath: string, stashName: string) {
  1176. Logger.log(`stashDelete('${repoPath}', '${stashName}')`);
  1177. return Git.stash_delete(repoPath, stashName);
  1178. }
  1179. stashSave(repoPath: string, message?: string, uris?: Uri[]) {
  1180. Logger.log(`stashSave('${repoPath}', '${message}', ${uris})`);
  1181. if (uris === undefined) return Git.stash_save(repoPath, message);
  1182. const pathspecs = uris.map(u => Git.splitPath(u.fsPath, repoPath)[0]);
  1183. return Git.stash_push(repoPath, pathspecs, message);
  1184. }
  1185. static getEncoding(repoPath: string, fileName: string): string;
  1186. static getEncoding(uri: Uri): string;
  1187. static getEncoding(repoPathOrUri: string | Uri, fileName?: string): string {
  1188. const uri = (typeof repoPathOrUri === 'string')
  1189. ? Uri.file(path.join(repoPathOrUri, fileName!))
  1190. : repoPathOrUri;
  1191. return Git.getEncoding(workspace.getConfiguration('files', uri).get<string>('encoding'));
  1192. }
  1193. static initialize(gitPath?: string): Promise<IGit> {
  1194. return Git.getGitInfo(gitPath);
  1195. }
  1196. static getGitPath(): string {
  1197. return Git.gitInfo().path;
  1198. }
  1199. static getGitVersion(): string {
  1200. return Git.gitInfo().version;
  1201. }
  1202. static isResolveRequired(sha: string): boolean {
  1203. return Git.isResolveRequired(sha);
  1204. }
  1205. static isSha(sha: string): boolean {
  1206. return Git.isSha(sha);
  1207. }
  1208. static isStagedUncommitted(sha: string | undefined): boolean {
  1209. return Git.isStagedUncommitted(sha);
  1210. }
  1211. static isUncommitted(sha: string | undefined): boolean {
  1212. return Git.isUncommitted(sha);
  1213. }
  1214. static normalizePath(fileName: string): string {
  1215. return Git.normalizePath(fileName);
  1216. }
  1217. static shortenSha(sha: string | undefined) {
  1218. if (sha === undefined) return undefined;
  1219. if (sha === GitService.deletedSha) return '(deleted)';
  1220. return Git.isSha(sha) || Git.isStagedUncommitted(sha)
  1221. ? Git.shortenSha(sha)
  1222. : sha;
  1223. }
  1224. static validateGitVersion(major: number, minor: number): boolean {
  1225. const [gitMajor, gitMinor] = this.getGitVersion().split('.');
  1226. return (parseInt(gitMajor, 10) >= major && parseInt(gitMinor, 10) >= minor);
  1227. }
  1228. }