Skip to content

Commit d365a2d

Browse files
committed
feat: add staged file tracking and selective commit support
1 parent 2bfd289 commit d365a2d

22 files changed

Lines changed: 770 additions & 237 deletions

File tree

apps/code/src/main/services/git/create-pr-saga.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface CreatePrSagaInput {
1717
prTitle?: string;
1818
prBody?: string;
1919
draft?: boolean;
20+
stagedOnly?: boolean;
2021
}
2122

2223
export interface CreatePrSagaOutput {
@@ -32,7 +33,11 @@ export interface CreatePrDeps {
3233
): Promise<{ previousBranch: string; currentBranch: string }>;
3334
getChangedFilesHead(dir: string): Promise<ChangedFile[]>;
3435
generateCommitMessage(dir: string): Promise<{ message: string }>;
35-
commit(dir: string, message: string): Promise<CommitOutput>;
36+
commit(
37+
dir: string,
38+
message: string,
39+
stagedOnly?: boolean,
40+
): Promise<CommitOutput>;
3641
getSyncStatus(dir: string): Promise<GitSyncStatus>;
3742
push(dir: string): Promise<PushOutput>;
3843
publish(dir: string): Promise<PublishOutput>;
@@ -120,7 +125,11 @@ export class CreatePrSaga extends Saga<CreatePrSagaInput, CreatePrSagaOutput> {
120125
await this.step({
121126
name: "committing",
122127
execute: async () => {
123-
const result = await this.deps.commit(directoryPath, commitMessage!);
128+
const result = await this.deps.commit(
129+
directoryPath,
130+
commitMessage!,
131+
input.stagedOnly,
132+
);
124133
if (!result.success) throw new Error(result.message);
125134
return result;
126135
},

apps/code/src/main/services/git/schemas.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const changedFileSchema = z.object({
2121
originalPath: z.string().optional(),
2222
linesAdded: z.number().optional(),
2323
linesRemoved: z.number().optional(),
24+
staged: z.boolean().optional(),
2425
});
2526

2627
export type ChangedFile = z.infer<typeof changedFileSchema>;
@@ -120,17 +121,23 @@ export const getFileAtHeadInput = z.object({
120121
});
121122
export const getFileAtHeadOutput = z.string().nullable();
122123

123-
// getDiffHead schemas
124-
export const getDiffHeadInput = z.object({
124+
// Shared diff schemas (getDiffHead, getDiffCached, getDiffUnstaged)
125+
export const diffInput = z.object({
125126
directoryPath: z.string(),
126127
ignoreWhitespace: z.boolean().optional(),
127128
});
128-
export const getDiffHeadOutput = z.string();
129+
export const diffOutput = z.string();
129130

130131
// getDiffStats schemas
131132
export const getDiffStatsInput = directoryPathInput;
132133
export const getDiffStatsOutput = diffStatsSchema;
133134

135+
// stageFiles / unstageFiles shared schema
136+
export const stageFilesInput = z.object({
137+
directoryPath: z.string(),
138+
paths: z.array(z.string()),
139+
});
140+
134141
// getCurrentBranch schemas
135142
export const getCurrentBranchInput = directoryPathInput;
136143
export const getCurrentBranchOutput = z.string().nullable();
@@ -198,6 +205,7 @@ export const commitInput = z.object({
198205
message: z.string(),
199206
paths: z.array(z.string()).optional(),
200207
allowEmpty: z.boolean().optional(),
208+
stagedOnly: z.boolean().optional(),
201209
});
202210

203211
export type CommitInput = z.infer<typeof commitInput>;
@@ -241,6 +249,7 @@ export const createPrInput = z.object({
241249
prTitle: z.string().optional(),
242250
prBody: z.string().optional(),
243251
draft: z.boolean().optional(),
252+
stagedOnly: z.boolean().optional(),
244253
});
245254

246255
export type CreatePrInput = z.infer<typeof createPrInput>;

apps/code/src/main/services/git/service.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
getUnstagedDiff,
2020
fetch as gitFetch,
2121
isGitRepository,
22+
stageFiles,
23+
unstageFiles,
2224
} from "@posthog/git/queries";
2325
import { CreateBranchSaga, SwitchBranchSaga } from "@posthog/git/sagas/branch";
2426
import { CloneSaga } from "@posthog/git/sagas/clone";
@@ -266,6 +268,7 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
266268
originalPath: f.originalPath,
267269
linesAdded: f.linesAdded,
268270
linesRemoved: f.linesRemoved,
271+
staged: f.staged,
269272
}));
270273
}
271274

@@ -283,6 +286,36 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
283286
return getDiffHead(directoryPath, { ignoreWhitespace });
284287
}
285288

