diff --git a/src/main/ipc/worktree-logic.test.ts b/src/main/ipc/worktree-logic.test.ts
index 0df96fa97abf..4a285c613621 100644
--- a/src/main/ipc/worktree-logic.test.ts
+++ b/src/main/ipc/worktree-logic.test.ts
@@ -289,6 +289,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/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.ts b/src/renderer/src/components/settings/GitPane.test.ts
index df0158497f0e..547a45825f29 100644
--- a/src/renderer/src/components/settings/GitPane.test.ts
+++ b/src/renderer/src/components/settings/GitPane.test.ts
@@ -331,10 +331,26 @@ describe('GitPane branch prefix feedback', () => {
it('warns in git-username mode when the displayed username is invalid', () => {
const html = renderBranchPrefixGitPane(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', () => {
const html = renderBranchPrefixGitPane(gitUsernamePrefixSettings(), 'jdoe/')
expect(html).toContain('jdoe/feature')
})
+
+ it('previews the lowercased git username that new branches use', () => {
+ const html = renderBranchPrefixGitPane(gitUsernamePrefixSettings(), 'EurFelux')
+ expect(html).toContain('eurfelux/feature')
+ expect(html).not.toContain('EurFelux')
+ })
+
+ it('does not preview a git username whose lowercase form is not a valid ref', () => {
+ const html = renderBranchPrefixGitPane(gitUsernamePrefixSettings(), 'Alice.LOCK')
+ 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 f2812f280419..42bcb820de6b 100644
--- a/src/renderer/src/components/settings/GitPane.tsx
+++ b/src/renderer/src/components/settings/GitPane.tsx
@@ -9,6 +9,10 @@ import { Switch } from '../ui/switch'
import { useAppStore } from '../../store'
import { getGitPaneSearchEntries } from './git-search'
import { SearchableSetting } from './SearchableSetting'
+import {
+ gitUsernamePrefixOmission,
+ selectBranchPrefixInput
+} from '../../../../shared/branch-prefix'
import { BranchPrefixFeedback } from './BranchPrefixFeedback'
import { matchesSettingsSearch } from './settings-search'
import { AutoRenameBranchFromWorkSetting } from './AutoRenameBranchFromWorkSetting'
@@ -171,7 +175,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, {
@@ -235,17 +241,33 @@ 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"
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 95f72406d7cb..7a53012f0ba0 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -7969,7 +7969,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 3857cbe56f70..0467c064d5c5 100644
--- a/src/renderer/src/i18n/locales/es.json
+++ b/src/renderer/src/i18n/locales/es.json
@@ -7607,7 +7607,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 a4b34c154322..e4319a95b651 100644
--- a/src/renderer/src/i18n/locales/fr.json
+++ b/src/renderer/src/i18n/locales/fr.json
@@ -7757,7 +7757,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 519bc6872a50..cd770e8411f1 100644
--- a/src/renderer/src/i18n/locales/ja.json
+++ b/src/renderer/src/i18n/locales/ja.json
@@ -7634,7 +7634,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 fd8d9392801d..58fff212207f 100644
--- a/src/renderer/src/i18n/locales/ko.json
+++ b/src/renderer/src/i18n/locales/ko.json
@@ -7597,7 +7597,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 7346dc7b66e6..03e62130dd78 100644
--- a/src/renderer/src/i18n/locales/zh.json
+++ b/src/renderer/src/i18n/locales/zh.json
@@ -7597,7 +7597,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 f22613367cb3..0b0d6bfb0ba1 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'
@@ -113,6 +114,33 @@ 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('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', () => {
+ 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', () => {
+ 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 004fe33e17c1..fc092c194afb 100644
--- a/src/shared/branch-prefix.ts
+++ b/src/shared/branch-prefix.ts
@@ -6,18 +6,64 @@ export type BranchPrefixSettings = {
branchPrefixCustom?: string
}
+/** A configured login was not used because its lowercase form is a rejected ref. */
+export type GitUsernamePrefixOmission = 'lowercase-lock'
+
/**
- * 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.
+ * 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'
+}
+
+/**
+ * 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: Omit & { branchPrefix: string },
gitUsername: string | null
): string | null | void {
switch (settings.branchPrefix) {
- case 'git-username':
- return gitUsername
+ case 'git-username': {
+ if (gitUsername === null) {
+ return null
+ }
+ const lower = gitUsername.toLowerCase()
+ 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 (gitUsernamePrefixOmission(gitUsername) === 'lowercase-lock') {
+ return null
+ }
+ return lower
+ }
case 'custom':
return settings.branchPrefixCustom ?? null
case 'none':