Skip to content
Merged
1 change: 1 addition & 0 deletions .github/VOUCHED.td
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ github:realAhmedRoach
github:shiroyasha9
github:Yash-Singh1
github:eggfriedrice24
github:Ymit24
41 changes: 40 additions & 1 deletion apps/server/src/git/Layers/GitCore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ const makeIsolatedGitCore = (gitService: GitServiceShape) =>
return {
status: (input) => core.status(input),
statusDetails: (cwd) => core.statusDetails(cwd),
prepareCommitContext: (cwd) => core.prepareCommitContext(cwd),
prepareCommitContext: (cwd, filePaths?) => core.prepareCommitContext(cwd, filePaths),
commit: (cwd, subject, body) => core.commit(cwd, subject, body),
pushCurrentBranch: (cwd, fallbackBranch) => core.pushCurrentBranch(cwd, fallbackBranch),
pullCurrentBranch: (cwd) => core.pullCurrentBranch(cwd),
Expand Down Expand Up @@ -1712,6 +1712,45 @@ it.layer(TestLayer)("git integration", (it) => {
}),
);

it.effect("prepareCommitContext stages only selected files when filePaths provided", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
yield* initRepoWithCommit(tmp);
const core = yield* GitCore;

yield* writeTextFile(path.join(tmp, "a.txt"), "file a\n");
yield* writeTextFile(path.join(tmp, "b.txt"), "file b\n");

const context = yield* core.prepareCommitContext(tmp, ["a.txt"]);
expect(context).not.toBeNull();
expect(context!.stagedSummary).toContain("a.txt");
expect(context!.stagedSummary).not.toContain("b.txt");

yield* core.commit(tmp, "Add only a.txt", "");

// b.txt should still be untracked after commit
const statusAfter = yield* git(tmp, ["status", "--porcelain"]);
expect(statusAfter).toContain("b.txt");
expect(statusAfter).not.toContain("a.txt");
}),
);

it.effect("prepareCommitContext stages everything when filePaths is undefined", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
yield* initRepoWithCommit(tmp);
const core = yield* GitCore;

yield* writeTextFile(path.join(tmp, "a.txt"), "file a\n");
yield* writeTextFile(path.join(tmp, "b.txt"), "file b\n");

const context = yield* core.prepareCommitContext(tmp);
expect(context).not.toBeNull();
expect(context!.stagedSummary).toContain("a.txt");
expect(context!.stagedSummary).toContain("b.txt");
}),
);

