選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

821 行
32 KiB

8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
8年前
  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. }