From 6e761c029ae6f21455c3ab2db672818227d4f478 Mon Sep 17 00:00:00 2001 From: Wooseong Kim Date: Thu, 1 Oct 2026 18:07:34 +0900 Subject: [PATCH 1/5] fix(git): lowercase the git-username branch prefix GitHub preserves signup case and treats the login as case-insensitive. Using that case as a branch prefix collides with an existing lowercase ref directory on APFS, so HEAD and the ref database disagree. Custom prefixes stay as typed. --- src/shared/branch-prefix.test.ts | 13 +++++++++++++ src/shared/branch-prefix.ts | 4 +++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/shared/branch-prefix.test.ts b/src/shared/branch-prefix.test.ts index 270a1fbfda82..84aaaf3ee7c9 100644 --- a/src/shared/branch-prefix.test.ts +++ b/src/shared/branch-prefix.test.ts @@ -113,6 +113,19 @@ describe('selectBranchPrefixInput', () => { expect(selectBranchPrefixInput({ branchPrefix: 'git-username' }, 'jdoe')).toBe('jdoe') }) + it('lowercases a git-username prefix so it cannot collide with an existing ref directory', () => { + expect(selectBranchPrefixInput({ branchPrefix: 'git-username' }, 'EurFelux')).toBe('eurfelux') + }) + + it('leaves a custom prefix in the case the user typed', () => { + expect( + selectBranchPrefixInput( + { branchPrefix: 'custom', branchPrefixCustom: 'EurFelux' }, + 'eurfelux' + ) + ).toBe('EurFelux') + }) + it('returns null for git-username when no username is available', () => { expect(selectBranchPrefixInput({ branchPrefix: 'git-username' }, null)).toBeNull() }) diff --git a/src/shared/branch-prefix.ts b/src/shared/branch-prefix.ts index 1c2abf922766..e36e4070c9ae 100644 --- a/src/shared/branch-prefix.ts +++ b/src/shared/branch-prefix.ts @@ -17,7 +17,9 @@ export function selectBranchPrefixInput( ): string | null { switch (settings.branchPrefix) { case 'git-username': - return gitUsername + // Why: GitHub keeps the signup case and treats the login as case-insensitive. + // A mixed-case prefix collides with an existing lowercase ref directory on APFS. + return gitUsername === null ? null : gitUsername.toLowerCase() case 'custom': return settings.branchPrefixCustom ?? null case 'none': From 98183881272501f34fae408377330e2b2d67f44b Mon Sep 17 00:00:00 2001 From: Wooseong Kim Date: Thu, 1 Oct 2026 20:09:36 +0900 Subject: [PATCH 2/5] fix(git): preview the prefix new branches use and keep a login whose lowercase form git rejects Co-authored-by: Cursor --- .../src/components/settings/GitPane.test.tsx | 12 ++++++++ .../src/components/settings/GitPane.tsx | 5 +++- src/shared/branch-prefix.test.ts | 6 ++++ src/shared/branch-prefix.ts | 28 ++++++++++++++----- 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/renderer/src/components/settings/GitPane.test.tsx b/src/renderer/src/components/settings/GitPane.test.tsx index 70cc28c32890..b7f9b13b658c 100644 --- a/src/renderer/src/components/settings/GitPane.test.tsx +++ b/src/renderer/src/components/settings/GitPane.test.tsx @@ -71,4 +71,16 @@ describe('GitPane branch prefix feedback', () => { const html = renderGitPane(gitUsernamePrefixSettings(), 'jdoe/') expect(html).toContain('jdoe/feature') }) + + it('previews the lowercased git username that new branches use', () => { + const html = renderGitPane(gitUsernamePrefixSettings(), 'EurFelux') + expect(html).toContain('eurfelux/feature') + expect(html).not.toContain('EurFelux') + }) + + it('keeps a mixed-case username in the preview when its lowercase form is not a valid ref', () => { + const html = renderGitPane(gitUsernamePrefixSettings(), 'Alice.LOCK') + expect(html).toContain('Alice.LOCK/feature') + expect(html).not.toContain('alice.lock') + }) }) diff --git a/src/renderer/src/components/settings/GitPane.tsx b/src/renderer/src/components/settings/GitPane.tsx index af739b6aa566..747f73ffd95f 100644 --- a/src/renderer/src/components/settings/GitPane.tsx +++ b/src/renderer/src/components/settings/GitPane.tsx @@ -9,6 +9,7 @@ import { Switch } from '../ui/switch' import { useAppStore } from '../../store' import { getGitPaneSearchEntries } from './git-search' import { SearchableSetting } from './SearchableSetting' +import { selectBranchPrefixInput } from '../../../../shared/branch-prefix' import { BranchPrefixFeedback } from './BranchPrefixFeedback' import { matchesSettingsSearch } from './settings-search' import { AutoRenameBranchFromWorkSetting } from './AutoRenameBranchFromWorkSetting' @@ -164,7 +165,9 @@ export function GitPane({ } }, [settings.branchPrefixCustom]) const branchPrefixInputValue = - settings.branchPrefix === 'git-username' ? displayedGitUsername : customPrefixDraft + settings.branchPrefix === 'git-username' + ? (selectBranchPrefixInput(settings, displayedGitUsername || null) ?? '') + : customPrefixDraft const visibleSections = [ matchesSettingsSearch(searchQuery, { diff --git a/src/shared/branch-prefix.test.ts b/src/shared/branch-prefix.test.ts index 84aaaf3ee7c9..ac0ad1953663 100644 --- a/src/shared/branch-prefix.test.ts +++ b/src/shared/branch-prefix.test.ts @@ -117,6 +117,12 @@ describe('selectBranchPrefixInput', () => { expect(selectBranchPrefixInput({ branchPrefix: 'git-username' }, 'EurFelux')).toBe('eurfelux') }) + it('keeps the original login when lowercasing would make the prefix a rejected ref', () => { + expect(selectBranchPrefixInput({ branchPrefix: 'git-username' }, 'Alice.LOCK')).toBe( + 'Alice.LOCK' + ) + }) + it('leaves a custom prefix in the case the user typed', () => { expect( selectBranchPrefixInput( diff --git a/src/shared/branch-prefix.ts b/src/shared/branch-prefix.ts index e36e4070c9ae..30749fc040a7 100644 --- a/src/shared/branch-prefix.ts +++ b/src/shared/branch-prefix.ts @@ -7,19 +7,33 @@ export type BranchPrefixSettings = { } /** - * Pick the raw, un-normalized value the configured strategy contributes, or - * null when no prefix applies. Shared so the main-process branch builder and - * the renderer's live settings feedback agree on which field each strategy uses. + * Pick the value the configured strategy contributes, or null when no prefix + * applies. Shared so the main-process branch builder and the renderer's live + * settings feedback agree on which field each strategy uses. + * + * A git username is lowercased so a mixed-case login cannot collide with an + * existing lowercase ref directory on a case-insensitive filesystem. When that + * lowercase form is a ref git rejects (`Alice.LOCK` becomes `alice.lock`), keep + * the original login if it is still valid so worktree creation does not drop it. */ export function selectBranchPrefixInput( settings: BranchPrefixSettings, gitUsername: string | null ): string | null { switch (settings.branchPrefix) { - case 'git-username': - // Why: GitHub keeps the signup case and treats the login as case-insensitive. - // A mixed-case prefix collides with an existing lowercase ref directory on APFS. - return gitUsername === null ? null : gitUsername.toLowerCase() + case 'git-username': { + if (gitUsername === null) { + return null + } + const lower = gitUsername.toLowerCase() + if (getBranchPrefixIssue(lower) === null) { + return lower + } + if (lower !== gitUsername && getBranchPrefixIssue(gitUsername) === null) { + return gitUsername + } + return lower + } case 'custom': return settings.branchPrefixCustom ?? null case 'none': From eedacc73b8d4c93b01f62aff89583c8967f45ff8 Mon Sep 17 00:00:00 2001 From: Wooseong Kim Date: Thu, 1 Oct 2026 20:27:08 +0900 Subject: [PATCH 3/5] fix(git): omit a username whose lowercase form is a rejected ref Alice.LOCK lowercases to alice.lock. Keeping the original login creates refs/heads/Alice.LOCK/, which collides with the alice.lock lockfile on a case-insensitive filesystem. That prefix is now omitted, and the settings preview no longer offers it. --- src/main/ipc/worktree-logic.test.ts | 9 +++++++++ .../src/components/settings/GitPane.test.tsx | 5 +++-- src/renderer/src/components/settings/GitPane.tsx | 13 +++++++++---- src/shared/branch-prefix.test.ts | 11 +++++++---- src/shared/branch-prefix.ts | 12 +++++++++--- 5 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/main/ipc/worktree-logic.test.ts b/src/main/ipc/worktree-logic.test.ts index a502e8023dd0..a791439367ac 100644 --- a/src/main/ipc/worktree-logic.test.ts +++ b/src/main/ipc/worktree-logic.test.ts @@ -285,6 +285,15 @@ describe('computeBranchName', () => { 'jdoe/feature' ) }) + + it('omits a git username whose lowercase form git rejects', () => { + expect(computeBranchName('feature', { branchPrefix: 'git-username' }, 'Alice.LOCK')).toBe( + 'feature' + ) + expect( + computeValidatedBranchName('feature', { branchPrefix: 'git-username' }, 'Alice.LOCK') + ).toBe('feature') + }) }) describe('getConfiguredBranchPrefix', () => { diff --git a/src/renderer/src/components/settings/GitPane.test.tsx b/src/renderer/src/components/settings/GitPane.test.tsx index b7f9b13b658c..bfc24c645282 100644 --- a/src/renderer/src/components/settings/GitPane.test.tsx +++ b/src/renderer/src/components/settings/GitPane.test.tsx @@ -78,9 +78,10 @@ describe('GitPane branch prefix feedback', () => { expect(html).not.toContain('EurFelux') }) - it('keeps a mixed-case username in the preview when its lowercase form is not a valid ref', () => { + it('does not preview a git username whose lowercase form is not a valid ref', () => { const html = renderGitPane(gitUsernamePrefixSettings(), 'Alice.LOCK') - expect(html).toContain('Alice.LOCK/feature') + expect(html).toContain('No prefix will be applied') + expect(html).not.toContain('Alice.LOCK') expect(html).not.toContain('alice.lock') }) }) diff --git a/src/renderer/src/components/settings/GitPane.tsx b/src/renderer/src/components/settings/GitPane.tsx index 747f73ffd95f..1ec6e1feebdb 100644 --- a/src/renderer/src/components/settings/GitPane.tsx +++ b/src/renderer/src/components/settings/GitPane.tsx @@ -231,10 +231,15 @@ export function GitPane({ }} placeholder={ settings.branchPrefix === 'git-username' - ? translate( - 'auto.components.settings.GitPane.aefa1ecb59', - 'No git username configured' - ) + ? displayedGitUsername.trim() + ? translate( + 'auto.components.settings.BranchPrefixFeedback.808f9a726e', + 'No prefix will be applied' + ) + : translate( + 'auto.components.settings.GitPane.aefa1ecb59', + 'No git username configured' + ) : translate('auto.components.settings.GitPane.b559bf9899', 'e.g. feature') } className="max-w-xs" diff --git a/src/shared/branch-prefix.test.ts b/src/shared/branch-prefix.test.ts index ac0ad1953663..d40f1ec517df 100644 --- a/src/shared/branch-prefix.test.ts +++ b/src/shared/branch-prefix.test.ts @@ -117,10 +117,13 @@ describe('selectBranchPrefixInput', () => { expect(selectBranchPrefixInput({ branchPrefix: 'git-username' }, 'EurFelux')).toBe('eurfelux') }) - it('keeps the original login when lowercasing would make the prefix a rejected ref', () => { - expect(selectBranchPrefixInput({ branchPrefix: 'git-username' }, 'Alice.LOCK')).toBe( - 'Alice.LOCK' - ) + it('drops a mixed-case login whose lowercase form git rejects', () => { + expect(selectBranchPrefixInput({ branchPrefix: 'git-username' }, 'Alice.LOCK')).toBeNull() + }) + + it('returns an already-lowercase invalid login so settings can warn', () => { + expect(selectBranchPrefixInput({ branchPrefix: 'git-username' }, 'alice.lock')).toBe('alice.lock') + expect(selectBranchPrefixInput({ branchPrefix: 'git-username' }, 'team x')).toBe('team x') }) it('leaves a custom prefix in the case the user typed', () => { diff --git a/src/shared/branch-prefix.ts b/src/shared/branch-prefix.ts index 30749fc040a7..5c4aa04b0473 100644 --- a/src/shared/branch-prefix.ts +++ b/src/shared/branch-prefix.ts @@ -13,8 +13,9 @@ export type BranchPrefixSettings = { * * A git username is lowercased so a mixed-case login cannot collide with an * existing lowercase ref directory on a case-insensitive filesystem. When that - * lowercase form is a ref git rejects (`Alice.LOCK` becomes `alice.lock`), keep - * the original login if it is still valid so worktree creation does not drop it. + * lowercase form is a ref git rejects (`Alice.LOCK` becomes `alice.lock`), drop + * the prefix. Keeping the original login creates `refs/heads/Alice.LOCK/`, which + * occupies the `alice.lock` lockfile path on a case-insensitive filesystem. */ export function selectBranchPrefixInput( settings: BranchPrefixSettings, @@ -29,8 +30,13 @@ export function selectBranchPrefixInput( if (getBranchPrefixIssue(lower) === null) { return lower } + // The original can pass git's case-sensitive check while its lowercase + // form is `*.lock`. Keeping that original still collides on a + // case-insensitive filesystem, so the prefix is omitted. An + // already-lowercase invalid login is returned unchanged so settings can + // show the rejection instead of pretending no username was configured. if (lower !== gitUsername && getBranchPrefixIssue(gitUsername) === null) { - return gitUsername + return null } return lower } From 01496ca52779bfeea78e7f2dc9576ed75ec3e68a Mon Sep 17 00:00:00 2001 From: Wooseong Kim Date: Thu, 1 Oct 2026 20:46:38 +0900 Subject: [PATCH 4/5] fix(git): explain when a username prefix is omitted A mixed-case login such as Alice.LOCK is left off new branches because its lowercase form ends in .lock. Settings now says that, instead of leaving the reason blank under the omitted prefix. --- .../settings/BranchPrefixFeedback.tsx | 21 +++++++++++++-- .../src/components/settings/GitPane.test.tsx | 5 +++- .../src/components/settings/GitPane.tsx | 18 +++++++++++-- src/renderer/src/i18n/locales/en.json | 3 ++- src/renderer/src/i18n/locales/es.json | 3 ++- src/renderer/src/i18n/locales/fr.json | 3 ++- src/renderer/src/i18n/locales/ja.json | 3 ++- src/renderer/src/i18n/locales/ko.json | 3 ++- src/renderer/src/i18n/locales/zh.json | 3 ++- src/shared/branch-prefix.test.ts | 6 +++++ src/shared/branch-prefix.ts | 26 ++++++++++++++++++- 11 files changed, 82 insertions(+), 12 deletions(-) diff --git a/src/renderer/src/components/settings/BranchPrefixFeedback.tsx b/src/renderer/src/components/settings/BranchPrefixFeedback.tsx index 2f892dba0507..ef3d39be1e10 100644 --- a/src/renderer/src/components/settings/BranchPrefixFeedback.tsx +++ b/src/renderer/src/components/settings/BranchPrefixFeedback.tsx @@ -1,12 +1,20 @@ import type { ReactNode } from 'react' -import { getBranchPrefixIssue, normalizeBranchPrefix } from '../../../../shared/branch-prefix' +import { + getBranchPrefixIssue, + normalizeBranchPrefix, + type GitUsernamePrefixOmission +} from '../../../../shared/branch-prefix' import { translate } from '@/i18n/i18n' type BranchPrefixFeedbackProps = { rawPrefix: string + omission?: GitUsernamePrefixOmission | null } -export function BranchPrefixFeedback({ rawPrefix }: BranchPrefixFeedbackProps): ReactNode { +export function BranchPrefixFeedback({ + rawPrefix, + omission = null +}: BranchPrefixFeedbackProps): ReactNode { const issue = getBranchPrefixIssue(rawPrefix) const normalized = normalizeBranchPrefix(rawPrefix) @@ -30,6 +38,15 @@ export function BranchPrefixFeedback({ rawPrefix }: BranchPrefixFeedbackProps): )} ) + } else if (omission === 'lowercase-lock') { + message = ( + + {translate( + 'auto.components.settings.BranchPrefixFeedback.c478cf4ec7', + 'No prefix will be applied because lowercasing this username ends in .lock, which Git rejects.' + )} + + ) } else if (rawPrefix.trim()) { message = ( diff --git a/src/renderer/src/components/settings/GitPane.test.tsx b/src/renderer/src/components/settings/GitPane.test.tsx index bfc24c645282..c87975c8d119 100644 --- a/src/renderer/src/components/settings/GitPane.test.tsx +++ b/src/renderer/src/components/settings/GitPane.test.tsx @@ -65,6 +65,7 @@ describe('GitPane branch prefix feedback', () => { it('warns in git-username mode when the displayed username is invalid', () => { const html = renderGitPane(gitUsernamePrefixSettings(), 'team x') expect(html).toContain('Prefix cannot contain spaces') + expect(html).not.toContain('which Git rejects') }) it('previews in git-username mode when the displayed username is valid', () => { @@ -80,7 +81,9 @@ describe('GitPane branch prefix feedback', () => { it('does not preview a git username whose lowercase form is not a valid ref', () => { const html = renderGitPane(gitUsernamePrefixSettings(), 'Alice.LOCK') - expect(html).toContain('No prefix will be applied') + expect(html).toContain( + 'No prefix will be applied because lowercasing this username ends in .lock, which Git rejects.' + ) expect(html).not.toContain('Alice.LOCK') expect(html).not.toContain('alice.lock') }) diff --git a/src/renderer/src/components/settings/GitPane.tsx b/src/renderer/src/components/settings/GitPane.tsx index 1ec6e1feebdb..92f44905676f 100644 --- a/src/renderer/src/components/settings/GitPane.tsx +++ b/src/renderer/src/components/settings/GitPane.tsx @@ -9,7 +9,10 @@ import { Switch } from '../ui/switch' import { useAppStore } from '../../store' import { getGitPaneSearchEntries } from './git-search' import { SearchableSetting } from './SearchableSetting' -import { selectBranchPrefixInput } from '../../../../shared/branch-prefix' +import { + gitUsernamePrefixOmission, + selectBranchPrefixInput +} from '../../../../shared/branch-prefix' import { BranchPrefixFeedback } from './BranchPrefixFeedback' import { matchesSettingsSearch } from './settings-search' import { AutoRenameBranchFromWorkSetting } from './AutoRenameBranchFromWorkSetting' @@ -246,7 +249,18 @@ export function GitPane({ readOnly={settings.branchPrefix === 'git-username'} /> )} - {isBranchPrefixInputMode && } + {isBranchPrefixInputMode && ( + + )} ) : null, matchesSettingsSearch(searchQuery, { diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index bf67a5a1fc3d..0213eef80767 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -7484,7 +7484,8 @@ "BranchPrefixFeedback": { "6c40c0908f": "Prefix cannot contain spaces or special characters like ~ ^ : ? * [ \\", "64d70b156a": "Branches will be named {{example}}", - "808f9a726e": "No prefix will be applied" + "808f9a726e": "No prefix will be applied", + "c478cf4ec7": "No prefix will be applied because lowercasing this username ends in .lock, which Git rejects." }, "GitPane": { "895d3f70b8": "gh", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 5ab6bbaaa5dc..0d7f2b91b715 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -6247,7 +6247,8 @@ "BranchPrefixFeedback": { "6c40c0908f": "El prefijo no puede contener espacios ni caracteres especiales como ~ ^ : ? * [ \\", "64d70b156a": "Las ramas se llamarán {{example}}", - "808f9a726e": "No se aplicará ningún prefijo" + "808f9a726e": "No se aplicará ningún prefijo", + "c478cf4ec7": "No se aplicará ningún prefijo porque el nombre de usuario en minúsculas termina en .lock y Git lo rechaza." }, "GitPane": { "895d3f70b8": "gh", diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 6465f23c87ec..96b3e0746260 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -7417,7 +7417,8 @@ "BranchPrefixFeedback": { "6c40c0908f": "Le préfixe ne peut pas contenir d'espaces ni de caractères spéciaux comme ~ ^ : ? * [ \\", "64d70b156a": "Les branches seront nommées {{example}}", - "808f9a726e": "Aucun préfixe ne sera appliqué" + "808f9a726e": "Aucun préfixe ne sera appliqué", + "c478cf4ec7": "Aucun préfixe ne sera appliqué, car le nom d'utilisateur en minuscules se termine par .lock, ce que Git refuse." }, "GitPane": { "895d3f70b8": "gh", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 4af40935b246..7e2998ffccab 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -7299,7 +7299,8 @@ "BranchPrefixFeedback": { "6c40c0908f": "プレフィックスにスペースや ~ ^ : ? * [ \\ などの特殊文字は使用できません", "64d70b156a": "ブランチ名は {{example}} になります", - "808f9a726e": "プレフィックスは適用されません" + "808f9a726e": "プレフィックスは適用されません", + "c478cf4ec7": "このユーザー名を小文字にすると末尾が .lock になり、Git が拒否するため接頭辞は付きません。" }, "GitPane": { "895d3f70b8": "gh", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index a72fbaa9eaa6..8bc4e7c09fd3 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -7262,7 +7262,8 @@ "BranchPrefixFeedback": { "6c40c0908f": "접두사에는 공백이나 ~ ^ : ? * [ \\ 같은 특수 문자를 사용할 수 없습니다", "64d70b156a": "브랜치 이름은 {{example}}(으)로 지정됩니다", - "808f9a726e": "접두사가 적용되지 않습니다" + "808f9a726e": "접두사가 적용되지 않습니다", + "c478cf4ec7": "이 사용자 이름을 소문자로 바꾸면 .lock으로 끝나 Git이 거절하므로 접두사를 적용하지 않습니다." }, "GitPane": { "895d3f70b8": "gh", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index cf73cbb7d8ff..c212e1b65295 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -7262,7 +7262,8 @@ "BranchPrefixFeedback": { "6c40c0908f": "前缀不能包含空格或 ~ ^ : ? * [ \\ 等特殊字符", "64d70b156a": "分支将命名为 {{example}}", - "808f9a726e": "不会应用前缀" + "808f9a726e": "不会应用前缀", + "c478cf4ec7": "将此用户名转为小写后会以 .lock 结尾,Git 会拒绝,因此不会应用前缀。" }, "GitPane": { "895d3f70b8": "gh", diff --git a/src/shared/branch-prefix.test.ts b/src/shared/branch-prefix.test.ts index d40f1ec517df..6e7335107e56 100644 --- a/src/shared/branch-prefix.test.ts +++ b/src/shared/branch-prefix.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest' import { assertBranchPrefixValid, getBranchPrefixIssue, + gitUsernamePrefixOmission, normalizeBranchPrefix, selectBranchPrefixInput } from './branch-prefix' @@ -119,6 +120,11 @@ describe('selectBranchPrefixInput', () => { it('drops a mixed-case login whose lowercase form git rejects', () => { expect(selectBranchPrefixInput({ branchPrefix: 'git-username' }, 'Alice.LOCK')).toBeNull() + expect(gitUsernamePrefixOmission('Alice.LOCK')).toBe('lowercase-lock') + expect(gitUsernamePrefixOmission('alice.lock')).toBeNull() + expect(gitUsernamePrefixOmission('team x')).toBeNull() + expect(gitUsernamePrefixOmission('EurFelux')).toBeNull() + expect(gitUsernamePrefixOmission(null)).toBeNull() }) it('returns an already-lowercase invalid login so settings can warn', () => { diff --git a/src/shared/branch-prefix.ts b/src/shared/branch-prefix.ts index 5c4aa04b0473..81f812003d74 100644 --- a/src/shared/branch-prefix.ts +++ b/src/shared/branch-prefix.ts @@ -17,6 +17,30 @@ export type BranchPrefixSettings = { * the prefix. Keeping the original login creates `refs/heads/Alice.LOCK/`, which * occupies the `alice.lock` lockfile path on a case-insensitive filesystem. */ +/** A configured login was not used because its lowercase form is a rejected ref. */ +export type GitUsernamePrefixOmission = 'lowercase-lock' + +/** + * Why Settings omits a git username. Only a mixed-case login whose lowercase + * form git rejects (`.LOCK` → `.lock`) is omitted. An already-lowercase invalid + * login stays visible so the existing character warning still explains it. + */ +export function gitUsernamePrefixOmission( + gitUsername: string | null +): GitUsernamePrefixOmission | null { + if (gitUsername === null) { + return null + } + const lower = gitUsername.toLowerCase() + if (lower === gitUsername || getBranchPrefixIssue(lower) === null) { + return null + } + if (getBranchPrefixIssue(gitUsername) !== null) { + return null + } + return 'lowercase-lock' +} + export function selectBranchPrefixInput( settings: BranchPrefixSettings, gitUsername: string | null @@ -35,7 +59,7 @@ export function selectBranchPrefixInput( // case-insensitive filesystem, so the prefix is omitted. An // already-lowercase invalid login is returned unchanged so settings can // show the rejection instead of pretending no username was configured. - if (lower !== gitUsername && getBranchPrefixIssue(gitUsername) === null) { + if (gitUsernamePrefixOmission(gitUsername) === 'lowercase-lock') { return null } return lower From c71cd068e2467910a138d528ad3df02515182740 Mon Sep 17 00:00:00 2001 From: Wooseong Kim Date: Thu, 1 Oct 2026 21:05:37 +0900 Subject: [PATCH 5/5] docs(git): put the prefix selector note back on its function The selector description was sitting above the omission type, so the function itself had no note. The block now sits directly above selectBranchPrefixInput. --- src/shared/branch-prefix.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/shared/branch-prefix.ts b/src/shared/branch-prefix.ts index 81f812003d74..8d4f892f909a 100644 --- a/src/shared/branch-prefix.ts +++ b/src/shared/branch-prefix.ts @@ -6,17 +6,6 @@ export type BranchPrefixSettings = { branchPrefixCustom?: string } -/** - * Pick the value the configured strategy contributes, or null when no prefix - * applies. Shared so the main-process branch builder and the renderer's live - * settings feedback agree on which field each strategy uses. - * - * A git username is lowercased so a mixed-case login cannot collide with an - * existing lowercase ref directory on a case-insensitive filesystem. When that - * lowercase form is a ref git rejects (`Alice.LOCK` becomes `alice.lock`), drop - * the prefix. Keeping the original login creates `refs/heads/Alice.LOCK/`, which - * occupies the `alice.lock` lockfile path on a case-insensitive filesystem. - */ /** A configured login was not used because its lowercase form is a rejected ref. */ export type GitUsernamePrefixOmission = 'lowercase-lock' @@ -41,6 +30,17 @@ export function gitUsernamePrefixOmission( return 'lowercase-lock' } +/** + * Pick the value the configured strategy contributes, or null when no prefix + * applies. Shared so the main-process branch builder and the renderer's live + * settings feedback agree on which field each strategy uses. + * + * A git username is lowercased so a mixed-case login cannot collide with an + * existing lowercase ref directory on a case-insensitive filesystem. When that + * lowercase form is a ref git rejects (`Alice.LOCK` becomes `alice.lock`), drop + * the prefix. Keeping the original login creates `refs/heads/Alice.LOCK/`, which + * occupies the `alice.lock` lockfile path on a case-insensitive filesystem. + */ export function selectBranchPrefixInput( settings: BranchPrefixSettings, gitUsername: string | null