diff --git a/extensions/terminal-suggest/src/completions/git.ts b/extensions/terminal-suggest/src/completions/git.ts index 7c0f74fb843..8b45edf5a7e 100644 --- a/extensions/terminal-suggest/src/completions/git.ts +++ b/extensions/terminal-suggest/src/completions/git.ts @@ -74,46 +74,67 @@ const postProcessBranches = const seen = new Set(); return output .split("\n") - .filter((line) => !line.trim().startsWith("HEAD")) + .filter((line) => line.trim() && !line.trim().startsWith("HEAD")) .map((branch) => { - let name = branch.trim(); - const parts = branch.match(/\S+/g); - if (parts && parts.length > 1) { - if (parts[0] === "*") { - // We are in a detached HEAD state - if (branch.includes("HEAD detached")) { - return null; + // Parse the format: branchName|author|hash|subject|timeAgo + const parts = branch.split("|"); + if (parts.length < 5) { + // Fallback to old parsing if format doesn't match + let name = branch.trim(); + const oldParts = branch.match(/\S+/g); + if (oldParts && oldParts.length > 1) { + if (oldParts[0] === "*") { + if (branch.includes("HEAD detached")) { + return null; + } + return { + name: branch.replaceAll("*", "").trim(), + description: "Current branch", + priority: 100, + icon: `vscode://icon?type=${vscode.TerminalCompletionItemKind.ScmBranch}` + }; + } else if (oldParts[0] === "+") { + name = branch.replaceAll("+", "").trim(); } - // Current branch - return { - name: branch.replace("*", "").trim(), - description: "Current branch", - priority: 100, - icon: `vscode://icon?type=${vscode.TerminalCompletionItemKind.ScmBranch}` - }; - } else if (parts[0] === "+") { - // Branch checked out in another worktree. - name = branch.replace("+", "").trim(); } + + let description = "Branch"; + if (insertWithoutRemotes && name.startsWith("remotes/")) { + name = name.slice(name.indexOf("/", 8) + 1); + description = "Remote branch"; + } + + const space = name.indexOf(" "); + if (space !== -1) { + name = name.slice(0, space); + } + + return { + name, + description, + icon: `vscode://icon?type=${vscode.TerminalCompletionItemKind.ScmBranch}`, + priority: 75, + }; } - let description = "Branch"; + let name = parts[0].trim(); + const author = parts[1].trim(); + const hash = parts[2].trim(); + const subject = parts[3].trim(); + const timeAgo = parts[4].trim(); + + const description = `${timeAgo} • ${author} • ${hash} • ${subject}`; + const priority = 75; if (insertWithoutRemotes && name.startsWith("remotes/")) { name = name.slice(name.indexOf("/", 8) + 1); - description = "Remote branch"; - } - - const space = name.indexOf(" "); - if (space !== -1) { - name = name.slice(0, space); } return { name, description, icon: `vscode://icon?type=${vscode.TerminalCompletionItemKind.ScmBranch}`, - priority: 75, + priority, }; }) .filter((suggestion) => { @@ -128,6 +149,15 @@ const postProcessBranches = }); }; +// Common git for-each-ref arguments for branch queries with commit details +const gitBranchForEachRefArgs = [ + "git", + "--no-optional-locks", + "for-each-ref", + "--sort=-committerdate", + "--format=%(refname:short)|%(authorname)|%(objectname:short)|%(subject)|%(committerdate:relative)", +] as const; + export const gitGenerators = { // Commit history commits: { @@ -252,23 +282,17 @@ export const gitGenerators = { // All branches remoteLocalBranches: { script: [ - "git", - "--no-optional-locks", - "branch", - "-a", - "--no-color", - "--sort=-committerdate", + ...gitBranchForEachRefArgs, + "refs/heads/", + "refs/remotes/", ], postProcess: postProcessBranches({ insertWithoutRemotes: true }), } satisfies Fig.Generator, localBranches: { script: [ - "git", - "--no-optional-locks", - "branch", - "--no-color", - "--sort=-committerdate", + ...gitBranchForEachRefArgs, + "refs/heads/", ], postProcess: postProcessBranches({ insertWithoutRemotes: true }), } satisfies Fig.Generator, @@ -278,37 +302,19 @@ export const gitGenerators = { localOrRemoteBranches: { custom: async (tokens, executeShellCommand) => { const pp = postProcessBranches({ insertWithoutRemotes: true }); - if (tokens.includes("-r")) { - return pp?.( - ( - await executeShellCommand({ - command: "git", - args: [ - "--no-optional-locks", - "-r", - "--no-color", - "--sort=-committerdate", - ], - }) - ).stdout, - tokens - ); - } else { - return pp?.( - ( - await executeShellCommand({ - command: "git", - args: [ - "--no-optional-locks", - "branch", - "--no-color", - "--sort=-committerdate", - ], - }) - ).stdout, - tokens - ); - } + const refs = tokens.includes("-r") ? "refs/remotes/" : "refs/heads/"; + return pp?.( + ( + await executeShellCommand({ + command: gitBranchForEachRefArgs[0], + args: [ + ...gitBranchForEachRefArgs.slice(1), + refs, + ], + }) + ).stdout, + tokens + ); }, } satisfies Fig.Generator, diff --git a/extensions/terminal-suggest/src/test/completions/git-branch.test.ts b/extensions/terminal-suggest/src/test/completions/git-branch.test.ts new file mode 100644 index 00000000000..20d140c3e7c --- /dev/null +++ b/extensions/terminal-suggest/src/test/completions/git-branch.test.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import 'mocha'; +import * as vscode from 'vscode'; +import { gitGenerators } from '../../completions/git'; + +suite('Git Branch Completions', () => { + test('postProcessBranches should parse git for-each-ref output with commit details', () => { + const input = `main|John Doe|abc1234|Fix response codeblock in debug view|2 days ago +feature/test|Jane Smith|def5678|Add new feature|1 week ago`; + + const result = gitGenerators.localBranches.postProcess!(input, []); + + assert.ok(result); + assert.strictEqual(result.length, 2); + assert.ok(result[0]); + assert.strictEqual(result[0].name, 'main'); + assert.strictEqual(result[0].description, '2 days ago • John Doe • abc1234 • Fix response codeblock in debug view'); + assert.strictEqual(result[0].icon, `vscode://icon?type=${vscode.TerminalCompletionItemKind.ScmBranch}`); + + assert.ok(result[1]); + assert.strictEqual(result[1].name, 'feature/test'); + assert.strictEqual(result[1].description, '1 week ago • Jane Smith • def5678 • Add new feature'); + assert.strictEqual(result[1].icon, `vscode://icon?type=${vscode.TerminalCompletionItemKind.ScmBranch}`); + }); + + test('postProcessBranches should handle remote branches', () => { + const input = `remotes/origin/main|John Doe|abc1234|Fix bug|2 days ago +remotes/origin/feature|Jane Smith|def5678|Add feature|1 week ago`; + + const result = gitGenerators.remoteLocalBranches.postProcess!(input, []); + + assert.ok(result); + assert.strictEqual(result.length, 2); + assert.ok(result[0]); + assert.strictEqual(result[0].name, 'main'); + assert.strictEqual(result[0].description, '2 days ago • John Doe • abc1234 • Fix bug'); + + assert.ok(result[1]); + assert.strictEqual(result[1].name, 'feature'); + assert.strictEqual(result[1].description, '1 week ago • Jane Smith • def5678 • Add feature'); + }); + + test('postProcessBranches should filter out HEAD branches', () => { + const input = `main|John Doe|abc1234|Fix bug|2 days ago +HEAD -> main|John Doe|abc1234|Fix bug|2 days ago`; + + const result = gitGenerators.localBranches.postProcess!(input, []); + + assert.ok(result); + assert.strictEqual(result.length, 1); + assert.ok(result[0]); + assert.strictEqual(result[0].name, 'main'); + }); + + test('postProcessBranches should handle empty input', () => { + const input = ''; + + const result = gitGenerators.localBranches.postProcess!(input, []); + + assert.ok(result); + assert.strictEqual(result.length, 0); + }); + + test('postProcessBranches should handle git error output', () => { + const input = 'fatal: not a git repository'; + + const result = gitGenerators.localBranches.postProcess!(input, []); + + assert.ok(result); + assert.strictEqual(result.length, 0); + }); + + test('postProcessBranches should deduplicate branches', () => { + const input = `main|John Doe|abc1234|Fix bug|2 days ago +main|John Doe|abc1234|Fix bug|2 days ago`; + + const result = gitGenerators.localBranches.postProcess!(input, []); + + assert.ok(result); + assert.strictEqual(result.length, 1); + assert.ok(result[0]); + assert.strictEqual(result[0].name, 'main'); + }); +});