Skip to content
Open
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
9 changes: 9 additions & 0 deletions src/main/ipc/worktree-logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
21 changes: 19 additions & 2 deletions src/renderer/src/components/settings/BranchPrefixFeedback.tsx
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -30,6 +38,15 @@ export function BranchPrefixFeedback({ rawPrefix }: BranchPrefixFeedbackProps):
)}
</span>
)
} else if (omission === 'lowercase-lock') {
message = (
<span className="text-muted-foreground">
{translate(
'auto.components.settings.BranchPrefixFeedback.c478cf4ec7',
'No prefix will be applied because lowercasing this username ends in .lock, which Git rejects.'
)}
</span>
)
} else if (rawPrefix.trim()) {
message = (
<span className="text-muted-foreground">
Expand Down
16 changes: 16 additions & 0 deletions src/renderer/src/components/settings/GitPane.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,26 @@ 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', () => {
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('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 because lowercasing this username ends in .lock, which Git rejects.'
)
expect(html).not.toContain('Alice.LOCK')
expect(html).not.toContain('alice.lock')
})
})
34 changes: 28 additions & 6 deletions src/renderer/src/components/settings/GitPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -164,7 +168,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, {
Expand Down Expand Up @@ -228,17 +234,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'
)
Comment thread
innocarpe marked this conversation as resolved.
: 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 && <BranchPrefixFeedback rawPrefix={branchPrefixInputValue} />}
{isBranchPrefixInputMode && (
<BranchPrefixFeedback
rawPrefix={branchPrefixInputValue}
omission={
settings.branchPrefix === 'git-username'
? gitUsernamePrefixOmission(
displayedGitUsername.trim() ? displayedGitUsername : null
)
: null
}
/>
)}
</SearchableSetting>
) : null,
matchesSettingsSearch(searchQuery, {
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -7299,7 +7299,8 @@
"BranchPrefixFeedback": {
"6c40c0908f": "プレフィックスにスペースや ~ ^ : ? * [ \\ などの特殊文字は使用できません",
"64d70b156a": "ブランチ名は {{example}} になります",
"808f9a726e": "プレフィックスは適用されません"
"808f9a726e": "プレフィックスは適用されません",
"c478cf4ec7": "このユーザー名を小文字にすると末尾が .lock になり、Git が拒否するため接頭辞は付きません。"
},
"GitPane": {
"895d3f70b8": "gh",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/i18n/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -7262,7 +7262,8 @@
"BranchPrefixFeedback": {
"6c40c0908f": "접두사에는 공백이나 ~ ^ : ? * [ \\ 같은 특수 문자를 사용할 수 없습니다",
"64d70b156a": "브랜치 이름은 {{example}}(으)로 지정됩니다",
"808f9a726e": "접두사가 적용되지 않습니다"
"808f9a726e": "접두사가 적용되지 않습니다",
"c478cf4ec7": "이 사용자 이름을 소문자로 바꾸면 .lock으로 끝나 Git이 거절하므로 접두사를 적용하지 않습니다."
},
"GitPane": {
"895d3f70b8": "gh",
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -7262,7 +7262,8 @@
"BranchPrefixFeedback": {
"6c40c0908f": "前缀不能包含空格或 ~ ^ : ? * [ \\ 等特殊字符",
"64d70b156a": "分支将命名为 {{example}}",
"808f9a726e": "不会应用前缀"
"808f9a726e": "不会应用前缀",
"c478cf4ec7": "将此用户名转为小写后会以 .lock 结尾,Git 会拒绝,因此不会应用前缀。"
},
"GitPane": {
"895d3f70b8": "gh",
Expand Down
28 changes: 28 additions & 0 deletions src/shared/branch-prefix.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import {
assertBranchPrefixValid,
getBranchPrefixIssue,
gitUsernamePrefixOmission,
normalizeBranchPrefix,
selectBranchPrefixInput
} from './branch-prefix'
Expand Down Expand Up @@ -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()
})
Expand Down
56 changes: 51 additions & 5 deletions src/shared/branch-prefix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Comment thread
innocarpe marked this conversation as resolved.

/**
* 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: BranchPrefixSettings,
gitUsername: string | null
): string | null {
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':
Expand Down