it.effect("pushes with upstream setup and then skips when up to date", () =>
Effect.gen(function* () {
const tmp = yield* makeTmpDir();
Expand Down
16 changes: 14 additions & 2 deletions apps/server/src/git/Layers/GitCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -846,9 +846,21 @@ const makeGitCore = Effect.gen(function* () {
})),
);

const prepareCommitContext: GitCoreShape["prepareCommitContext"] = (cwd) =>
const prepareCommitContext: GitCoreShape["prepareCommitContext"] = (cwd, filePaths) =>
Effect.gen(function* () {
yield* runGit("GitCore.prepareCommitContext.addAll", cwd, ["add", "-A"]);
if (filePaths && filePaths.length > 0) {
yield* runGit("GitCore.prepareCommitContext.reset", cwd, ["reset"]).pipe(
Effect.catch(() => Effect.void),
);
yield* runGit("GitCore.prepareCommitContext.addSelected", cwd, [
"add",
"-A",
"--",
...filePaths,
]);
} else {
yield* runGit("GitCore.prepareCommitContext.addAll", cwd, ["add", "-A"]);
}

const stagedSummary = yield* runGitStdout("GitCore.prepareCommitContext.stagedSummary", cwd, [
"diff",
Expand Down
26 changes: 26 additions & 0 deletions apps/server/src/git/Layers/GitManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ function runStackedAction(
action: "commit" | "commit_push" | "commit_push_pr";
commitMessage?: string;
featureBranch?: boolean;
filePaths?: readonly string[];
},
) {
return manager.runStackedAction(input);
Expand Down Expand Up @@ -767,6 +768,31 @@ it.layer(GitManagerTestLayer)("GitManager", (it) => {
}),
);

it.effect("commits only selected files when filePaths is provided", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
yield* initRepo(repoDir);
fs.writeFileSync(path.join(repoDir, "a.txt"), "file a\n");
fs.writeFileSync(path.join(repoDir, "b.txt"), "file b\n");

const { manager } = yield* makeManager();
const result = yield* runStackedAction(manager, {
cwd: repoDir,
action: "commit",
filePaths: ["a.txt"],
});

expect(result.commit.status).toBe("created");

// b.txt should remain in the working tree
const statusStdout = yield* runGit(repoDir, ["status", "--porcelain"]).pipe(
Effect.map((r) => r.stdout),
);
expect(statusStdout).toContain("b.txt");
expect(statusStdout).not.toContain("a.txt");
}),
);

it.effect("creates feature branch, commits, and pushes with featureBranch option", () =>
Effect.gen(function* () {
const repoDir = yield* makeTempDir("t3code-git-manager-");
Expand Down
15 changes: 13 additions & 2 deletions apps/server/src/git/Layers/GitManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -638,9 +638,10 @@ export const makeGitManager = Effect.gen(function* () {
commitMessage?: string;
/** When true, also produce a semantic feature branch name. */
includeBranch?: boolean;
filePaths?: readonly string[];
}) =>
Effect.gen(function* () {
const context = yield* gitCore.prepareCommitContext(input.cwd);
const context = yield* gitCore.prepareCommitContext(input.cwd, input.filePaths);
if (!context) {
return null;
}
Expand Down Expand Up @@ -680,6 +681,7 @@ export const makeGitManager = Effect.gen(function* () {
branch: string | null,
commitMessage?: string,
preResolvedSuggestion?: CommitAndBranchSuggestion,
filePaths?: readonly string[],
) =>
Effect.gen(function* () {
const suggestion =
Expand All @@ -688,6 +690,7 @@ export const makeGitManager = Effect.gen(function* () {
cwd,
branch,
...(commitMessage ? { commitMessage } : {}),
...(filePaths ? { filePaths } : {}),
}));
if (!suggestion) {
return { status: "skipped_no_changes" as const };
Expand Down Expand Up @@ -971,12 +974,18 @@ export const makeGitManager = Effect.gen(function* () {
},
);

const runFeatureBranchStep = (cwd: string, branch: string | null, commitMessage?: string) =>
const runFeatureBranchStep = (
cwd: string,
branch: string | null,
commitMessage?: string,
filePaths?: readonly string[],
) =>
Effect.gen(function* () {
const suggestion = yield* resolveCommitAndBranchSuggestion({
cwd,
branch,
...(commitMessage ? { commitMessage } : {}),
...(filePaths ? { filePaths } : {}),
includeBranch: true,
});
if (!suggestion) {
Expand Down Expand Up @@ -1025,6 +1034,7 @@ export const makeGitManager = Effect.gen(function* () {
input.cwd,
initialStatus.branch,
input.commitMessage,
input.filePaths,
);
branchStep = result.branchStep;
commitMessageForStep = result.resolvedCommitMessage;
Expand All @@ -1040,6 +1050,7 @@ export const makeGitManager = Effect.gen(function* () {
currentBranch,
commitMessageForStep,
preResolvedCommitSuggestion,
input.filePaths,
);

const push = wantsPush
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/git/Services/GitCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export interface GitCoreShape {
*/
readonly prepareCommitContext: (
cwd: string,
filePaths?: readonly string[],
) => Effect.Effect<GitPreparedCommitContext | null, GitCommandError>;

/**
Expand Down
Loading
Loading