25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

358 satır
11 KiB

4 yıl önce
4 yıl önce
4 yıl önce
4 yıl önce
  1. 'use strict';
  2. import { MarkdownString } from 'vscode';
  3. import { DiffWithCommand, ShowQuickCommitCommand } from '../commands';
  4. import { GlyphChars } from '../constants';
  5. import { Container } from '../container';
  6. import {
  7. CommitFormatter,
  8. GitBlameCommit,
  9. GitCommit,
  10. GitDiffHunk,
  11. GitDiffHunkLine,
  12. GitLogCommit,
  13. GitRemote,
  14. GitRevision,
  15. } from '../git/git';
  16. import { GitUri } from '../git/gitUri';
  17. import { Logger, TraceLevel } from '../logger';
  18. import { Iterables, Promises, Strings } from '../system';
  19. export namespace Hovers {
  20. export async function changesMessage(
  21. commit: GitBlameCommit | GitLogCommit,
  22. uri: GitUri,
  23. editorLine: number,
  24. ): Promise<MarkdownString | undefined> {
  25. const documentRef = uri.sha;
  26. let hunkLine;
  27. if (GitBlameCommit.is(commit)) {
  28. // TODO: Figure out how to optimize this
  29. let ref;
  30. if (commit.isUncommitted) {
  31. if (GitRevision.isUncommittedStaged(documentRef)) {
  32. ref = documentRef;
  33. }
  34. } else {
  35. ref = documentRef ? commit.previousSha : commit.sha;
  36. }
  37. const line = editorLine + 1;
  38. const commitLine = commit.lines.find(l => l.line === line) ?? commit.lines[0];
  39. let originalFileName = commit.originalFileName;
  40. if (originalFileName == null) {
  41. if (uri.fsPath !== commit.uri.fsPath) {
  42. originalFileName = commit.fileName;
  43. }
  44. }
  45. editorLine = commitLine.originalLine - 1;
  46. // TODO: Doesn't work with dirty files -- pass in editor? or contents?
  47. hunkLine = await Container.git.getDiffForLine(uri, editorLine, ref, uri.sha, originalFileName);
  48. // If we didn't find a diff & ref is undefined (meaning uncommitted), check for a staged diff
  49. if (hunkLine == null && ref == null) {
  50. hunkLine = await Container.git.getDiffForLine(
  51. uri,
  52. editorLine,
  53. undefined,
  54. GitRevision.uncommittedStaged,
  55. originalFileName,
  56. );
  57. }
  58. }
  59. if (hunkLine == null || commit.previousSha == null) return undefined;
  60. const diff = getDiffFromHunkLine(hunkLine);
  61. let message;
  62. let previous;
  63. let current;
  64. if (commit.isUncommitted) {
  65. const diffUris = await commit.getPreviousLineDiffUris(uri, editorLine, documentRef);
  66. if (diffUris == null || diffUris.previous == null) {
  67. return undefined;
  68. }
  69. message = `[$(compare-changes)](${DiffWithCommand.getMarkdownCommandArgs({
  70. lhs: {
  71. sha: diffUris.previous.sha ?? '',
  72. uri: diffUris.previous.documentUri(),
  73. },
  74. rhs: {
  75. sha: diffUris.current.sha ?? '',
  76. uri: diffUris.current.documentUri(),
  77. },
  78. repoPath: commit.repoPath,
  79. line: editorLine,
  80. })} "Open Changes")`;
  81. previous =
  82. diffUris.previous.sha == null || diffUris.previous.isUncommitted
  83. ? `_${GitRevision.shorten(diffUris.previous.sha, {
  84. strings: {
  85. working: 'Working Tree',
  86. },
  87. })}_`
  88. : `[$(git-commit) ${GitRevision.shorten(
  89. diffUris.previous.sha || '',
  90. )}](${ShowQuickCommitCommand.getMarkdownCommandArgs(diffUris.previous.sha || '')} "Show Commit")`;
  91. current =
  92. diffUris.current.sha == null || diffUris.current.isUncommitted
  93. ? `_${GitRevision.shorten(diffUris.current.sha, {
  94. strings: {
  95. working: 'Working Tree',
  96. },
  97. })}_`
  98. : `[$(git-commit) ${GitRevision.shorten(
  99. diffUris.current.sha || '',
  100. )}](${ShowQuickCommitCommand.getMarkdownCommandArgs(diffUris.current.sha || '')} "Show Commit")`;
  101. } else {
  102. message = `[$(compare-changes)](${DiffWithCommand.getMarkdownCommandArgs(
  103. commit,
  104. editorLine,
  105. )} "Open Changes")`;
  106. previous = `[$(git-commit) ${commit.previousShortSha}](${ShowQuickCommitCommand.getMarkdownCommandArgs(
  107. commit.previousSha,
  108. )} "Show Commit")`;
  109. current = `[$(git-commit) ${commit.shortSha}](${ShowQuickCommitCommand.getMarkdownCommandArgs(
  110. commit.sha,
  111. )} "Show Commit")`;
  112. }
  113. message = `${diff}\n---\n\nChanges &nbsp;${previous} &nbsp;${GlyphChars.ArrowLeftRightLong}&nbsp; ${current} &nbsp;&nbsp;|&nbsp;&nbsp; ${message}`;
  114. const markdown = new MarkdownString(message, true);
  115. markdown.isTrusted = true;
  116. return markdown;
  117. }
  118. export function localChangesMessage(
  119. fromCommit: GitLogCommit | undefined,
  120. uri: GitUri,
  121. editorLine: number,
  122. hunk: GitDiffHunk,
  123. ): MarkdownString {
  124. const diff = getDiffFromHunk(hunk);
  125. let message;
  126. let previous;
  127. let current;
  128. if (fromCommit == null) {
  129. previous = '_Working Tree_';
  130. current = '_Unsaved_';
  131. } else {
  132. const file = fromCommit.findFile(uri.fsPath)!;
  133. message = `[$(compare-changes)](${DiffWithCommand.getMarkdownCommandArgs({
  134. lhs: {
  135. sha: fromCommit.sha,
  136. uri: GitUri.fromFile(file, uri.repoPath!, undefined, true).toFileUri(),
  137. },
  138. rhs: {
  139. sha: '',
  140. uri: uri.toFileUri(),
  141. },
  142. repoPath: uri.repoPath!,
  143. line: editorLine,
  144. })} "Open Changes")`;
  145. previous = `[$(git-commit) ${fromCommit.shortSha}](${ShowQuickCommitCommand.getMarkdownCommandArgs(
  146. fromCommit.sha,
  147. )} "Show Commit")`;
  148. current = '_Working Tree_';
  149. }
  150. message = `${diff}\n---\n\nLocal Changes &nbsp;${previous} &nbsp;${
  151. GlyphChars.ArrowLeftRightLong
  152. }&nbsp; ${current}${message == null ? '' : ` &nbsp;&nbsp;|&nbsp;&nbsp; ${message}`}`;
  153. const markdown = new MarkdownString(message, true);
  154. markdown.isTrusted = true;
  155. return markdown;
  156. }
  157. export async function detailsMessage(
  158. commit: GitCommit,
  159. uri: GitUri,
  160. editorLine: number,
  161. dateFormat: string | null,
  162. ): Promise<MarkdownString> {
  163. if (dateFormat === null) {
  164. dateFormat = 'MMMM Do, YYYY h:mma';
  165. }
  166. const remotes = await Container.git.getRemotes(commit.repoPath, { sort: true });
  167. const [previousLineDiffUris, autolinkedIssuesOrPullRequests, pr, presence] = await Promise.all([
  168. commit.isUncommitted ? commit.getPreviousLineDiffUris(uri, editorLine, uri.sha) : undefined,
  169. getAutoLinkedIssuesOrPullRequests(commit.message, remotes),
  170. getPullRequestForCommit(commit.ref, remotes),
  171. Container.vsls.maybeGetPresence(commit.email).catch(() => undefined),
  172. ]);
  173. const details = await CommitFormatter.fromTemplateAsync(Container.config.hovers.detailsMarkdownFormat, commit, {
  174. autolinkedIssuesOrPullRequests: autolinkedIssuesOrPullRequests,
  175. dateFormat: dateFormat,
  176. line: editorLine,
  177. markdown: true,
  178. messageAutolinks: Container.config.hovers.autolinks.enabled,
  179. pullRequestOrRemote: pr,
  180. presence: presence,
  181. previousLineDiffUris: previousLineDiffUris,
  182. remotes: remotes,
  183. });
  184. const markdown = new MarkdownString(details, true);
  185. markdown.isTrusted = true;
  186. return markdown;
  187. }
  188. function getDiffFromHunk(hunk: GitDiffHunk): string {
  189. return `\`\`\`diff\n${hunk.diff.trim()}\n\`\`\``;
  190. }
  191. function getDiffFromHunkLine(hunkLine: GitDiffHunkLine, diffStyle?: 'line' | 'hunk'): string {
  192. if (diffStyle === 'hunk' || (diffStyle == null && Container.config.hovers.changesDiff === 'hunk')) {
  193. return getDiffFromHunk(hunkLine.hunk);
  194. }
  195. return `\`\`\`diff${hunkLine.previous == null ? '' : `\n-${hunkLine.previous.line.trim()}`}${
  196. hunkLine.current == null ? '' : `\n+${hunkLine.current.line.trim()}`
  197. }\n\`\`\``;
  198. }
  199. async function getAutoLinkedIssuesOrPullRequests(message: string, remotes: GitRemote[]) {
  200. const cc = Logger.getNewCorrelationContext('Hovers.getAutoLinkedIssues');
  201. Logger.debug(cc, `${GlyphChars.Dash} message=<message>`);
  202. const start = process.hrtime();
  203. if (
  204. !Container.config.hovers.autolinks.enabled ||
  205. !Container.config.hovers.autolinks.enhanced ||
  206. !CommitFormatter.has(Container.config.hovers.detailsMarkdownFormat, 'message')
  207. ) {
  208. Logger.debug(cc, `completed ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`);
  209. return undefined;
  210. }
  211. const remote = await Container.git.getRemoteWithApiProvider(remotes);
  212. if (remote?.provider == null) {
  213. Logger.debug(cc, `completed ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`);
  214. return undefined;
  215. }
  216. // TODO: Make this configurable?
  217. const timeout = 250;
  218. try {
  219. const autolinks = await Container.autolinks.getIssueOrPullRequestLinks(message, remote, {
  220. timeout: timeout,
  221. });
  222. if (autolinks != null && (Logger.level === TraceLevel.Debug || Logger.isDebugging)) {
  223. // If there are any issues/PRs that timed out, log it
  224. const count = Iterables.count(autolinks.values(), pr => pr instanceof Promises.CancellationError);
  225. if (count !== 0) {
  226. Logger.debug(
  227. cc,
  228. `timed out ${
  229. GlyphChars.Dash
  230. } ${count} issue/pull request queries took too long (over ${timeout} ms) ${
  231. GlyphChars.Dot
  232. } ${Strings.getDurationMilliseconds(start)} ms`,
  233. );
  234. // const pending = [
  235. // ...Iterables.map(autolinks.values(), issueOrPullRequest =>
  236. // issueOrPullRequest instanceof Promises.CancellationError
  237. // ? issueOrPullRequest.promise
  238. // : undefined,
  239. // ),
  240. // ];
  241. // void Promise.all(pending).then(() => {
  242. // Logger.debug(
  243. // cc,
  244. // `${GlyphChars.Dot} ${count} issue/pull request queries completed; refreshing...`,
  245. // );
  246. // void commands.executeCommand('editor.action.showHover');
  247. // });
  248. return autolinks;
  249. }
  250. }
  251. Logger.debug(cc, `completed ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`);
  252. return autolinks;
  253. } catch (ex) {
  254. Logger.error(ex, cc, `failed ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`);
  255. return undefined;
  256. }
  257. }
  258. async function getPullRequestForCommit(ref: string, remotes: GitRemote[]) {
  259. const cc = Logger.getNewCorrelationContext('Hovers.getPullRequestForCommit');
  260. Logger.debug(cc, `${GlyphChars.Dash} ref=${ref}`);
  261. const start = process.hrtime();
  262. if (
  263. !Container.config.hovers.pullRequests.enabled ||
  264. !CommitFormatter.has(
  265. Container.config.hovers.detailsMarkdownFormat,
  266. 'pullRequest',
  267. 'pullRequestAgo',
  268. 'pullRequestAgoOrDate',
  269. 'pullRequestDate',
  270. 'pullRequestState',
  271. )
  272. ) {
  273. Logger.debug(cc, `completed ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`);
  274. return undefined;
  275. }
  276. const remote = await Container.git.getRemoteWithApiProvider(remotes, { includeDisconnected: true });
  277. if (remote?.provider == null) {
  278. Logger.debug(cc, `completed ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`);
  279. return undefined;
  280. }
  281. const { provider } = remote;
  282. const connected = provider.maybeConnected ?? (await provider.isConnected());
  283. if (!connected) {
  284. Logger.debug(cc, `completed ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`);
  285. return remote;
  286. }
  287. try {
  288. const pr = await Container.git.getPullRequestForCommit(ref, provider, { timeout: 250 });
  289. Logger.debug(cc, `completed ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`);
  290. return pr;
  291. } catch (ex) {
  292. if (ex instanceof Promises.CancellationError) {
  293. Logger.debug(cc, `timed out ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`);
  294. return ex;
  295. }
  296. Logger.error(ex, cc, `failed ${GlyphChars.Dot} ${Strings.getDurationMilliseconds(start)} ms`);
  297. return undefined;
  298. }
  299. }
  300. }