289+
public async getDiffCached(
290+
directoryPath: string,
291+
ignoreWhitespace?: boolean,
292+
): Promise<string> {
293+
return getStagedDiff(directoryPath, { ignoreWhitespace });
294+
}
295+
296+
public async getDiffUnstaged(
297+
directoryPath: string,
298+
ignoreWhitespace?: boolean,
299+
): Promise<string> {
300+
return getUnstagedDiff(directoryPath, { ignoreWhitespace });
301+
}
302+
303+
public async stageFiles(
304+
directoryPath: string,
305+
paths: string[],
306+
): Promise<GitStateSnapshot> {
307+
await stageFiles(directoryPath, paths);
308+
return this.getStateSnapshot(directoryPath);
309+
}
310+
311+
public async unstageFiles(
312+
directoryPath: string,
313+
paths: string[],
314+
): Promise<GitStateSnapshot> {
315+
await unstageFiles(directoryPath, paths);
316+
return this.getStateSnapshot(directoryPath);
317+
}
318+
286319
public async getDiffStats(directoryPath: string): Promise<DiffStats> {
287320
const stats = await getDiffStats(directoryPath, {
288321
excludePatterns: [".claude", "CLAUDE.local.md"],
@@ -479,6 +512,7 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
479512
prTitle?: string;
480513
prBody?: string;
481514
draft?: boolean;
515+
stagedOnly?: boolean;
482516
}): Promise<CreatePrOutput> {
483517
const { directoryPath, flowId } = input;
484518

@@ -502,7 +536,7 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
502536
checkoutBranch: (dir, name) => this.checkoutBranch(dir, name),
503537
getChangedFilesHead: (dir) => this.getChangedFilesHead(dir),
504538
generateCommitMessage: (dir) => this.generateCommitMessage(dir),
505-
commit: (dir, msg) => this.commit(dir, msg),
539+
commit: (dir, msg, stagedOnly) => this.commit(dir, msg, { stagedOnly }),
506540
getSyncStatus: (dir) => this.getGitSyncStatus(dir),
507541
push: (dir) => this.push(dir),
508542
publish: (dir) => this.publish(dir),
@@ -521,6 +555,7 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
521555
prTitle: input.prTitle,
522556
prBody: input.prBody,
523557
draft: input.draft,
558+
stagedOnly: input.stagedOnly,
524559
});
525560

526561
if (!result.success) {
@@ -584,8 +619,7 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
584619
public async commit(
585620
directoryPath: string,
586621
message: string,
587-
paths?: string[],
588-
allowEmpty?: boolean,
622+
options?: { paths?: string[]; allowEmpty?: boolean; stagedOnly?: boolean },
589623
): Promise<CommitOutput> {
590624
const fail = (msg: string): CommitOutput => ({
591625
success: false,
@@ -600,8 +634,7 @@ export class GitService extends TypedEventEmitter<GitServiceEvents> {
600634
const result = await saga.run({
601635
baseDir: directoryPath,
602636
message: message.trim(),
603-
paths,
604-
allowEmpty,
637+
...options,
605638
});
606639

607640
if (!result.success) return fail(result.error);

apps/code/src/main/trpc/routers/git.ts

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
createPrOutput,
1414
detectRepoInput,
1515
detectRepoOutput,
16+
diffInput,
17+
diffOutput,
1618
discardFileChangesInput,
1719
discardFileChangesOutput,
1820
generateCommitMessageInput,
@@ -29,8 +31,6 @@ import {
2931
getCommitConventionsOutput,
3032
getCurrentBranchInput,
3133
getCurrentBranchOutput,
32-
getDiffHeadInput,
33-
getDiffHeadOutput,
3434
getDiffStatsInput,
3535
getDiffStatsOutput,
3636
getFileAtHeadInput,
@@ -45,6 +45,7 @@ import {
4545
getPrTemplateInput,
4646
getPrTemplateOutput,
4747
ghStatusOutput,
48+
gitStateSnapshotSchema,
4849
openPrInput,
4950
openPrOutput,
5051
prStatusInput,
@@ -57,6 +58,7 @@ import {
5758
pushOutput,
5859
searchGithubIssuesInput,
5960
searchGithubIssuesOutput,
61+
stageFilesInput,
6062
syncInput,
6163
syncOutput,
6264
validateRepoInput,
@@ -139,17 +141,45 @@ export const gitRouter = router({
139141
),
140142

141143
getDiffHead: publicProcedure
142-
.input(getDiffHeadInput)
143-
.output(getDiffHeadOutput)
144+
.input(diffInput)
145+
.output(diffOutput)
144146
.query(({ input }) =>
145147
getService().getDiffHead(input.directoryPath, input.ignoreWhitespace),
146148
),
147149

150+
getDiffCached: publicProcedure
151+
.input(diffInput)
152+
.output(diffOutput)
153+
.query(({ input }) =>
154+
getService().getDiffCached(input.directoryPath, input.ignoreWhitespace),
155+
),
156+
157+
getDiffUnstaged: publicProcedure
158+
.input(diffInput)
159+
.output(diffOutput)
160+
.query(({ input }) =>
161+
getService().getDiffUnstaged(input.directoryPath, input.ignoreWhitespace),
162+
),
163+
148164
getDiffStats: publicProcedure
149165
.input(getDiffStatsInput)
150166
.output(getDiffStatsOutput)
151167
.query(({ input }) => getService().getDiffStats(input.directoryPath)),
152168

169+
stageFiles: publicProcedure
170+
.input(stageFilesInput)
171+
.output(gitStateSnapshotSchema)
172+
.mutation(({ input }) =>
173+
getService().stageFiles(input.directoryPath, input.paths),
174+
),
175+
176+
unstageFiles: publicProcedure
177+
.input(stageFilesInput)
178+
.output(gitStateSnapshotSchema)
179+
.mutation(({ input }) =>
180+
getService().unstageFiles(input.directoryPath, input.paths),
181+
),
182+
153183
discardFileChanges: publicProcedure
154184
.input(discardFileChangesInput)
155185
.output(discardFileChangesOutput)
@@ -189,12 +219,11 @@ export const gitRouter = router({
189219
.input(commitInput)
190220
.output(commitOutput)
191221
.mutation(({ input }) =>
192-
getService().commit(
193-
input.directoryPath,
194-
input.message,
195-
input.paths,
196-
input.allowEmpty,
197-
),
222+
getService().commit(input.directoryPath, input.message, {
223+
paths: input.paths,
224+
allowEmpty: input.allowEmpty,
225+
stagedOnly: input.stagedOnly,
226+
}),
198227
),
199228

200229
push: publicProcedure

0 commit comments

Comments
 (0)