authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-02 14:35:36-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-02 20:46:05-08:00
log61289a152d7c655a3239ff9c9cfef4ee3bd9f6c0
treec330486d774d1461a9208be9474e7a5906833ec8
parent906550f9af7d6fc37e1c9e4f21018dcb9ea03591

Fix duplicated branch logic and feature-branch dialog on push-only

Deduplicate feature/ prefix logic in resolveAutoFeatureBranchName by delegating to sanitizeFeatureBranchName. Skip the default-branch confirmation dialog when there are no uncommitted changes (push-only), since runFeatureBranchStep requires dirty worktree. Add merge-base assertions to feature branch tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

3 files changed, 19 insertions(+), 7 deletions(-)

apps/server/src/git/Layers/GitManager.test.ts+16
...@@ -563,6 +563,14 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -563,6 +563,14 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
563 Effect.map((result) => result.stdout.trim()),563 Effect.map((result) => result.stdout.trim()),
564 ),564 ),
565 ).toBe("feature/implement-stacked-git-actions");565 ).toBe("feature/implement-stacked-git-actions");
566
567 const mainSha = yield* runGit(repoDir, ["rev-parse", "main"]).pipe(
568 Effect.map((r) => r.stdout.trim()),
569 );
570 const mergeBase = yield* runGit(repoDir, ["merge-base", "main", "HEAD"]).pipe(
571 Effect.map((r) => r.stdout.trim()),
572 );
573 expect(mergeBase).toBe(mainSha);
566 }),574 }),
567 );575 );
568576
...@@ -598,6 +606,14 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {...@@ -598,6 +606,14 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
598 expect(result.commit.status).toBe("created");606 expect(result.commit.status).toBe("created");
599 expect(result.commit.subject).toBe("feat: custom summary line");607 expect(result.commit.subject).toBe("feat: custom summary line");
600 expect(generatedCount).toBe(0);608 expect(generatedCount).toBe(0);
609
610 const mainSha = yield* runGit(repoDir, ["rev-parse", "main"]).pipe(
611 Effect.map((r) => r.stdout.trim()),
612 );
613 const mergeBase = yield* runGit(repoDir, ["merge-base", "main", result.branch.name!]).pipe(
614 Effect.map((r) => r.stdout.trim()),
615 );
616 expect(mergeBase).toBe(mainSha);
601 }),617 }),
602 );618 );
603619
apps/web/src/components/GitActionsControl.tsx+2-1
...@@ -254,7 +254,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions...@@ -254,7 +254,8 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
254 !skipDefaultBranchPrompt &&254 !skipDefaultBranchPrompt &&
255 !featureBranch &&255 !featureBranch &&
256 requiresDefaultBranchConfirmation(action, actionIsDefaultBranch) &&256 requiresDefaultBranchConfirmation(action, actionIsDefaultBranch) &&
257 actionBranch257 actionBranch &&
258 !!actionStatus?.hasWorkingTreeChanges
258 ) {259 ) {
259 if (action !== "commit_push" && action !== "commit_push_pr") {260 if (action !== "commit_push" && action !== "commit_push_pr") {
260 return;261 return;
packages/shared/src/git.ts+1-6
...@@ -43,14 +43,9 @@ export function resolveAutoFeatureBranchName(...@@ -43,14 +43,9 @@ export function resolveAutoFeatureBranchName(
43 preferredBranch?: string,43 preferredBranch?: string,
44): string {44): string {
45 const preferred = preferredBranch?.trim();45 const preferred = preferredBranch?.trim();
46 const normalized = sanitizeBranchFragment(46 const resolvedBase = sanitizeFeatureBranchName(
47 preferred && preferred.length > 0 ? preferred : AUTO_FEATURE_BRANCH_FALLBACK,47 preferred && preferred.length > 0 ? preferred : AUTO_FEATURE_BRANCH_FALLBACK,
48 );48 );
49 const resolvedBase = normalized.includes("/")
50 ? normalized.startsWith("feature/")
51 ? normalized
52 : `feature/${normalized}`
53 : `feature/${normalized}`;
54 const existingNames = new Set(existingBranchNames);49 const existingNames = new Set(existingBranchNames);
5550
56 if (!existingNames.has(resolvedBase)) {51 if (!existingNames.has(resolvedBase)) {