From 4edd6047738e0558c29d9e449f208226940615f5 Mon Sep 17 00:00:00 2001 From: unknowlei <1049573774@qq.com> Date: Fri, 2 Oct 2026 11:18:56 +0800 Subject: [PATCH] fix: preserve remote branch names containing HEAD --- .changeset/fix-head-named-remote-branches.md | 5 ++ .../src/common/utils/git-utils.spec.ts | 62 +++++++++++++++++++ .../tm-core/src/common/utils/git-utils.ts | 4 +- 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-head-named-remote-branches.md create mode 100644 packages/tm-core/src/common/utils/git-utils.spec.ts diff --git a/.changeset/fix-head-named-remote-branches.md b/.changeset/fix-head-named-remote-branches.md new file mode 100644 index 0000000000..f31480e192 --- /dev/null +++ b/.changeset/fix-head-named-remote-branches.md @@ -0,0 +1,5 @@ +--- +"task-master-ai": patch +--- + +Preserve remote branches whose names contain HEAD when listing branches. Only the remote HEAD reference is excluded. diff --git a/packages/tm-core/src/common/utils/git-utils.spec.ts b/packages/tm-core/src/common/utils/git-utils.spec.ts new file mode 100644 index 0000000000..4dcc3491c5 --- /dev/null +++ b/packages/tm-core/src/common/utils/git-utils.spec.ts @@ -0,0 +1,62 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { getRemoteBranches } from './git-utils.js'; + +const { execAsync } = vi.hoisted(() => ({ execAsync: vi.fn() })); + +vi.mock('util', () => ({ promisify: () => execAsync })); + +describe('getRemoteBranches', () => { + beforeEach(() => { + execAsync.mockReset(); + }); + + it('preserves branch names containing HEAD', async () => { + execAsync.mockResolvedValue({ + stdout: 'origin/fix/HEAD-request\norigin/HEAD-fix\norigin/feature/HEAD\n' + }); + + await expect(getRemoteBranches('/project')).resolves.toEqual([ + 'fix/HEAD-request', + 'HEAD-fix', + 'feature/HEAD' + ]); + }); + + it('preserves branches whose remote name contains HEAD', async () => { + execAsync.mockResolvedValue({ stdout: 'HEADquarters/main\n' }); + + await expect(getRemoteBranches('/project')).resolves.toEqual(['main']); + }); + + it('excludes remote HEAD references', async () => { + execAsync.mockResolvedValue({ + stdout: 'origin/HEAD\r\nupstream/HEAD\r\norigin/main\r\n' + }); + + await expect(getRemoteBranches('/project')).resolves.toEqual(['main']); + }); + + it('deduplicates branches and ignores empty lines', async () => { + execAsync.mockResolvedValue({ + stdout: '\norigin/main\nupstream/main\n\norigin/feature/test\n' + }); + + await expect(getRemoteBranches('/project')).resolves.toEqual([ + 'main', + 'feature/test' + ]); + }); + + it('returns an empty list when git fails', async () => { + execAsync.mockRejectedValue(new Error('not a git repository')); + + await expect(getRemoteBranches('/project')).resolves.toEqual([]); + }); + + it('requires a project root without running git', async () => { + await expect(getRemoteBranches('')).rejects.toThrow( + 'projectRoot is required for getRemoteBranches' + ); + expect(execAsync).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/tm-core/src/common/utils/git-utils.ts b/packages/tm-core/src/common/utils/git-utils.ts index f08e31d522..aa9e9f8d5a 100644 --- a/packages/tm-core/src/common/utils/git-utils.ts +++ b/packages/tm-core/src/common/utils/git-utils.ts @@ -132,8 +132,8 @@ export async function getRemoteBranches( const names = stdout .trim() .split('\n') - .filter((branch) => branch.length > 0 && !branch.includes('HEAD')) - .map((branch) => branch.replace(/^[^/]+\//, '').trim()); + .map((branch) => branch.replace(/^[^/]+\//, '').trim()) + .filter((branch) => branch.length > 0 && branch !== 'HEAD'); return Array.from(new Set(names)); } catch (error) { return [];