Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions packages/gittensory-mcp/lib/local-branch.js
Original file line number Diff line number Diff line change
Expand Up @@ -557,9 +557,13 @@ function assertSourceUploadDisabled() {
}
}

function extractLinkedIssues(text) {
// Word-boundary the closing keywords (as the server-side extractors in src/db/repositories.ts and
// src/signals/engine.ts already do) so a keyword embedded in a longer word does not spuriously link an
// issue: without \b, `hotfix 5` / `prefixes 12` matched the `fix`/`fixes` substring and captured the
// trailing number. The bare `#` branch stays boundary-free so `#123` still matches anywhere.
export function extractLinkedIssues(text) {
const issues = [];
for (const match of String(text).matchAll(/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?|#)\s*#?(\d+)/gi)) issues.push(Number(match[1]));
for (const match of String(text).matchAll(/(?:\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)|#)\s*#?(\d+)/gi)) issues.push(Number(match[1]));
return issues.filter((issue) => Number.isInteger(issue) && issue > 0);
}

Expand Down
15 changes: 15 additions & 0 deletions test/unit/local-branch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1737,6 +1737,21 @@ describe("local MCP git metadata collection", () => {
);
});

it("extracts linked issues only from standalone closing keywords, not keyword substrings", async () => {
// @ts-expect-error package helper is plain JS because the local wrapper ships as a Node bin package.
const { extractLinkedIssues } = await import("../../packages/gittensory-mcp/lib/local-branch.js");
// Standalone closing keywords (hash optional, as this client-side extractor allows) and bare #refs link.
expect(extractLinkedIssues("fixes #5")).toEqual([5]);
expect(extractLinkedIssues("Closes 12 and resolves #34")).toEqual([12, 34]);
expect(extractLinkedIssues("see #7")).toEqual([7]);
expect(extractLinkedIssues("closes#3")).toEqual([3]);
// Regression: a closing keyword embedded in a longer word must NOT capture a trailing number.
expect(extractLinkedIssues("hotfix 5")).toEqual([]);
expect(extractLinkedIssues("prefixes 12")).toEqual([]);
expect(extractLinkedIssues("unclosed 9")).toEqual([]);
expect(extractLinkedIssues("no references here")).toEqual([]);
});

it("parses remotes, changed-file stats, linked issues, and refuses source upload mode", async () => {
// @ts-expect-error package helper is plain JS because the local wrapper ships as a Node bin package.
const { collectLocalBranchMetadata, parseGitRemote } = await import("../../packages/gittensory-mcp/lib/local-branch.js");
Expand Down