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.

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