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.

844 lines
33 KiB

8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
  1. 'use strict';
  2. import { Iterables, Objects } from './system';
  3. import { Disposable, Event, EventEmitter, ExtensionContext, FileSystemWatcher, languages, Location, Position, Range, TextDocument, TextEditor, Uri, workspace } from 'vscode';
  4. import { CommandContext, setCommandContext } from './commands';
  5. import { CodeLensVisibility, IConfig } from './configuration';
  6. import { DocumentSchemes } from './constants';
  7. import { Git, GitBlameParser, GitBranch, GitCommit, GitLogCommit, GitLogParser, GitRemote, GitStashParser, GitStatusFile, GitStatusParser, IGitAuthor, IGitBlame, IGitBlameLine, IGitBlameLines, IGitLog, IGitStash, IGitStatus } from './git/git';
  8. import { IGitUriData, GitUri } from './git/gitUri';
  9. import { GitCodeLensProvider } from './gitCodeLensProvider';
  10. import { Logger } from './logger';
  11. import * as fs from 'fs';
  12. import * as ignore from 'ignore';
  13. import * as moment from 'moment';
  14. import * as path from 'path';
  15. export { getGitStatusIcon } from './git/git';
  16. export { Git, GitUri };
  17. export * from './git/git';
  18. class UriCacheEntry {
  19. constructor(public uri: GitUri) { }
  20. }
  21. class GitCacheEntry {
  22. blame?: ICachedBlame;
  23. log?: ICachedLog;
  24. get hasErrors() {
  25. return !!((this.blame && this.blame.errorMessage) || (this.log && this.log.errorMessage));
  26. }
  27. constructor(public key: string) { }
  28. }
  29. interface ICachedItem<T> {
  30. //date: Date;
  31. item: Promise<T>;
  32. errorMessage?: string;
  33. }
  34. interface ICachedBlame extends ICachedItem<IGitBlame> { }
  35. interface ICachedLog extends ICachedItem<IGitLog> { }
  36. enum RemoveCacheReason {
  37. DocumentClosed,
  38. DocumentSaved
  39. }
  40. export class GitService extends Disposable {
  41. private _onDidChangeGitCacheEmitter = new EventEmitter<void>();
  42. get onDidChangeGitCache(): Event<void> {
  43. return this._onDidChangeGitCacheEmitter.event;
  44. }
  45. private _onDidBlameFailEmitter = new EventEmitter<string>();
  46. get onDidBlameFail(): Event<string> {
  47. return this._onDidBlameFailEmitter.event;
  48. }
  49. private _gitCache: Map<string, GitCacheEntry> | undefined;
  50. private _remotesCache: GitRemote[];
  51. private _cacheDisposable: Disposable | undefined;
  52. private _uriCache: Map<string, UriCacheEntry> | undefined;
  53. config: IConfig;
  54. private _codeLensProvider: GitCodeLensProvider | undefined;
  55. private _codeLensProviderDisposable: Disposable | undefined;
  56. private _disposable: Disposable;
  57. private _fsWatcher: FileSystemWatcher;
  58. private _gitignore: Promise<ignore.Ignore>;
  59. static EmptyPromise: Promise<IGitBlame | IGitLog> = Promise.resolve(undefined);
  60. constructor(private context: ExtensionContext, public repoPath: string) {
  61. super(() => this.dispose());
  62. this._uriCache = new Map();
  63. this._onConfigurationChanged();
  64. const subscriptions: Disposable[] = [];
  65. subscriptions.push(workspace.onDidChangeConfiguration(this._onConfigurationChanged, this));
  66. this._disposable = Disposable.from(...subscriptions);
  67. }
  68. dispose() {
  69. this._disposable && this._disposable.dispose();
  70. this._codeLensProviderDisposable && this._codeLensProviderDisposable.dispose();
  71. this._codeLensProviderDisposable = undefined;
  72. this._codeLensProvider = undefined;
  73. this._cacheDisposable && this._cacheDisposable.dispose();
  74. this._cacheDisposable = undefined;
  75. this._fsWatcher && this._fsWatcher.dispose();
  76. this._fsWatcher = undefined;
  77. this._gitCache && this._gitCache.clear();
  78. this._gitCache = undefined;
  79. this._uriCache && this._uriCache.clear();
  80. this._uriCache = undefined;
  81. }
  82. public get UseGitCaching() {
  83. return !!this._gitCache;
  84. }
  85. private _onConfigurationChanged() {
  86. const config = workspace.getConfiguration().get<IConfig>('gitlens');
  87. const codeLensChanged = !Objects.areEquivalent(config.codeLens, this.config && this.config.codeLens);
  88. const advancedChanged = !Objects.areEquivalent(config.advanced, this.config && this.config.advanced);
  89. if (codeLensChanged || advancedChanged) {
  90. Logger.log('CodeLens config changed; resetting CodeLens provider');
  91. if (config.codeLens.visibility === CodeLensVisibility.Auto && (config.codeLens.recentChange.enabled || config.codeLens.authors.enabled)) {
  92. if (this._codeLensProvider) {
  93. this._codeLensProvider.reset();
  94. }
  95. else {
  96. this._codeLensProvider = new GitCodeLensProvider(this.context, this);
  97. this._codeLensProviderDisposable = languages.registerCodeLensProvider(GitCodeLensProvider.selector, this._codeLensProvider);
  98. }
  99. }
  100. else {
  101. this._codeLensProviderDisposable && this._codeLensProviderDisposable.dispose();
  102. this._codeLensProviderDisposable = undefined;
  103. this._codeLensProvider = undefined;
  104. }
  105. setCommandContext(CommandContext.CanToggleCodeLens, config.codeLens.visibility === CodeLensVisibility.OnDemand && (config.codeLens.recentChange.enabled || config.codeLens.authors.enabled));
  106. }
  107. if (advancedChanged) {
  108. if (config.advanced.caching.enabled) {
  109. this._gitCache = new Map();
  110. this._cacheDisposable && this._cacheDisposable.dispose();
  111. this._fsWatcher = this._fsWatcher || workspace.createFileSystemWatcher('**/.git/index', true, false, true);
  112. const disposables: Disposable[] = [];
  113. disposables.push(workspace.onDidCloseTextDocument(d => this._removeCachedEntry(d, RemoveCacheReason.DocumentClosed)));
  114. disposables.push(workspace.onDidSaveTextDocument(d => this._removeCachedEntry(d, RemoveCacheReason.DocumentSaved)));
  115. disposables.push(this._fsWatcher.onDidChange(this._onGitChanged, this));
  116. this._cacheDisposable = Disposable.from(...disposables);
  117. }
  118. else {
  119. this._cacheDisposable && this._cacheDisposable.dispose();
  120. this._cacheDisposable = undefined;
  121. this._fsWatcher && this._fsWatcher.dispose();
  122. this._fsWatcher = undefined;
  123. this._gitCache && this._gitCache.clear();
  124. this._gitCache = undefined;
  125. }
  126. this._gitignore = new Promise<ignore.Ignore | undefined>((resolve, reject) => {
  127. if (!config.advanced.gitignore.enabled) {
  128. resolve(undefined);
  129. return;
  130. }
  131. const gitignorePath = path.join(this.repoPath, '.gitignore');
  132. fs.exists(gitignorePath, e => {
  133. if (e) {
  134. fs.readFile(gitignorePath, 'utf8', (err, data) => {
  135. if (!err) {
  136. resolve(ignore().add(data));
  137. return;
  138. }
  139. resolve(undefined);
  140. });
  141. return;
  142. }
  143. resolve(undefined);
  144. });
  145. });
  146. }
  147. this.config = config;
  148. }
  149. private _onGitChanged() {
  150. this._gitCache && this._gitCache.clear();
  151. this._onDidChangeGitCacheEmitter.fire();
  152. this._codeLensProvider && this._codeLensProvider.reset();
  153. }
  154. private _removeCachedEntry(document: TextDocument, reason: RemoveCacheReason) {
  155. if (!this.UseGitCaching) return;
  156. if (document.uri.scheme !== DocumentSchemes.File) return;
  157. const cacheKey = this.getCacheEntryKey(document.fileName);
  158. if (reason === RemoveCacheReason.DocumentSaved) {
  159. // Don't remove broken blame on save (since otherwise we'll have to run the broken blame again)
  160. const entry = this._gitCache.get(cacheKey);
  161. if (entry && entry.hasErrors) return;
  162. }
  163. if (this._gitCache.delete(cacheKey)) {
  164. Logger.log(`Clear cache entry for '${cacheKey}', reason=${RemoveCacheReason[reason]}`);
  165. if (reason === RemoveCacheReason.DocumentSaved) {
  166. this._onDidChangeGitCacheEmitter.fire();
  167. // Refresh the codelenses with the updated blame
  168. this._codeLensProvider && this._codeLensProvider.reset();
  169. }
  170. }
  171. }
  172. private async _fileExists(repoPath: string, fileName: string): Promise<boolean> {
  173. return await new Promise<boolean>((resolve, reject) => fs.exists(path.resolve(repoPath, fileName), e => resolve(e)));
  174. }
  175. async findNextCommit(repoPath: string, fileName: string, sha?: string): Promise<GitLogCommit> {
  176. let log = await this.getLogForFile(repoPath, fileName, sha, 1, undefined, true);
  177. let commit = log && Iterables.first(log.commits.values());
  178. if (commit) return commit;
  179. fileName = await this.findNextFileName(repoPath, fileName, sha);
  180. if (fileName) {
  181. log = await this.getLogForFile(repoPath, fileName, sha, 1, undefined, true);
  182. commit = log && Iterables.first(log.commits.values());
  183. }
  184. return commit;
  185. }
  186. async findNextFileName(repoPath: string, fileName: string, sha?: string): Promise<string> {
  187. [fileName, repoPath] = Git.splitPath(fileName, repoPath);
  188. return (await this._fileExists(repoPath, fileName))
  189. ? fileName
  190. : await this._findNextFileName(repoPath, fileName, sha);
  191. }
  192. async _findNextFileName(repoPath: string, fileName: string, sha?: string): Promise<string> {
  193. if (sha === undefined) {
  194. // Get the most recent commit for this file name
  195. const c = await this.getLogCommit(repoPath, fileName);
  196. if (!c) return undefined;
  197. sha = c.sha;
  198. }
  199. // Get the full commit (so we can see if there are any matching renames in the file statuses)
  200. const log = await this.getLogForRepo(repoPath, sha, 1);
  201. if (!log) return undefined;
  202. const c = Iterables.first(log.commits.values());
  203. const status = c.fileStatuses.find(_ => _.originalFileName === fileName);
  204. if (!status) return undefined;
  205. return status.fileName;
  206. }
  207. async findWorkingFileName(commit: GitCommit): Promise<string>;
  208. async findWorkingFileName(repoPath: string, fileName: string): Promise<string>;
  209. async findWorkingFileName(commitOrRepoPath: GitCommit | string, fileName?: string): Promise<string> {
  210. let repoPath: string;
  211. if (typeof commitOrRepoPath === 'string') {
  212. repoPath = commitOrRepoPath;
  213. [fileName] = Git.splitPath(fileName, repoPath);
  214. }
  215. else {
  216. const c = commitOrRepoPath;
  217. repoPath = c.repoPath;
  218. if (c.workingFileName && await this._fileExists(repoPath, c.workingFileName)) return c.workingFileName;
  219. fileName = c.fileName;
  220. }
  221. while (true) {
  222. if (await this._fileExists(repoPath, fileName)) return fileName;
  223. fileName = await this._findNextFileName(repoPath, fileName);
  224. if (fileName === undefined) return undefined;
  225. }
  226. }
  227. public getBlameability(fileName: string): boolean {
  228. if (!this.UseGitCaching) return true;
  229. const cacheKey = this.getCacheEntryKey(fileName);
  230. const entry = this._gitCache.get(cacheKey);
  231. return !(entry && entry.hasErrors);
  232. }
  233. async getBlameForFile(uri: GitUri): Promise<IGitBlame | undefined> {
  234. Logger.log(`getBlameForFile('${uri.repoPath}', '${uri.fsPath}', ${uri.sha})`);
  235. const fileName = uri.fsPath;
  236. let entry: GitCacheEntry | undefined;
  237. if (this.UseGitCaching && !uri.sha) {
  238. const cacheKey = this.getCacheEntryKey(fileName);
  239. entry = this._gitCache.get(cacheKey);
  240. if (entry !== undefined && entry.blame !== undefined) return entry.blame.item;
  241. if (entry === undefined) {
  242. entry = new GitCacheEntry(cacheKey);
  243. }
  244. }
  245. const promise = this._getBlameForFile(uri, fileName, entry);
  246. if (entry) {
  247. Logger.log(`Add blame cache for '${entry.key}'`);
  248. entry.blame = {
  249. //date: new Date(),
  250. item: promise
  251. } as ICachedBlame;
  252. this._gitCache.set(entry.key, entry);
  253. }
  254. return promise;
  255. }
  256. private async _getBlameForFile(uri: GitUri, fileName: string, entry: GitCacheEntry | undefined): Promise<IGitBlame> {
  257. const [file, root] = Git.splitPath(fileName, uri.repoPath, false);
  258. const ignore = await this._gitignore;
  259. if (ignore && !ignore.filter([file]).length) {
  260. Logger.log(`Skipping blame; '${fileName}' is gitignored`);
  261. if (entry && entry.key) {
  262. this._onDidBlameFailEmitter.fire(entry.key);
  263. }
  264. return await GitService.EmptyPromise as IGitBlame;
  265. }
  266. try {
  267. const data = await Git.blame(root, file, uri.sha);
  268. return GitBlameParser.parse(data, root, file);
  269. }
  270. catch (ex) {
  271. // Trap and cache expected blame errors
  272. if (entry) {
  273. const msg = ex && ex.toString();
  274. Logger.log(`Replace blame cache with empty promise for '${entry.key}'`);
  275. entry.blame = {
  276. //date: new Date(),
  277. item: GitService.EmptyPromise,
  278. errorMessage: msg
  279. } as ICachedBlame;
  280. this._onDidBlameFailEmitter.fire(entry.key);
  281. this._gitCache.set(entry.key, entry);
  282. return await GitService.EmptyPromise as IGitBlame;
  283. }
  284. return undefined;
  285. }
  286. };
  287. async getBlameForLine(uri: GitUri, line: number): Promise<IGitBlameLine | undefined> {
  288. Logger.log(`getBlameForLine('${uri.repoPath}', '${uri.fsPath}', ${line}, ${uri.sha})`);
  289. if (this.UseGitCaching && !uri.sha) {
  290. const blame = await this.getBlameForFile(uri);
  291. const blameLine = blame && blame.lines[line];
  292. if (!blameLine) return undefined;
  293. const commit = blame.commits.get(blameLine.sha);
  294. return {
  295. author: Object.assign({}, blame.authors.get(commit.author), { lineCount: commit.lines.length }),
  296. commit: commit,
  297. line: blameLine
  298. } as IGitBlameLine;
  299. }
  300. const fileName = uri.fsPath;
  301. try {
  302. const data = await Git.blame(uri.repoPath, fileName, uri.sha, line + 1, line + 1);
  303. const blame = GitBlameParser.parse(data, uri.repoPath, fileName);
  304. if (!blame) return undefined;
  305. const commit = Iterables.first(blame.commits.values());
  306. if (uri.repoPath) {
  307. commit.repoPath = uri.repoPath;
  308. }
  309. return {
  310. author: Iterables.first(blame.authors.values()),
  311. commit: commit,
  312. line: blame.lines[line]
  313. } as IGitBlameLine;
  314. }
  315. catch (ex) {
  316. return undefined;
  317. }
  318. }
  319. async getBlameForRange(uri: GitUri, range: Range): Promise<IGitBlameLines | undefined> {
  320. Logger.log(`getBlameForRange('${uri.repoPath}', '${uri.fsPath}', [${range.start.line}, ${range.end.line}], ${uri.sha})`);
  321. const blame = await this.getBlameForFile(uri);
  322. if (!blame) return undefined;
  323. return this.getBlameForRangeSync(blame, uri, range);
  324. }
  325. getBlameForRangeSync(blame: IGitBlame, uri: GitUri, range: Range): IGitBlameLines | undefined {
  326. Logger.log(`getBlameForRangeSync('${uri.repoPath}', '${uri.fsPath}', [${range.start.line}, ${range.end.line}], ${uri.sha})`);
  327. if (!blame.lines.length) return Object.assign({ allLines: blame.lines }, blame);
  328. if (range.start.line === 0 && range.end.line === blame.lines.length - 1) {
  329. return Object.assign({ allLines: blame.lines }, blame);
  330. }
  331. const lines = blame.lines.slice(range.start.line, range.end.line + 1);
  332. const shas: Set<string> = new Set();
  333. lines.forEach(l => shas.add(l.sha));
  334. const authors: Map<string, IGitAuthor> = new Map();
  335. const commits: Map<string, GitCommit> = new Map();
  336. blame.commits.forEach(c => {
  337. if (!shas.has(c.sha)) return;
  338. const commit: GitCommit = new GitCommit('blame', c.repoPath, c.sha, c.fileName, c.author, c.date, c.message,
  339. c.lines.filter(l => l.line >= range.start.line && l.line <= range.end.line), c.originalFileName, c.previousSha, c.previousFileName);
  340. commits.set(c.sha, commit);
  341. let author = authors.get(commit.author);
  342. if (!author) {
  343. author = {
  344. name: commit.author,
  345. lineCount: 0
  346. };
  347. authors.set(author.name, author);
  348. }
  349. author.lineCount += commit.lines.length;
  350. });
  351. const sortedAuthors: Map<string, IGitAuthor> = new Map();
  352. Array.from(authors.values())
  353. .sort((a, b) => b.lineCount - a.lineCount)
  354. .forEach(a => sortedAuthors.set(a.name, a));
  355. return {
  356. authors: sortedAuthors,
  357. commits: commits,
  358. lines: lines,
  359. allLines: blame.lines
  360. } as IGitBlameLines;
  361. }
  362. async getBlameLocations(uri: GitUri, range: Range, selectedSha?: string, line?: number): Promise<Location[] | undefined> {
  363. Logger.log(`getBlameLocations('${uri.repoPath}', '${uri.fsPath}', [${range.start.line}, ${range.end.line}], ${uri.sha})`);
  364. const blame = await this.getBlameForRange(uri, range);
  365. if (!blame) return undefined;
  366. const commitCount = blame.commits.size;
  367. const locations: Array<Location> = [];
  368. Iterables.forEach(blame.commits.values(), (c, i) => {
  369. if (c.isUncommitted) return;
  370. const decoration = `\u2937 ${c.author}, ${moment(c.date).format('MMMM Do, YYYY h:MMa')}`;
  371. const uri = GitService.toReferenceGitContentUri(c, i + 1, commitCount, c.originalFileName, decoration);
  372. locations.push(new Location(uri, new Position(0, 0)));
  373. if (c.sha === selectedSha) {
  374. locations.push(new Location(uri, new Position(line + 1, 0)));
  375. }
  376. });
  377. return locations;
  378. }
  379. async getBranch(repoPath: string): Promise<GitBranch> {
  380. Logger.log(`getBranch('${repoPath}')`);
  381. const data = await Git.branch(repoPath, false);
  382. const branches = data.split('\n').filter(_ => !!_).map(_ => new GitBranch(_));
  383. return branches.find(_ => _.current);
  384. }
  385. async getBranches(repoPath: string): Promise<GitBranch[]> {
  386. Logger.log(`getBranches('${repoPath}')`);
  387. const data = await Git.branch(repoPath, true);
  388. const branches = data.split('\n').filter(_ => !!_).map(_ => new GitBranch(_));
  389. return branches;
  390. }
  391. getCacheEntryKey(fileName: string) {
  392. return Git.normalizePath(fileName).toLowerCase();
  393. }
  394. getGitUriForFile(fileName: string) {
  395. const cacheKey = this.getCacheEntryKey(fileName);
  396. const entry = this._uriCache.get(cacheKey);
  397. return entry && entry.uri;
  398. }
  399. async getLogCommit(repoPath: string, fileName: string, options?: { firstIfMissing?: boolean, previous?: boolean }): Promise<GitLogCommit | undefined>;
  400. async getLogCommit(repoPath: string, fileName: string, sha: string, options?: { firstIfMissing?: boolean, previous?: boolean }): Promise<GitLogCommit | undefined>;
  401. async getLogCommit(repoPath: string, fileName: string, shaOrOptions?: string | { firstIfMissing?: boolean, previous?: boolean }, options?: { firstIfMissing?: boolean, previous?: boolean }): Promise<GitLogCommit | undefined> {
  402. let sha: string;
  403. if (typeof shaOrOptions === 'string') {
  404. sha = shaOrOptions;
  405. }
  406. else if (!options) {
  407. options = shaOrOptions;
  408. }
  409. options = options || {};
  410. const log = await this.getLogForFile(repoPath, fileName, sha, options.previous ? 2 : 1);
  411. if (!log) return undefined;
  412. const commit = sha && log.commits.get(sha);
  413. if (!commit && sha && !options.firstIfMissing) return undefined;
  414. return commit || Iterables.first(log.commits.values());
  415. }
  416. async getLogForRepo(repoPath: string, sha?: string, maxCount?: number, reverse: boolean = false): Promise<IGitLog | undefined> {
  417. Logger.log(`getLogForRepo('${repoPath}', ${sha}, ${maxCount})`);
  418. if (maxCount == null) {
  419. maxCount = this.config.advanced.maxQuickHistory || 0;
  420. }
  421. try {
  422. const data = await Git.log(repoPath, sha, maxCount, reverse);
  423. return GitLogParser.parse(data, 'branch', repoPath, undefined, sha, maxCount, reverse, undefined);
  424. }
  425. catch (ex) {
  426. return undefined;
  427. }
  428. }
  429. getLogForFile(repoPath: string, fileName: string, sha?: string, maxCount?: number, range?: Range, reverse: boolean = false): Promise<IGitLog | undefined> {
  430. Logger.log(`getLogForFile('${repoPath}', '${fileName}', ${sha}, ${maxCount}, ${range && `[${range.start.line}, ${range.end.line}]`}, ${reverse})`);
  431. let entry: GitCacheEntry | undefined;
  432. if (this.UseGitCaching && !sha && !range && !maxCount && !reverse) {
  433. const cacheKey = this.getCacheEntryKey(fileName);
  434. entry = this._gitCache.get(cacheKey);
  435. if (entry !== undefined && entry.log !== undefined) return entry.log.item;
  436. if (entry === undefined) {
  437. entry = new GitCacheEntry(cacheKey);
  438. }
  439. }
  440. const promise = this._getLogForFile(repoPath, fileName, sha, range, maxCount, reverse, entry);
  441. if (entry) {
  442. Logger.log(`Add log cache for '${entry.key}'`);
  443. entry.log = {
  444. //date: new Date(),
  445. item: promise
  446. } as ICachedLog;
  447. this._gitCache.set(entry.key, entry);
  448. }
  449. return promise;
  450. }
  451. private async _getLogForFile(repoPath: string, fileName: string, sha: string, range: Range, maxCount: number, reverse: boolean, entry: GitCacheEntry | undefined): Promise<IGitLog> {
  452. const [file, root] = Git.splitPath(fileName, repoPath, false);
  453. const ignore = await this._gitignore;
  454. if (ignore && !ignore.filter([file]).length) {
  455. Logger.log(`Skipping log; '${fileName}' is gitignored`);
  456. return await GitService.EmptyPromise as IGitLog;
  457. }
  458. try {
  459. const data = await Git.log_file(root, file, sha, maxCount, reverse, range && range.start.line + 1, range && range.end.line + 1);
  460. return GitLogParser.parse(data, 'file', root, file, sha, maxCount, reverse, range);
  461. }
  462. catch (ex) {
  463. // Trap and cache expected log errors
  464. if (entry) {
  465. const msg = ex && ex.toString();
  466. Logger.log(`Replace log cache with empty promise for '${entry.key}'`);
  467. entry.log = {
  468. //date: new Date(),
  469. item: GitService.EmptyPromise,
  470. errorMessage: msg
  471. } as ICachedLog;
  472. this._gitCache.set(entry.key, entry);
  473. return await GitService.EmptyPromise as IGitLog;
  474. }
  475. return undefined;
  476. }
  477. };
  478. async getLogLocations(uri: GitUri, selectedSha?: string, line?: number): Promise<Location[] | undefined> {
  479. Logger.log(`getLogLocations('${uri.repoPath}', '${uri.fsPath}', ${uri.sha}, ${selectedSha}, ${line})`);
  480. const log = await this.getLogForFile(uri.repoPath, uri.fsPath, uri.sha);
  481. if (!log) return undefined;
  482. const commitCount = log.commits.size;
  483. const locations: Array<Location> = [];
  484. Iterables.forEach(log.commits.values(), (c, i) => {
  485. if (c.isUncommitted) return;
  486. const decoration = `\u2937 ${c.author}, ${moment(c.date).format('MMMM Do, YYYY h:MMa')}`;
  487. const uri = GitService.toReferenceGitContentUri(c, i + 1, commitCount, c.originalFileName, decoration);
  488. locations.push(new Location(uri, new Position(0, 0)));
  489. if (c.sha === selectedSha) {
  490. locations.push(new Location(uri, new Position(line + 1, 0)));
  491. }
  492. });
  493. return locations;
  494. }
  495. async getRemotes(repoPath: string): Promise<GitRemote[]> {
  496. if (!this.config.insiders) return Promise.resolve([]);
  497. Logger.log(`getRemotes('${repoPath}')`);
  498. if (this.UseGitCaching && this._remotesCache) return this._remotesCache;
  499. const data = await Git.remote(repoPath);
  500. const remotes = data.split('\n').filter(_ => !!_).map(_ => new GitRemote(_));
  501. if (this.UseGitCaching) {
  502. this._remotesCache = remotes;
  503. }
  504. return remotes;
  505. }
  506. getRepoPath(cwd: string): Promise<string> {
  507. return Git.getRepoPath(cwd);
  508. }
  509. async getRepoPathFromFile(fileName: string): Promise<string | undefined> {
  510. const log = await this.getLogForFile(undefined, fileName, undefined, 1);
  511. return log && log.repoPath;
  512. }
  513. async getRepoPathFromUri(uri?: Uri, fallbackRepoPath?: string): Promise<string | undefined> {
  514. if (!(uri instanceof Uri)) return fallbackRepoPath;
  515. const gitUri = await GitUri.fromUri(uri, this);
  516. if (gitUri.repoPath) return gitUri.repoPath;
  517. return (await this.getRepoPathFromFile(gitUri.fsPath)) || fallbackRepoPath;
  518. }
  519. async getStashList(repoPath: string): Promise<IGitStash> {
  520. Logger.log(`getStash('${repoPath}')`);
  521. const data = await Git.stash_list(repoPath);
  522. return GitStashParser.parse(data, repoPath);
  523. }
  524. async getStatusForFile(repoPath: string, fileName: string): Promise<GitStatusFile> {
  525. Logger.log(`getStatusForFile('${repoPath}', '${fileName}')`);
  526. const porcelainVersion = Git.validateVersion(2, 11) ? 2 : 1;
  527. const data = await Git.status_file(repoPath, fileName, porcelainVersion);
  528. const status = GitStatusParser.parse(data, repoPath, porcelainVersion);
  529. return status && status.files.length && status.files[0];
  530. }
  531. async getStatusForRepo(repoPath: string): Promise<IGitStatus> {
  532. Logger.log(`getStatusForRepo('${repoPath}')`);
  533. const porcelainVersion = Git.validateVersion(2, 11) ? 2 : 1;
  534. const data = await Git.status(repoPath, porcelainVersion);
  535. return GitStatusParser.parse(data, repoPath, porcelainVersion);
  536. }
  537. async getVersionedFile(repoPath: string, fileName: string, sha: string) {
  538. Logger.log(`getVersionedFile('${repoPath}', '${fileName}', ${sha})`);
  539. const file = await Git.getVersionedFile(repoPath, fileName, sha);
  540. const cacheKey = this.getCacheEntryKey(file);
  541. const entry = new UriCacheEntry(new GitUri(Uri.file(fileName), { sha, repoPath, fileName }));
  542. this._uriCache.set(cacheKey, entry);
  543. return file;
  544. }
  545. getVersionedFileText(repoPath: string, fileName: string, sha: string) {
  546. Logger.log(`getVersionedFileText('${repoPath}', '${fileName}', ${sha})`);
  547. return Git.show(repoPath, fileName, sha);
  548. }
  549. hasGitUriForFile(editor: TextEditor): boolean;
  550. hasGitUriForFile(fileName: string): boolean;
  551. hasGitUriForFile(fileNameOrEditor: string | TextEditor): boolean {
  552. let fileName: string;
  553. if (typeof fileNameOrEditor === 'string') {
  554. fileName = fileNameOrEditor;
  555. }
  556. else {
  557. if (!fileNameOrEditor || !fileNameOrEditor.document || !fileNameOrEditor.document.uri) return false;
  558. fileName = fileNameOrEditor.document.uri.fsPath;
  559. }
  560. const cacheKey = this.getCacheEntryKey(fileName);
  561. return this._uriCache.has(cacheKey);
  562. }
  563. isEditorBlameable(editor: TextEditor): boolean {
  564. return (editor.viewColumn !== undefined ||
  565. editor.document.uri.scheme === DocumentSchemes.File ||
  566. editor.document.uri.scheme === DocumentSchemes.Git ||
  567. this.hasGitUriForFile(editor));
  568. }
  569. async isFileUncommitted(uri: GitUri): Promise<boolean> {
  570. Logger.log(`isFileUncommitted('${uri.repoPath}', '${uri.fsPath}')`);
  571. const status = await this.getStatusForFile(uri.repoPath, uri.fsPath);
  572. return !!status;
  573. }
  574. openDirectoryDiff(repoPath: string, sha1: string, sha2?: string) {
  575. Logger.log(`openDirectoryDiff('${repoPath}', ${sha1}, ${sha2})`);
  576. return Git.difftool_dirDiff(repoPath, sha1, sha2);
  577. }
  578. stashApply(repoPath: string, stashName: string, deleteAfter: boolean = false) {
  579. Logger.log(`stashApply('${repoPath}', ${stashName}, ${deleteAfter})`);
  580. return Git.stash_apply(repoPath, stashName, deleteAfter);
  581. }
  582. stashDelete(repoPath: string, stashName: string) {
  583. Logger.log(`stashDelete('${repoPath}', ${stashName}})`);
  584. return Git.stash_delete(repoPath, stashName);
  585. }
  586. stashSave(repoPath: string, message?: string, unstagedOnly: boolean = false) {
  587. Logger.log(`stashSave('${repoPath}', ${message}, ${unstagedOnly})`);
  588. return Git.stash_save(repoPath, message, unstagedOnly);
  589. }
  590. toggleCodeLens(editor: TextEditor) {
  591. if (this.config.codeLens.visibility !== CodeLensVisibility.OnDemand ||
  592. (!this.config.codeLens.recentChange.enabled && !this.config.codeLens.authors.enabled)) return;
  593. Logger.log(`toggleCodeLens(${editor})`);
  594. if (this._codeLensProviderDisposable) {
  595. this._codeLensProviderDisposable.dispose();
  596. this._codeLensProviderDisposable = undefined;
  597. return;
  598. }
  599. this._codeLensProviderDisposable = languages.registerCodeLensProvider(GitCodeLensProvider.selector, new GitCodeLensProvider(this.context, this));
  600. }
  601. static fromGitContentUri(uri: Uri): IGitUriData {
  602. if (uri.scheme !== DocumentSchemes.GitLensGit) throw new Error(`fromGitUri(uri=${uri}) invalid scheme`);
  603. return GitService._fromGitContentUri<IGitUriData>(uri);
  604. }
  605. private static _fromGitContentUri<T extends IGitUriData>(uri: Uri): T {
  606. return JSON.parse(uri.query) as T;
  607. }
  608. static isUncommitted(sha: string) {
  609. return Git.isUncommitted(sha);
  610. }
  611. static toGitContentUri(sha: string, shortSha: string, fileName: string, repoPath: string, originalFileName: string): Uri;
  612. static toGitContentUri(commit: GitCommit): Uri;
  613. static toGitContentUri(shaOrcommit: string | GitCommit, shortSha?: string, fileName?: string, repoPath?: string, originalFileName?: string): Uri {
  614. let data: IGitUriData;
  615. if (typeof shaOrcommit === 'string') {
  616. data = GitService._toGitUriData({
  617. sha: shaOrcommit,
  618. fileName: fileName,
  619. repoPath: repoPath,
  620. originalFileName: originalFileName
  621. });
  622. }
  623. else {
  624. data = GitService._toGitUriData(shaOrcommit, undefined, shaOrcommit.originalFileName);
  625. fileName = shaOrcommit.fileName;
  626. shortSha = shaOrcommit.shortSha;
  627. }
  628. const extension = path.extname(fileName);
  629. return Uri.parse(`${DocumentSchemes.GitLensGit}:${path.basename(fileName, extension)}:${shortSha}${extension}?${JSON.stringify(data)}`);
  630. }
  631. static toReferenceGitContentUri(commit: GitCommit, index: number, commitCount: number, originalFileName?: string, decoration?: string): Uri {
  632. return GitService._toReferenceGitContentUri(commit, DocumentSchemes.GitLensGit, commitCount, GitService._toGitUriData(commit, index, originalFileName, decoration));
  633. }
  634. private static _toReferenceGitContentUri(commit: GitCommit, scheme: DocumentSchemes, commitCount: number, data: IGitUriData) {
  635. const pad = (n: number) => ('0000000' + n).slice(-('' + commitCount).length);
  636. const ext = path.extname(data.fileName);
  637. const uriPath = `${path.relative(commit.repoPath, data.fileName.slice(0, -ext.length))}/${commit.shortSha}${ext}`;
  638. let message = commit.message;
  639. if (message.length > 50) {
  640. message = message.substring(0, 49) + '\u2026';
  641. }
  642. // NOTE: Need to specify an index here, since I can't control the sort order -- just alphabetic or by file location
  643. return Uri.parse(`${scheme}:${pad(data.index)} \u2022 ${encodeURIComponent(message)} \u2022 ${moment(commit.date).format('MMM D, YYYY hh:MMa')} \u2022 ${encodeURIComponent(uriPath)}?${JSON.stringify(data)}`);
  644. }
  645. private static _toGitUriData<T extends IGitUriData>(commit: IGitUriData, index?: number, originalFileName?: string, decoration?: string): T {
  646. const fileName = Git.normalizePath(path.resolve(commit.repoPath, commit.fileName));
  647. const data = { repoPath: commit.repoPath, fileName: fileName, sha: commit.sha, index: index } as T;
  648. if (originalFileName) {
  649. data.originalFileName = Git.normalizePath(path.resolve(commit.repoPath, originalFileName));
  650. }
  651. if (decoration) {
  652. data.decoration = decoration;
  653. }
  654. return data;
  655. }
  656. }