authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-02-28 22:35:09-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-01 20:06:21-08:00
log82681606d0a4c0fecf5487751812487abb489c4c
tree1e59a3947385d4a4d1d9deb8584733302e7d3017
parentc91b79b6e2e6a77bd44a301689e995288f080a79

Remove git.renameBranch RPC and harden branch rename args

- remove `git.renameBranch` from WS/contracts/native API surface and update server/web tests - use `git branch -m -- ...` in GitCore to safely separate branch rename arguments

9 files changed, 48 insertions(+), 73 deletions(-)

apps/server/src/git/Layers/GitCore.test.ts+35
......@@ -913,6 +913,41 @@ it.layer(TestLayer)("git integration", (it) => {
913913 expect(renamed.branch).toBe("t3code/feat/session-2");
914914 }),
915915 );
916
917 it.effect("uses '--' separator for branch rename arguments", () =>
918 Effect.gen(function* () {
919 const tmp = yield* makeTmpDir();
920 yield* initRepoWithCommit(tmp);
921 yield* createGitBranch({ cwd: tmp, branch: "feature/old-name" });
922 yield* checkoutGitBranch({ cwd: tmp, branch: "feature/old-name" });
923
924 const realGitService = yield* GitService;
925 let renameArgs: ReadonlyArray<string> | null = null;
926 const core = yield* makeIsolatedGitCore({
927 execute: (input) => {
928 if (input.args[0] === "branch" && input.args[1] === "-m") {
929 renameArgs = [...input.args];
930 }
931 return realGitService.execute(input);
932 },
933 });
934
935 const renamed = yield* core.renameBranch({
936 cwd: tmp,
937 oldBranch: "feature/old-name",
938 newBranch: "feature/new-name",
939 });
940
941 expect(renamed.branch).toBe("feature/new-name");
942 expect(renameArgs).toEqual([
943 "branch",
944 "-m",
945 "--",
946 "feature/old-name",
947 "feature/new-name",
948 ]);
949 }),
950 );
916951 });
917952
918953 // ── createGitWorktree + removeGitWorktree ──
apps/server/src/git/Layers/GitCore.ts+2-2
......@@ -282,7 +282,7 @@ const makeGitCore = Effect.gen(function* () {
282282 return yield* createGitCommandError(
283283 "GitCore.renameBranch",
284284 cwd,
285 ["branch", "-m", desiredBranch],
285 ["branch", "-m", "--", desiredBranch],
286286 `Could not find an available branch name for '${desiredBranch}'.`,
287287 );
288288 });
......@@ -1075,7 +1075,7 @@ const makeGitCore = Effect.gen(function* () {
10751075 yield* executeGit(
10761076 "GitCore.renameBranch",
10771077 input.cwd,
1078 ["branch", "-m", input.oldBranch, targetBranch],
1078 ["branch", "-m", "--", input.oldBranch, targetBranch],
10791079 {
10801080 timeoutMs: 10_000,
10811081 fallbackErrorMessage: "git branch rename failed",
apps/server/src/git/Services/GitCore.ts+10-2
......@@ -17,8 +17,6 @@ import type {
1717 GitListBranchesInput,
1818 GitListBranchesResult,
1919 GitPullResult,
20 GitRenameBranchInput,
21 GitRenameBranchResult,
2220 GitRemoveWorktreeInput,
2321 GitStatusInput,
2422 GitStatusResult,
......@@ -48,6 +46,16 @@ export interface GitRangeContext {
4846 diffPatch: string;
4947}
5048
49export interface GitRenameBranchInput {
50 cwd: string;
51 oldBranch: string;
52 newBranch: string;
53}
54
55export interface GitRenameBranchResult {
56 branch: string;
57}
58
5159/**
5260 * GitCoreShape - Service API for low-level Git repository interactions.
5361 */
apps/server/src/wsServer.test.ts+1-15
......@@ -371,7 +371,7 @@ describe("WebSocket Server", () => {
371371 gitManager?: GitManagerShape;
372372 gitCore?: Pick<
373373 GitCoreShape,
374 "listBranches" | "initRepo" | "pullCurrentBranch" | "renameBranch"
374 "listBranches" | "initRepo" | "pullCurrentBranch"
375375 >;
376376 terminalManager?: TerminalManagerShape;
377377 } = {},
......@@ -1471,7 +1471,6 @@ describe("WebSocket Server", () => {
14711471 }),
14721472 );
14731473 const initRepo = vi.fn(() => Effect.void);
1474 const renameBranch = vi.fn(() => Effect.succeed({ branch: "t3code/new-name" }));
14751474 const pullCurrentBranch = vi.fn(() =>
14761475 Effect.fail(
14771476 new GitCommandError({
......@@ -1488,7 +1487,6 @@ describe("WebSocket Server", () => {
14881487 gitCore: {
14891488 listBranches,
14901489 initRepo,
1491 renameBranch,
14921490 pullCurrentBranch,
14931491 },
14941492 });
......@@ -1513,18 +1511,6 @@ describe("WebSocket Server", () => {
15131511 expect(pullResponse.error?.message).toContain("No upstream configured");
15141512 expect(pullCurrentBranch).toHaveBeenCalledWith("/repo/path");
15151513
1516 const renameResponse = await sendRequest(ws, WS_METHODS.gitRenameBranch, {
1517 cwd: "/repo/path",
1518 oldBranch: "t3code/tmp-456",
1519 newBranch: "t3code/new-name",
1520 });
1521 expect(renameResponse.error).toBeUndefined();
1522 expect(renameResponse.result).toEqual({ branch: "t3code/new-name" });
1523 expect(renameBranch).toHaveBeenCalledWith({
1524 cwd: "/repo/path",
1525 oldBranch: "t3code/tmp-456",
1526 newBranch: "t3code/new-name",
1527 });
15281514 });
15291515
15301516 it("supports git.status over websocket", async () => {
apps/server/src/wsServer.ts-5
......@@ -572,11 +572,6 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
572572 return yield* git.createWorktree(body);
573573 }
574574
575 case WS_METHODS.gitRenameBranch: {
576 const body = stripRequestTag(request.body);
577 return yield* git.renameBranch(body);
578 }
579
580575 case WS_METHODS.gitRemoveWorktree: {
581576 const body = stripRequestTag(request.body);
582577 return yield* git.removeWorktree(body);
apps/web/src/wsNativeApi.ts-1
......@@ -164,7 +164,6 @@ export function createWsNativeApi(): NativeApi {
164164 runStackedAction: (input) => transport.request(WS_METHODS.gitRunStackedAction, input),
165165 listBranches: (input) => transport.request(WS_METHODS.gitListBranches, input),
166166 createWorktree: (input) => transport.request(WS_METHODS.gitCreateWorktree, input),
167 renameBranch: (input) => transport.request(WS_METHODS.gitRenameBranch, input),
168167 removeWorktree: (input) => transport.request(WS_METHODS.gitRemoveWorktree, input),
169168 createBranch: (input) => transport.request(WS_METHODS.gitCreateBranch, input),
170169 checkout: (input) => transport.request(WS_METHODS.gitCheckout, input),
packages/contracts/src/git.ts-42
......@@ -1,6 +1,5 @@
11import { Schema } from "effect";
22import { NonNegativeInt, PositiveInt, TrimmedNonEmptyString } from "./baseSchemas";
3import { ChatAttachment, PROVIDER_SEND_TURN_MAX_ATTACHMENTS } from "./orchestration";
43
54const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString;
65
......@@ -70,32 +69,6 @@ export const GitCreateWorktreeInput = Schema.Struct({
7069});
7170export type GitCreateWorktreeInput = typeof GitCreateWorktreeInput.Type;
7271
73export const GitGenerateBranchNameInput = Schema.Struct({
74 cwd: TrimmedNonEmptyStringSchema,
75 message: TrimmedNonEmptyStringSchema,
76 attachments: Schema.optional(
77 Schema.Array(ChatAttachment).check(Schema.isMaxLength(PROVIDER_SEND_TURN_MAX_ATTACHMENTS)),
78 ),
79});
80export type GitGenerateBranchNameInput = typeof GitGenerateBranchNameInput.Type;
81
82export const GitGenerateAndRenameBranchInput = Schema.Struct({
83 cwd: TrimmedNonEmptyStringSchema,
84 oldBranch: TrimmedNonEmptyStringSchema,
85 message: TrimmedNonEmptyStringSchema,
86 attachments: Schema.optional(
87 Schema.Array(ChatAttachment).check(Schema.isMaxLength(PROVIDER_SEND_TURN_MAX_ATTACHMENTS)),
88 ),
89});
90export type GitGenerateAndRenameBranchInput = typeof GitGenerateAndRenameBranchInput.Type;
91
92export const GitRenameBranchInput = Schema.Struct({
93 cwd: TrimmedNonEmptyStringSchema,
94 oldBranch: TrimmedNonEmptyStringSchema,
95 newBranch: TrimmedNonEmptyStringSchema,
96});
97export type GitRenameBranchInput = typeof GitRenameBranchInput.Type;
98
9972export const GitRemoveWorktreeInput = Schema.Struct({
10073 cwd: TrimmedNonEmptyStringSchema,
10174 path: TrimmedNonEmptyStringSchema,
......@@ -164,21 +137,6 @@ export const GitCreateWorktreeResult = Schema.Struct({
164137});
165138export type GitCreateWorktreeResult = typeof GitCreateWorktreeResult.Type;
166139
167export const GitGenerateBranchNameResult = Schema.Struct({
168 branch: Schema.NullOr(TrimmedNonEmptyStringSchema),
169});
170export type GitGenerateBranchNameResult = typeof GitGenerateBranchNameResult.Type;
171
172export const GitGenerateAndRenameBranchResult = Schema.Struct({
173 branch: Schema.NullOr(TrimmedNonEmptyStringSchema),
174});
175export type GitGenerateAndRenameBranchResult = typeof GitGenerateAndRenameBranchResult.Type;
176
177export const GitRenameBranchResult = Schema.Struct({
178 branch: TrimmedNonEmptyStringSchema,
179});
180export type GitRenameBranchResult = typeof GitRenameBranchResult.Type;
181
182140export const GitRunStackedActionResult = Schema.Struct({
183141 action: GitStackedAction,
184142 commit: Schema.Struct({
packages/contracts/src/ipc.ts-3
......@@ -1,8 +1,6 @@
11import type {
22 GitCheckoutInput,
33 GitCreateBranchInput,
4 GitRenameBranchInput,
5 GitRenameBranchResult,
64 GitCreateWorktreeInput,
75 GitCreateWorktreeResult,
86 GitInitInput,
......@@ -76,7 +74,6 @@ export interface NativeApi {
7674 // Existing branch/worktree API
7775 listBranches: (input: GitListBranchesInput) => Promise<GitListBranchesResult>;
7876 createWorktree: (input: GitCreateWorktreeInput) => Promise<GitCreateWorktreeResult>;
79 renameBranch: (input: GitRenameBranchInput) => Promise<GitRenameBranchResult>;
8077 removeWorktree: (input: GitRemoveWorktreeInput) => Promise<void>;
8178 createBranch: (input: GitCreateBranchInput) => Promise<void>;
8279 checkout: (input: GitCheckoutInput) => Promise<void>;
packages/contracts/src/ws.ts-3
......@@ -16,7 +16,6 @@ import {
1616 GitInitInput,
1717 GitListBranchesInput,
1818 GitPullInput,
19 GitRenameBranchInput,
2019 GitRemoveWorktreeInput,
2120 GitRunStackedActionInput,
2221 GitStatusInput,
......@@ -51,7 +50,6 @@ export const WS_METHODS = {
5150 gitRunStackedAction: "git.runStackedAction",
5251 gitListBranches: "git.listBranches",
5352 gitCreateWorktree: "git.createWorktree",
54 gitRenameBranch: "git.renameBranch",
5553 gitRemoveWorktree: "git.removeWorktree",
5654 gitCreateBranch: "git.createBranch",
5755 gitCheckout: "git.checkout",
......@@ -113,7 +111,6 @@ export const WebSocketRequestBody = Schema.Union([
113111 tagRequestBody(WS_METHODS.gitRunStackedAction, GitRunStackedActionInput),
114112 tagRequestBody(WS_METHODS.gitListBranches, GitListBranchesInput),
115113 tagRequestBody(WS_METHODS.gitCreateWorktree, GitCreateWorktreeInput),
116 tagRequestBody(WS_METHODS.gitRenameBranch, GitRenameBranchInput),
117114 tagRequestBody(WS_METHODS.gitRemoveWorktree, GitRemoveWorktreeInput),
118115 tagRequestBody(WS_METHODS.gitCreateBranch, GitCreateBranchInput),
119116 tagRequestBody(WS_METHODS.gitCheckout, GitCheckoutInput),