Skip to content

Commit 0bed968

Browse files
Copilotalexr00
andauthored
Link commit SHAs in inline file comments (#8278)
* Initial plan * Initial analysis complete Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Add commit SHA linking to file view comments Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Add tests for commit SHA regex pattern Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Extract COMMIT_SHA_EXPRESSION as exported constant Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Revert unrelated type definition change Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Fix test: regex correctly excludes SHAs in backticks Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com>
1 parent 545a0ba commit 0bed968

File tree

2 files changed

+83
-2
lines changed

2 files changed

+83
-2
lines changed

src/github/prComment.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ export class TemporaryComment extends CommentBase {
217217
const SUGGESTION_EXPRESSION = /```suggestion(\u0020*(\r\n|\n))((?<suggestion>[\s\S]*?)(\r\n|\n))?```/;
218218
const IMG_EXPRESSION = /<img .*src=['"](?<src>.+?)['"].*?>/g;
219219
const UUID_EXPRESSION = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}/;
220+
export const COMMIT_SHA_EXPRESSION = /(?<![`\/\w])([0-9a-f]{7})([0-9a-f]{33})?(?![`\/\w])/g;
220221

221222
export class GHPRComment extends CommentBase {
222223
private static ID = 'GHPRComment';
@@ -421,6 +422,36 @@ ${lineContents}
421422
return replaceImages(body, html, this.githubRepository?.remote.host);
422423
}
423424

425+
private replaceCommitShas(body: string): string {
426+
const githubRepository = this.githubRepository;
427+
if (!githubRepository) {
428+
return body;
429+
}
430+
431+
// Match commit SHAs that are:
432+
// - Either 7 or 40 hex characters
433+
// - Not already part of a URL or markdown link
434+
// - Not inside code blocks (backticks)
435+
return body.replace(COMMIT_SHA_EXPRESSION, (match, shortSha, remaining, offset) => {
436+
// Don't replace if inside code blocks
437+
const beforeMatch = body.substring(0, offset);
438+
const backtickCount = (beforeMatch.match(/`/g)?.length ?? 0);
439+
if (backtickCount % 2 === 1) {
440+
return match;
441+
}
442+
443+
// Don't replace if already part of a markdown link
444+
if (beforeMatch.endsWith('[') || body.substring(offset + match.length).startsWith(']')) {
445+
return match;
446+
}
447+
448+
const owner = githubRepository.remote.owner;
449+
const repo = githubRepository.remote.repositoryName;
450+
const commitUrl = `https://${githubRepository.remote.host}/${owner}/${repo}/commit/${match}`;
451+
return `[${shortSha}](${commitUrl})`;
452+
});
453+
}
454+
424455
private replaceNewlines(body: string) {
425456
return body.replace(/(?<!\s)(\r\n|\n)/g, ' \n');
426457
}
@@ -463,7 +494,8 @@ ${lineContents}
463494
return `${substring.startsWith('@') ? '' : substring.charAt(0)}[@${username}](${url})`;
464495
});
465496

466-
const permalinkReplaced = await this.replacePermalink(linkified);
497+
const commitShasReplaced = this.replaceCommitShas(linkified);
498+
const permalinkReplaced = await this.replacePermalink(commitShasReplaced);
467499
await emojiPromise;
468500
return this.postpendSpecialAuthorComment(emojify(this.replaceImg(this.replaceSuggestion(permalinkReplaced))));
469501
}

src/test/github/prComment.test.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,56 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { default as assert } from 'assert';
7-
import { replaceImages } from '../../github/prComment';
7+
import { COMMIT_SHA_EXPRESSION, replaceImages } from '../../github/prComment';
8+
9+
describe('commit SHA replacement', function () {
10+
it('should match 7-character commit SHAs', function () {
11+
const text = 'Fixed in commit 5cf56bc and also in abc1234';
12+
const matches = Array.from(text.matchAll(COMMIT_SHA_EXPRESSION));
13+
assert.strictEqual(matches.length, 2);
14+
assert.strictEqual(matches[0][1], '5cf56bc');
15+
assert.strictEqual(matches[1][1], 'abc1234');
16+
});
17+
18+
it('should match 40-character commit SHAs', function () {
19+
const text = 'Fixed in commit 5cf56bc1234567890abcdef1234567890abcdef0';
20+
const matches = Array.from(text.matchAll(COMMIT_SHA_EXPRESSION));
21+
assert.strictEqual(matches.length, 1);
22+
assert.strictEqual(matches[0][0], '5cf56bc1234567890abcdef1234567890abcdef0');
23+
});
24+
25+
it('should not match SHAs in URLs', function () {
26+
const text = 'https://github.com/owner/repo/commit/5cf56bc';
27+
const matches = Array.from(text.matchAll(COMMIT_SHA_EXPRESSION));
28+
assert.strictEqual(matches.length, 0);
29+
});
30+
31+
it('should not match SHAs in code blocks', function () {
32+
const text = 'Fixed in commit 5cf56bc but not in `abc1234`';
33+
const matches = Array.from(text.matchAll(COMMIT_SHA_EXPRESSION));
34+
// The regex should only match the first SHA, not the one inside backticks
35+
assert.strictEqual(matches.length, 1);
36+
assert.strictEqual(matches[0][1], '5cf56bc');
37+
});
38+
39+
it('should not match non-hex strings', function () {
40+
const text = 'Not a SHA: 1234xyz or ABCDEFG';
41+
const matches = Array.from(text.matchAll(COMMIT_SHA_EXPRESSION));
42+
assert.strictEqual(matches.length, 0);
43+
});
44+
45+
it('should not match SHAs with alphanumeric prefix', function () {
46+
const text = 'prefix5cf56bc is not a SHA';
47+
const matches = Array.from(text.matchAll(COMMIT_SHA_EXPRESSION));
48+
assert.strictEqual(matches.length, 0);
49+
});
50+
51+
it('should not match SHAs with alphanumeric suffix', function () {
52+
const text = '5cf56bcsuffix is not a SHA';
53+
const matches = Array.from(text.matchAll(COMMIT_SHA_EXPRESSION));
54+
assert.strictEqual(matches.length, 0);
55+
});
56+
});
857

958
describe('replace images', function () {
1059
it('github.com', function () {

0 commit comments

Comments
 (0)