authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-01 19:50:05-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-01 20:06:01-08:00
logba03241df35b56a43c8f676a0b12c70b04ce004c
treecdd8f5fb550c94f3c5959759d513e79dcc2962c4
parenta8059dc2c0341e72efab496c88b15b8dadac71e2

fix: address latest git action review feedback

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

4 files changed, 76 insertions(+), 15 deletions(-)

apps/server/src/git/Layers/GitCore.test.ts+36
......@@ -1234,6 +1234,42 @@ it.layer(TestLayer)("git integration", (it) => {
12341234 }),
12351235 );
12361236
1237 it.effect(
1238 "pushes with upstream setup when comparable base exists but remote branch is missing",
1239 () =>
1240 Effect.gen(function* () {
1241 const tmp = yield* makeTmpDir();
1242 const remote = yield* makeTmpDir();
1243 yield* git(remote, ["init", "--bare"]);
1244
1245 yield* initRepoWithCommit(tmp);
1246 const initialBranch = (yield* listGitBranches({ cwd: tmp })).branches.find(
1247 (branch) => branch.current,
1248 )!.name;
1249 yield* git(tmp, ["remote", "add", "origin", remote]);
1250 yield* git(tmp, ["push", "-u", "origin", initialBranch]);
1251
1252 yield* writeTextFile(path.join(tmp, "default-ahead.txt"), "ahead on default\n");
1253 yield* git(tmp, ["add", "default-ahead.txt"]);
1254 yield* git(tmp, ["commit", "-m", "default ahead"]);
1255
1256 const featureBranch = "feature/publish-no-upstream";
1257 yield* git(tmp, ["checkout", "-b", featureBranch]);
1258
1259 const core = yield* GitCore;
1260 const pushed = yield* core.pushCurrentBranch(tmp, null);
1261 expect(pushed.status).toBe("pushed");
1262 expect(pushed.setUpstream).toBe(true);
1263 expect(pushed.upstreamBranch).toBe(`origin/${featureBranch}`);
1264 expect(yield* git(tmp, ["rev-parse", "--abbrev-ref", "@{upstream}"])).toBe(
1265 `origin/${featureBranch}`,
1266 );
1267 expect(yield* git(tmp, ["ls-remote", "--heads", "origin", featureBranch])).toContain(
1268 featureBranch,
1269 );
1270 }),
1271 );
1272
12371273 it.effect("includes command context when worktree removal fails", () =>
12381274 Effect.gen(function* () {
12391275 const tmp = yield* makeTmpDir();
apps/server/src/git/Layers/GitCore.ts+24-4
......@@ -381,6 +381,11 @@ const makeGitCore = Effect.gen(function* () {
381381 },
382382 ).pipe(Effect.map((result) => result.code === 0));
383383
384 const originRemoteExists = (cwd: string): Effect.Effect<boolean, GitCommandError> =>
385 executeGit("GitCore.originRemoteExists", cwd, ["remote", "get-url", "origin"], {
386 allowNonZeroExit: true,
387 }).pipe(Effect.map((result) => result.code === 0));
388
384389 const resolveBaseBranchForNoUpstream = (
385390 cwd: string,
386391 branch: string,
......@@ -668,10 +673,25 @@ const makeGitCore = Effect.gen(function* () {
668673 Effect.catch(() => Effect.succeed(null)),
669674 );
670675 if (comparableBaseBranch) {
671 return {
672 status: "skipped_up_to_date" as const,
673 branch,
674 };
676 const hasOriginRemote = yield* originRemoteExists(cwd).pipe(
677 Effect.catch(() => Effect.succeed(false)),
678 );
679 if (!hasOriginRemote) {
680 return {
681 status: "skipped_up_to_date" as const,
682 branch,
683 };
684 }
685
686 const hasRemoteBranch = yield* remoteBranchExists(cwd, branch).pipe(
687 Effect.catch(() => Effect.succeed(false)),
688 );
689 if (hasRemoteBranch) {
690 return {
691 status: "skipped_up_to_date" as const,
692 branch,
693 };
694 }
675695 }
676696 }
677697
apps/web/src/components/GitActionsControl.logic.ts+3-9
......@@ -31,6 +31,8 @@ export interface DefaultBranchActionDialogCopy {
3131 continueLabel: string;
3232}
3333
34export type DefaultBranchConfirmableAction = "commit_push" | "commit_push_pr";
35
3436const SHORT_SHA_LENGTH = 7;
3537const TOAST_DESCRIPTION_MAX = 72;
3638const AUTO_FEATURE_BRANCH_PREFIX = "feature/stacked";
......@@ -283,21 +285,13 @@ export function requiresDefaultBranchConfirmation(
283285}
284286
285287export function resolveDefaultBranchActionDialogCopy(input: {
286 action: GitStackedAction;
288 action: DefaultBranchConfirmableAction;
287289 branchName: string;
288290 includesCommit: boolean;
289291}): DefaultBranchActionDialogCopy {
290292 const branchLabel = input.branchName;
291293 const suffix = ` on "${branchLabel}". You can continue on this branch or create a feature branch and run the same action there.`;
292294
293 if (input.action === "commit") {
294 return {
295 title: "Commit to default branch?",
296 description: `This action will commit changes${suffix}`,
297 continueLabel: `Commit to ${branchLabel}`,
298 };
299 }
300
301295 if (input.action === "commit_push") {
302296 if (input.includesCommit) {
303297 return {
apps/web/src/components/GitActionsControl.tsx+13-2
......@@ -9,6 +9,7 @@ import {
99 type GitActionIconName,
1010 type GitActionMenuItem,
1111 type GitQuickAction,
12 type DefaultBranchConfirmableAction,
1213 requiresDefaultBranchConfirmation,
1314 resolveAutoFeatureBranchName,
1415 resolveDefaultBranchActionDialogCopy,
......@@ -50,7 +51,7 @@ interface GitActionsControlProps {
5051}
5152
5253interface PendingDefaultBranchAction {
53 action: GitStackedAction;
54 action: DefaultBranchConfirmableAction;
5455 branchName: string;
5556 includesCommit: boolean;
5657 commitMessage?: string;
......@@ -268,6 +269,9 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
268269 requiresDefaultBranchConfirmation(action, actionIsDefaultBranch) &&
269270 actionBranch
270271 ) {
272 if (action !== "commit_push" && action !== "commit_push_pr") {
273 return;
274 }
271275 setPendingDefaultBranchAction({
272276 action,
273277 branchName: actionBranch,
......@@ -578,11 +582,18 @@ export default function GitActionsControl({ gitCwd, activeThreadId }: GitActions
578582 if (!isCommitDialogOpen) return;
579583 const commitMessage = dialogCommitMessage.trim();
580584 setIsCommitDialogOpen(false);
585 setDialogCommitMessage("");
581586 void runGitActionWithToast({
582587 action: "commit",
583588 ...(commitMessage ? { commitMessage } : {}),
584589 });
585 }, [dialogCommitMessage, isCommitDialogOpen, runGitActionWithToast, setIsCommitDialogOpen]);
590 }, [
591 dialogCommitMessage,
592 isCommitDialogOpen,
593 runGitActionWithToast,
594 setDialogCommitMessage,
595 setIsCommitDialogOpen,
596 ]);
586597
587598 const openChangedFileInEditor = useCallback(
588599 (filePath: string) => {