mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 08:03:12 +00:00
* Normalize branch prefixes and flag invalid ones in settings A custom branch prefix ending in a slash (e.g. "team/") produced a double-slashed branch name like "team//feature" that git rejects, and the raw check-ref-format error gave no hint that the prefix caused it. - Normalize the configured prefix (trim whitespace, strip leading/ trailing and duplicate slashes) in the shared branch-name builder so the common trailing-slash case just works, for local and SSH worktrees. - Validate the prefix on the worktree-create path (computeValidatedBranchName) so a genuinely invalid prefix fails fast with a clear "update it in Settings -> Git" message instead of an opaque git error. - Add a live BranchPrefixFeedback under the Branch Prefix setting: previews the resulting branch name, warns on invalid characters, and notes when a prefix collapses to none. - Keep the background first-work rename on the non-throwing builder since the prefix is already validated at create time. * Keep caret in place when editing the branch prefix The custom branch prefix input was directly controlled by settings, but updateSettings persists through an async IPC round-trip, so the value updated a tick late and React re-assigned it, snapping the caret to the end on mid-string edits. Drive the input from a local draft and only adopt genuine external settings changes so the caret stays put (and fast typing survives slow SSH round-trips). Co-authored-by: Cursor <cursoragent@cursor.com> * Return ReactNode from BranchPrefixFeedback JSX.Element needlessly excludes null/string/number returns; ReactNode keeps the component's return type from over-constraining future changes. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
146 lines
4.7 KiB
TypeScript
146 lines
4.7 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
assertBranchPrefixValid,
|
|
getBranchPrefixIssue,
|
|
normalizeBranchPrefix,
|
|
selectBranchPrefixInput
|
|
} from './branch-prefix'
|
|
|
|
describe('normalizeBranchPrefix', () => {
|
|
it('strips a trailing slash so the join does not double it', () => {
|
|
expect(normalizeBranchPrefix('team/')).toBe('team')
|
|
})
|
|
|
|
it('strips a leading slash', () => {
|
|
expect(normalizeBranchPrefix('/team')).toBe('team')
|
|
})
|
|
|
|
it('collapses internal double slashes', () => {
|
|
expect(normalizeBranchPrefix('team//frontend')).toBe('team/frontend')
|
|
})
|
|
|
|
it('trims surrounding whitespace', () => {
|
|
expect(normalizeBranchPrefix(' team ')).toBe('team')
|
|
})
|
|
|
|
it('preserves a legitimate multi-segment prefix', () => {
|
|
expect(normalizeBranchPrefix('team/frontend')).toBe('team/frontend')
|
|
})
|
|
|
|
it('returns empty when the value is only slashes/whitespace', () => {
|
|
expect(normalizeBranchPrefix(' // ')).toBe('')
|
|
})
|
|
|
|
it('leaves a plain prefix untouched', () => {
|
|
expect(normalizeBranchPrefix('feature')).toBe('feature')
|
|
})
|
|
})
|
|
|
|
describe('getBranchPrefixIssue', () => {
|
|
it('accepts a normal prefix', () => {
|
|
expect(getBranchPrefixIssue('team')).toBeNull()
|
|
})
|
|
|
|
it('accepts a prefix that only needs trailing-slash normalization', () => {
|
|
expect(getBranchPrefixIssue('team/')).toBeNull()
|
|
})
|
|
|
|
it('accepts a hyphenated prefix', () => {
|
|
expect(getBranchPrefixIssue('feat-x')).toBeNull()
|
|
})
|
|
|
|
it('accepts a multi-segment prefix', () => {
|
|
expect(getBranchPrefixIssue('team/frontend')).toBeNull()
|
|
})
|
|
|
|
it('accepts a mid-ref segment that ends with a dot (git allows it)', () => {
|
|
expect(getBranchPrefixIssue('team./frontend')).toBeNull()
|
|
})
|
|
|
|
it('accepts a non-leading segment that starts with a dash (git allows it)', () => {
|
|
expect(getBranchPrefixIssue('team/-frontend')).toBeNull()
|
|
})
|
|
|
|
it('treats an empty prefix as valid (no prefix)', () => {
|
|
expect(getBranchPrefixIssue('')).toBeNull()
|
|
})
|
|
|
|
it('flags whitespace inside the prefix', () => {
|
|
expect(getBranchPrefixIssue('team x')).toBe('invalid-characters')
|
|
})
|
|
|
|
it('flags git ref-reserved characters', () => {
|
|
expect(getBranchPrefixIssue('team~')).toBe('invalid-characters')
|
|
expect(getBranchPrefixIssue('team:x')).toBe('invalid-characters')
|
|
expect(getBranchPrefixIssue('team[')).toBe('invalid-characters')
|
|
expect(getBranchPrefixIssue('team\\')).toBe('invalid-characters')
|
|
})
|
|
|
|
it('flags ASCII control characters', () => {
|
|
expect(getBranchPrefixIssue('team\x01')).toBe('invalid-characters')
|
|
})
|
|
|
|
it('flags a `..` sequence', () => {
|
|
expect(getBranchPrefixIssue('team..x')).toBe('invalid-characters')
|
|
})
|
|
|
|
it('flags a `@{` sequence', () => {
|
|
expect(getBranchPrefixIssue('team@{x')).toBe('invalid-characters')
|
|
})
|
|
|
|
it('flags a leading dash on the whole prefix', () => {
|
|
expect(getBranchPrefixIssue('-team')).toBe('invalid-characters')
|
|
})
|
|
|
|
it('flags a segment starting with a dot', () => {
|
|
expect(getBranchPrefixIssue('.team')).toBe('invalid-characters')
|
|
expect(getBranchPrefixIssue('team/.frontend')).toBe('invalid-characters')
|
|
})
|
|
|
|
it('flags the whole prefix ending with a dot', () => {
|
|
expect(getBranchPrefixIssue('team.')).toBe('invalid-characters')
|
|
expect(getBranchPrefixIssue('team/frontend.')).toBe('invalid-characters')
|
|
})
|
|
|
|
it('flags a `.lock` suffix on any segment', () => {
|
|
expect(getBranchPrefixIssue('team.lock')).toBe('invalid-characters')
|
|
expect(getBranchPrefixIssue('team.lock/x')).toBe('invalid-characters')
|
|
})
|
|
})
|
|
|
|
describe('selectBranchPrefixInput', () => {
|
|
it('returns the git username for the git-username strategy', () => {
|
|
expect(selectBranchPrefixInput({ branchPrefix: 'git-username' }, 'jdoe')).toBe('jdoe')
|
|
})
|
|
|
|
it('returns null for git-username when no username is available', () => {
|
|
expect(selectBranchPrefixInput({ branchPrefix: 'git-username' }, null)).toBeNull()
|
|
})
|
|
|
|
it('returns the raw custom value for the custom strategy', () => {
|
|
expect(
|
|
selectBranchPrefixInput({ branchPrefix: 'custom', branchPrefixCustom: 'team/' }, null)
|
|
).toBe('team/')
|
|
})
|
|
|
|
it('returns null for custom when no value is set', () => {
|
|
expect(selectBranchPrefixInput({ branchPrefix: 'custom' }, null)).toBeNull()
|
|
})
|
|
|
|
it('returns null for the none strategy', () => {
|
|
expect(selectBranchPrefixInput({ branchPrefix: 'none' }, 'jdoe')).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('assertBranchPrefixValid', () => {
|
|
it('does not throw for a valid prefix', () => {
|
|
expect(() => assertBranchPrefixValid('team')).not.toThrow()
|
|
})
|
|
|
|
it('throws with a settings hint for an invalid prefix', () => {
|
|
expect(() => assertBranchPrefixValid('team x')).toThrow(
|
|
'Branch prefix "team x" contains characters git rejects — update it in Settings → Git'
|
|
)
|
|
})
|
|
})
|