authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-02 19:40:55-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-02 20:46:05-08:00
logb2be7f4e4f0aed3c4b03e3986dd3507c501a4abb
tree86237676586e04ab6742c9b9da721241fbc41f67
parent94beb21a4358cda0590c448ba684f663ad476ec1

Address PR feedback on git workflows

Co-authored-by: codex <codex@users.noreply.github.com>

6 files changed, 56 insertions(+), 32 deletions(-)

apps/server/src/git/Layers/GitManager.test.ts+17-1
......@@ -546,8 +546,23 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
546546 yield* runGit(repoDir, ["remote", "add", "origin", remoteDir]);
547547 yield* runGit(repoDir, ["push", "-u", "origin", "main"]);
548548 fs.writeFileSync(path.join(repoDir, "README.md"), "hello\nfeature-branch\n");
549 let generatedCount = 0;
549550
550 const { manager } = yield* makeManager();
551 const { manager } = yield* makeManager({
552 textGeneration: {
553 generateCommitMessage: (input) =>
554 Effect.sync(() => {
555 generatedCount += 1;
556 return {
557 subject: "Implement stacked git actions",
558 body: "",
559 ...(input.includeBranch
560 ? { branch: "feature/implement-stacked-git-actions" }
561 : {}),
562 };
563 }),
564 },
565 });
551566 const result = yield* runStackedAction(manager, {
552567 cwd: repoDir,
553568 action: "commit_push",
......@@ -571,6 +586,7 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
571586 Effect.map((r) => r.stdout.trim()),
572587 );
573588 expect(mergeBase).toBe(mainSha);
589 expect(generatedCount).toBe(1);
574590 }),
575591 );
576592
apps/server/src/git/Layers/GitManager.ts+29-7
......@@ -105,6 +105,13 @@ function sanitizeCommitMessage(generated: {
105105 };
106106}
107107
108interface CommitAndBranchSuggestion {
109 subject: string;
110 body: string;
111 branch?: string | undefined;
112 commitMessage: string;
113}
114
108115function formatCommitMessage(subject: string, body: string): string {
109116 const trimmedBody = body.trim();
110117 if (trimmedBody.length === 0) {
......@@ -311,13 +318,20 @@ export const makeGitManager = Effect.gen(function* () {
311318 };
312319 });
313320
314 const runCommitStep = (cwd: string, branch: string | null, commitMessage?: string) =>
321 const runCommitStep = (
322 cwd: string,
323 branch: string | null,
324 commitMessage?: string,
325 preResolvedSuggestion?: CommitAndBranchSuggestion,
326 ) =>
315327 Effect.gen(function* () {
316 const suggestion = yield* resolveCommitAndBranchSuggestion({
317 cwd,
318 branch,
319 ...(commitMessage ? { commitMessage } : {}),
320 });
328 const suggestion =
329 preResolvedSuggestion ??
330 (yield* resolveCommitAndBranchSuggestion({
331 cwd,
332 branch,
333 ...(commitMessage ? { commitMessage } : {}),
334 }));
321335 if (!suggestion) {
322336 return { status: "skipped_no_changes" as const };
323337 }
......@@ -456,6 +470,7 @@ export const makeGitManager = Effect.gen(function* () {
456470 return {
457471 branchStep: { status: "created" as const, name: resolvedBranch },
458472 resolvedCommitMessage: suggestion.commitMessage,
473 resolvedCommitSuggestion: suggestion,
459474 };
460475 });
461476
......@@ -477,6 +492,7 @@ export const makeGitManager = Effect.gen(function* () {
477492
478493 let branchStep: { status: "created" | "skipped_not_requested"; name?: string };
479494 let commitMessageForStep = input.commitMessage;
495 let preResolvedCommitSuggestion: CommitAndBranchSuggestion | undefined = undefined;
480496
481497 if (input.featureBranch) {
482498 const result = yield* runFeatureBranchStep(
......@@ -486,13 +502,19 @@ export const makeGitManager = Effect.gen(function* () {
486502 );
487503 branchStep = result.branchStep;
488504 commitMessageForStep = result.resolvedCommitMessage;
505 preResolvedCommitSuggestion = result.resolvedCommitSuggestion;
489506 } else {
490507 branchStep = { status: "skipped_not_requested" as const };
491508 }
492509
493510 const currentBranch = branchStep.name ?? initialStatus.branch;
494511
495 const commit = yield* runCommitStep(input.cwd, currentBranch, commitMessageForStep);
512 const commit = yield* runCommitStep(
513 input.cwd,
514 currentBranch,
515 commitMessageForStep,
516 preResolvedCommitSuggestion,
517 );
496518
497519 const push = wantsPush
498520 ? yield* gitCore.pushCurrentBranch(input.cwd, currentBranch)
apps/web/src/components/GitActionsControl.logic.test.ts+5
......@@ -943,6 +943,11 @@ describe("resolveAutoFeatureBranchName", () => {
943943 assert.equal(branch, "feature/fix-toast-copy-3");
944944 });
945945
946 it("treats existing branch names as case-insensitive for collision checks", () => {
947 const branch = resolveAutoFeatureBranchName(["Feature/Ticket-1"], "feature/ticket-1");
948 assert.equal(branch, "feature/ticket-1-2");
949 });
950
946951 it("falls back to feature/update when no preferred name is provided", () => {
947952 const branch = resolveAutoFeatureBranchName(["main"]);
948953 assert.equal(branch, "feature/update");
apps/web/src/components/GitActionsControl.tsx+4-3
......@@ -254,8 +254,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
254254 !skipDefaultBranchPrompt &&
255255 !featureBranch &&
256256 requiresDefaultBranchConfirmation(action, actionIsDefaultBranch) &&
257 actionBranch &&
258 !!actionStatus?.hasWorkingTreeChanges
257 actionBranch
259258 ) {
260259 if (action !== "commit_push" && action !== "commit_push_pr") {
261260 return;
......@@ -416,6 +415,7 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
416415 (actionParams: {
417416 action: GitStackedAction;
418417 commitMessage?: string;
418 forcePushOnlyProgress?: boolean;
419419 onConfirmed?: () => void;
420420 }) => {
421421 void runGitActionWithToast({
......@@ -429,11 +429,12 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
429429
430430 const checkoutFeatureBranchAndContinuePendingAction = useCallback(() => {
431431 if (!pendingDefaultBranchAction) return;
432 const { action, commitMessage, onConfirmed } = pendingDefaultBranchAction;
432 const { action, commitMessage, forcePushOnlyProgress, onConfirmed } = pendingDefaultBranchAction;
433433 setPendingDefaultBranchAction(null);
434434 checkoutNewBranchAndRunAction({
435435 action,
436436 ...(commitMessage ? { commitMessage } : {}),
437 forcePushOnlyProgress,
437438 ...(onConfirmed ? { onConfirmed } : {}),
438439 });
439440 }, [pendingDefaultBranchAction, checkoutNewBranchAndRunAction]);
apps/web/src/lib/gitReactQuery.ts-20
......@@ -16,8 +16,6 @@ export const gitQueryKeys = {
1616export const gitMutationKeys = {
1717 init: (cwd: string | null) => ["git", "mutation", "init", cwd] as const,
1818 checkout: (cwd: string | null) => ["git", "mutation", "checkout", cwd] as const,
19 createBranchAndCheckout: (cwd: string | null) =>
20 ["git", "mutation", "create-branch-and-checkout", cwd] as const,
2119 runStackedAction: (cwd: string | null) => ["git", "mutation", "run-stacked-action", cwd] as const,
2220 pull: (cwd: string | null) => ["git", "mutation", "pull", cwd] as const,
2321};
......@@ -89,24 +87,6 @@ export function gitCheckoutMutationOptions(input: {
8987 });
9088}
9189
92export function gitCreateBranchAndCheckoutMutationOptions(input: {
93 cwd: string | null;
94 queryClient: QueryClient;
95}) {
96 return mutationOptions({
97 mutationKey: gitMutationKeys.createBranchAndCheckout(input.cwd),
98 mutationFn: async (branch: string) => {
99 const api = ensureNativeApi();
100 if (!input.cwd) throw new Error("Git branch creation is unavailable.");
101 await api.git.createBranch({ cwd: input.cwd, branch });
102 return api.git.checkout({ cwd: input.cwd, branch });
103 },
104 onSuccess: async () => {
105 await invalidateGitQueries(input.queryClient);
106 },
107 });
108}
109
11090export function gitRunStackedActionMutationOptions(input: {
11191 cwd: string | null;
11292 queryClient: QueryClient;
packages/shared/src/git.ts+1-1
......@@ -46,7 +46,7 @@ export function resolveAutoFeatureBranchName(
4646 const resolvedBase = sanitizeFeatureBranchName(
4747 preferred && preferred.length > 0 ? preferred : AUTO_FEATURE_BRANCH_FALLBACK,
4848 );
49 const existingNames = new Set(existingBranchNames);
49 const existingNames = new Set(existingBranchNames.map((branch) => branch.toLowerCase()));
5050
5151 if (!existingNames.has(resolvedBase)) {
5252 return resolvedBase;