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) => {...@@ -913,6 +913,41 @@ it.layer(TestLayer)("git integration", (it) => {
913 expect(renamed.branch).toBe("t3code/feat/session-2");913 expect(renamed.branch).toBe("t3code/feat/session-2");
914 }),914 }),
915 );915 );
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 );
916 });951 });
917952
918 // ── createGitWorktree + removeGitWorktree ──953 // ── createGitWorktree + removeGitWorktree ──
apps/server/src/git/Layers/GitCore.ts+2-2
...@@ -282,7 +282,7 @@ const makeGitCore = Effect.gen(function* () {...@@ -282,7 +282,7 @@ const makeGitCore = Effect.gen(function* () {
282 return yield* createGitCommandError(282 return yield* createGitCommandError(
283 "GitCore.renameBranch",283 "GitCore.renameBranch",
284 cwd,284 cwd,
285 ["branch", "-m", desiredBranch],285 ["branch", "-m", "--", desiredBranch],
286 `Could not find an available branch name for '${desiredBranch}'.`,286 `Could not find an available branch name for '${desiredBranch}'.`,
287 );287 );
288 });288 });
...@@ -1075,7 +1075,7 @@ const makeGitCore = Effect.gen(function* () {...@@ -1075,7 +1075,7 @@ const makeGitCore = Effect.gen(function* () {
1075 yield* executeGit(1075 yield* executeGit(
1076 "GitCore.renameBranch",1076 "GitCore.renameBranch",
1077 input.cwd,1077 input.cwd,
1078 ["branch", "-m", input.oldBranch, targetBranch],1078 ["branch", "-m", "--", input.oldBranch, targetBranch],
1079 {1079 {
1080 timeoutMs: 10_000,1080 timeoutMs: 10_000,
1081 fallbackErrorMessage: "git branch rename failed",1081 fallbackErrorMessage: "git branch rename failed",
apps/server/src/git/Services/GitCore.ts+10-2
...@@ -17,8 +17,6 @@ import type {...@@ -17,8 +17,6 @@ import type {
17 GitListBranchesInput,17 GitListBranchesInput,
18 GitListBranchesResult,18 GitListBranchesResult,
19 GitPullResult,19 GitPullResult,
20 GitRenameBranchInput,
21 GitRenameBranchResult,
22 GitRemoveWorktreeInput,20 GitRemoveWorktreeInput,
23 GitStatusInput,21 GitStatusInput,
24 GitStatusResult,22 GitStatusResult,
...@@ -48,6 +46,16 @@ export interface GitRangeContext {...@@ -48,6 +46,16 @@ export interface GitRangeContext {
48 diffPatch: string;46 diffPatch: string;
49}47}
5048
49export interface GitRenameBranchInput {
50 cwd: string;
51 oldBranch: string;
52 newBranch: string;
53}
54
55export interface GitRenameBranchResult {
56 branch: string;
57}
58
51/**59/**
52 * GitCoreShape - Service API for low-level Git repository interactions.60 * GitCoreShape - Service API for low-level Git repository interactions.
53 */61 */
apps/server/src/wsServer.test.ts+1-15
...@@ -371,7 +371,7 @@ describe("WebSocket Server", () => {...@@ -371,7 +371,7 @@ describe("WebSocket Server", () => {
371 gitManager?: GitManagerShape;371 gitManager?: GitManagerShape;
372 gitCore?: Pick<372 gitCore?: Pick<
373 GitCoreShape,373 GitCoreShape,
374 "listBranches" | "initRepo" | "pullCurrentBranch" | "renameBranch"374 "listBranches" | "initRepo" | "pullCurrentBranch"
375 >;375 >;
376 terminalManager?: TerminalManagerShape;376 terminalManager?: TerminalManagerShape;
377 } = {},377 } = {},
...@@ -1471,7 +1471,6 @@ describe("WebSocket Server", () => {...@@ -1471,7 +1471,6 @@ describe("WebSocket Server", () => {
1471 }),1471 }),
1472 );1472 );
1473 const initRepo = vi.fn(() => Effect.void);1473 const initRepo = vi.fn(() => Effect.void);
1474 const renameBranch = vi.fn(() => Effect.succeed({ branch: "t3code/new-name" }));
1475 const pullCurrentBranch = vi.fn(() =>1474 const pullCurrentBranch = vi.fn(() =>
1476 Effect.fail(1475 Effect.fail(
1477 new GitCommandError({1476 new GitCommandError({
...@@ -1488,7 +1487,6 @@ describe("WebSocket Server", () => {...@@ -1488,7 +1487,6 @@ describe("WebSocket Server", () => {
1488 gitCore: {1487 gitCore: {
1489 listBranches,1488 listBranches,
1490 initRepo,1489 initRepo,
1491 renameBranch,
1492 pullCurrentBranch,1490 pullCurrentBranch,
1493 },1491 },
1494 });1492 });
...@@ -1513,18 +1511,6 @@ describe("WebSocket Server", () => {...@@ -1513,18 +1511,6 @@ describe("WebSocket Server", () => {
1513 expect(pullResponse.error?.message).toContain("No upstream configured");1511 expect(pullResponse.error?.message).toContain("No upstream configured");
1514 expect(pullCurrentBranch).toHaveBeenCalledWith("/repo/path");1512 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 });
1528 });1514 });
15291515
1530 it("supports git.status over websocket", async () => {1516 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<...@@ -572,11 +572,6 @@ export const createServer = Effect.fn(function* (): Effect.fn.Return<
572 return yield* git.createWorktree(body);572 return yield* git.createWorktree(body);
573 }573 }
574574
575 case WS_METHODS.gitRenameBranch: {
576 const body = stripRequestTag(request.body);
577 return yield* git.renameBranch(body);
578 }
579
580 case WS_METHODS.gitRemoveWorktree: {575 case WS_METHODS.gitRemoveWorktree: {
581 const body = stripRequestTag(request.body);576 const body = stripRequestTag(request.body);
582 return yield* git.removeWorktree(body);577 return yield* git.removeWorktree(body);
apps/web/src/wsNativeApi.ts-1
...@@ -164,7 +164,6 @@ export function createWsNativeApi(): NativeApi {...@@ -164,7 +164,6 @@ export function createWsNativeApi(): NativeApi {
164 runStackedAction: (input) => transport.request(WS_METHODS.gitRunStackedAction, input),164 runStackedAction: (input) => transport.request(WS_METHODS.gitRunStackedAction, input),
165 listBranches: (input) => transport.request(WS_METHODS.gitListBranches, input),165 listBranches: (input) => transport.request(WS_METHODS.gitListBranches, input),
166 createWorktree: (input) => transport.request(WS_METHODS.gitCreateWorktree, input),166 createWorktree: (input) => transport.request(WS_METHODS.gitCreateWorktree, input),
167 renameBranch: (input) => transport.request(WS_METHODS.gitRenameBranch, input),
168 removeWorktree: (input) => transport.request(WS_METHODS.gitRemoveWorktree, input),167 removeWorktree: (input) => transport.request(WS_METHODS.gitRemoveWorktree, input),
169 createBranch: (input) => transport.request(WS_METHODS.gitCreateBranch, input),168 createBranch: (input) => transport.request(WS_METHODS.gitCreateBranch, input),
170 checkout: (input) => transport.request(WS_METHODS.gitCheckout, input),169 checkout: (input) => transport.request(WS_METHODS.gitCheckout, input),
packages/contracts/src/git.ts-42
...@@ -1,6 +1,5 @@...@@ -1,6 +1,5 @@
1import { Schema } from "effect";1import { Schema } from "effect";
2import { NonNegativeInt, PositiveInt, TrimmedNonEmptyString } from "./baseSchemas";2import { NonNegativeInt, PositiveInt, TrimmedNonEmptyString } from "./baseSchemas";
3import { ChatAttachment, PROVIDER_SEND_TURN_MAX_ATTACHMENTS } from "./orchestration";
43
5const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString;4const TrimmedNonEmptyStringSchema = TrimmedNonEmptyString;
65
...@@ -70,32 +69,6 @@ export const GitCreateWorktreeInput = Schema.Struct({...@@ -70,32 +69,6 @@ export const GitCreateWorktreeInput = Schema.Struct({
70});69});
71export type GitCreateWorktreeInput = typeof GitCreateWorktreeInput.Type;70export 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
99export const GitRemoveWorktreeInput = Schema.Struct({72export const GitRemoveWorktreeInput = Schema.Struct({
100 cwd: TrimmedNonEmptyStringSchema,73 cwd: TrimmedNonEmptyStringSchema,
101 path: TrimmedNonEmptyStringSchema,74 path: TrimmedNonEmptyStringSchema,
...@@ -164,21 +137,6 @@ export const GitCreateWorktreeResult = Schema.Struct({...@@ -164,21 +137,6 @@ export const GitCreateWorktreeResult = Schema.Struct({
164});137});
165export type GitCreateWorktreeResult = typeof GitCreateWorktreeResult.Type;138export 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
182export const GitRunStackedActionResult = Schema.Struct({140export const GitRunStackedActionResult = Schema.Struct({
183 action: GitStackedAction,141 action: GitStackedAction,
184 commit: Schema.Struct({142 commit: Schema.Struct({
packages/contracts/src/ipc.ts-3
...@@ -1,8 +1,6 @@...@@ -1,8 +1,6 @@
1import type {1import type {
2 GitCheckoutInput,2 GitCheckoutInput,
3 GitCreateBranchInput,3 GitCreateBranchInput,
4 GitRenameBranchInput,
5 GitRenameBranchResult,
6 GitCreateWorktreeInput,4 GitCreateWorktreeInput,
7 GitCreateWorktreeResult,5 GitCreateWorktreeResult,
8 GitInitInput,6 GitInitInput,
...@@ -76,7 +74,6 @@ export interface NativeApi {...@@ -76,7 +74,6 @@ export interface NativeApi {
76 // Existing branch/worktree API74 // Existing branch/worktree API
77 listBranches: (input: GitListBranchesInput) => Promise<GitListBranchesResult>;75 listBranches: (input: GitListBranchesInput) => Promise<GitListBranchesResult>;
78 createWorktree: (input: GitCreateWorktreeInput) => Promise<GitCreateWorktreeResult>;76 createWorktree: (input: GitCreateWorktreeInput) => Promise<GitCreateWorktreeResult>;
79 renameBranch: (input: GitRenameBranchInput) => Promise<GitRenameBranchResult>;
80 removeWorktree: (input: GitRemoveWorktreeInput) => Promise<void>;77 removeWorktree: (input: GitRemoveWorktreeInput) => Promise<void>;
81 createBranch: (input: GitCreateBranchInput) => Promise<void>;78 createBranch: (input: GitCreateBranchInput) => Promise<void>;
82 checkout: (input: GitCheckoutInput) => Promise<void>;79 checkout: (input: GitCheckoutInput) => Promise<void>;
packages/contracts/src/ws.ts-3
...@@ -16,7 +16,6 @@ import {...@@ -16,7 +16,6 @@ import {
16 GitInitInput,16 GitInitInput,
17 GitListBranchesInput,17 GitListBranchesInput,
18 GitPullInput,18 GitPullInput,
19 GitRenameBranchInput,
20 GitRemoveWorktreeInput,19 GitRemoveWorktreeInput,
21 GitRunStackedActionInput,20 GitRunStackedActionInput,
22 GitStatusInput,21 GitStatusInput,
...@@ -51,7 +50,6 @@ export const WS_METHODS = {...@@ -51,7 +50,6 @@ export const WS_METHODS = {
51 gitRunStackedAction: "git.runStackedAction",50 gitRunStackedAction: "git.runStackedAction",
52 gitListBranches: "git.listBranches",51 gitListBranches: "git.listBranches",
53 gitCreateWorktree: "git.createWorktree",52 gitCreateWorktree: "git.createWorktree",
54 gitRenameBranch: "git.renameBranch",
55 gitRemoveWorktree: "git.removeWorktree",53 gitRemoveWorktree: "git.removeWorktree",
56 gitCreateBranch: "git.createBranch",54 gitCreateBranch: "git.createBranch",
57 gitCheckout: "git.checkout",55 gitCheckout: "git.checkout",
...@@ -113,7 +111,6 @@ export const WebSocketRequestBody = Schema.Union([...@@ -113,7 +111,6 @@ export const WebSocketRequestBody = Schema.Union([
113 tagRequestBody(WS_METHODS.gitRunStackedAction, GitRunStackedActionInput),111 tagRequestBody(WS_METHODS.gitRunStackedAction, GitRunStackedActionInput),
114 tagRequestBody(WS_METHODS.gitListBranches, GitListBranchesInput),112 tagRequestBody(WS_METHODS.gitListBranches, GitListBranchesInput),
115 tagRequestBody(WS_METHODS.gitCreateWorktree, GitCreateWorktreeInput),113 tagRequestBody(WS_METHODS.gitCreateWorktree, GitCreateWorktreeInput),
116 tagRequestBody(WS_METHODS.gitRenameBranch, GitRenameBranchInput),
117 tagRequestBody(WS_METHODS.gitRemoveWorktree, GitRemoveWorktreeInput),114 tagRequestBody(WS_METHODS.gitRemoveWorktree, GitRemoveWorktreeInput),
118 tagRequestBody(WS_METHODS.gitCreateBranch, GitCreateBranchInput),115 tagRequestBody(WS_METHODS.gitCreateBranch, GitCreateBranchInput),
119 tagRequestBody(WS_METHODS.gitCheckout, GitCheckoutInput),116 tagRequestBody(WS_METHODS.gitCheckout, GitCheckoutInput),