From 28fc5eede6fd3be31801be4770ac1a8286c48aac Mon Sep 17 00:00:00 2001 From: Adam Bowker Date: Wed, 1 Apr 2026 14:17:49 -0700 Subject: [PATCH] feat(code): add commit trailers for attribution --- .../src/main/services/git/create-pr-saga.ts | 12 +++++------ apps/code/src/main/services/git/schemas.ts | 2 ++ apps/code/src/main/services/git/service.ts | 11 ++++++++-- apps/code/src/main/trpc/routers/git.ts | 1 + .../hooks/useGitInteraction.ts | 2 ++ packages/git/src/sagas/commit.ts | 21 ++++++++++++++++--- 6 files changed, 38 insertions(+), 11 deletions(-) diff --git a/apps/code/src/main/services/git/create-pr-saga.ts b/apps/code/src/main/services/git/create-pr-saga.ts index 3a2bb74ed..41fe4a590 100644 --- a/apps/code/src/main/services/git/create-pr-saga.ts +++ b/apps/code/src/main/services/git/create-pr-saga.ts @@ -18,6 +18,7 @@ export interface CreatePrSagaInput { prBody?: string; draft?: boolean; stagedOnly?: boolean; + taskId?: string; } export interface CreatePrSagaOutput { @@ -36,7 +37,7 @@ export interface CreatePrDeps { commit( dir: string, message: string, - stagedOnly?: boolean, + options?: { stagedOnly?: boolean; taskId?: string }, ): Promise; getSyncStatus(dir: string): Promise; push(dir: string): Promise; @@ -125,11 +126,10 @@ export class CreatePrSaga extends Saga { await this.step({ name: "committing", execute: async () => { - const result = await this.deps.commit( - directoryPath, - commitMessage!, - input.stagedOnly, - ); + const result = await this.deps.commit(directoryPath, commitMessage!, { + stagedOnly: input.stagedOnly, + taskId: input.taskId, + }); if (!result.success) throw new Error(result.message); return result; }, diff --git a/apps/code/src/main/services/git/schemas.ts b/apps/code/src/main/services/git/schemas.ts index c587582ef..fa783fbfc 100644 --- a/apps/code/src/main/services/git/schemas.ts +++ b/apps/code/src/main/services/git/schemas.ts @@ -206,6 +206,7 @@ export const commitInput = z.object({ paths: z.array(z.string()).optional(), allowEmpty: z.boolean().optional(), stagedOnly: z.boolean().optional(), + taskId: z.string().optional(), }); export type CommitInput = z.infer; @@ -250,6 +251,7 @@ export const createPrInput = z.object({ prBody: z.string().optional(), draft: z.boolean().optional(), stagedOnly: z.boolean().optional(), + taskId: z.string().optional(), }); export type CreatePrInput = z.infer; diff --git a/apps/code/src/main/services/git/service.ts b/apps/code/src/main/services/git/service.ts index 29609f5da..3181d8f9f 100644 --- a/apps/code/src/main/services/git/service.ts +++ b/apps/code/src/main/services/git/service.ts @@ -513,6 +513,7 @@ export class GitService extends TypedEventEmitter { prBody?: string; draft?: boolean; stagedOnly?: boolean; + taskId?: string; }): Promise { const { directoryPath, flowId } = input; @@ -536,7 +537,7 @@ export class GitService extends TypedEventEmitter { checkoutBranch: (dir, name) => this.checkoutBranch(dir, name), getChangedFilesHead: (dir) => this.getChangedFilesHead(dir), generateCommitMessage: (dir) => this.generateCommitMessage(dir), - commit: (dir, msg, stagedOnly) => this.commit(dir, msg, { stagedOnly }), + commit: (dir, msg, opts) => this.commit(dir, msg, opts), getSyncStatus: (dir) => this.getGitSyncStatus(dir), push: (dir) => this.push(dir), publish: (dir) => this.publish(dir), @@ -556,6 +557,7 @@ export class GitService extends TypedEventEmitter { prBody: input.prBody, draft: input.draft, stagedOnly: input.stagedOnly, + taskId: input.taskId, }); if (!result.success) { @@ -619,7 +621,12 @@ export class GitService extends TypedEventEmitter { public async commit( directoryPath: string, message: string, - options?: { paths?: string[]; allowEmpty?: boolean; stagedOnly?: boolean }, + options?: { + paths?: string[]; + allowEmpty?: boolean; + stagedOnly?: boolean; + taskId?: string; + }, ): Promise { const fail = (msg: string): CommitOutput => ({ success: false, diff --git a/apps/code/src/main/trpc/routers/git.ts b/apps/code/src/main/trpc/routers/git.ts index 8fa869f07..9be6da0ad 100644 --- a/apps/code/src/main/trpc/routers/git.ts +++ b/apps/code/src/main/trpc/routers/git.ts @@ -223,6 +223,7 @@ export const gitRouter = router({ paths: input.paths, allowEmpty: input.allowEmpty, stagedOnly: input.stagedOnly, + taskId: input.taskId, }), ), diff --git a/apps/code/src/renderer/features/git-interaction/hooks/useGitInteraction.ts b/apps/code/src/renderer/features/git-interaction/hooks/useGitInteraction.ts index b9227c5a8..b65c6d74e 100644 --- a/apps/code/src/renderer/features/git-interaction/hooks/useGitInteraction.ts +++ b/apps/code/src/renderer/features/git-interaction/hooks/useGitInteraction.ts @@ -230,6 +230,7 @@ export function useGitInteraction( prBody: store.prBody.trim() || undefined, draft: store.createPrDraft || undefined, stagedOnly: stagedOnly || undefined, + taskId, }); if (!result.success) { @@ -349,6 +350,7 @@ export function useGitInteraction( directoryPath: repoPath, message, stagedOnly: stagedOnly || undefined, + taskId, }); if (!result.success) { diff --git a/packages/git/src/sagas/commit.ts b/packages/git/src/sagas/commit.ts index 7dac9c573..5416a9f88 100644 --- a/packages/git/src/sagas/commit.ts +++ b/packages/git/src/sagas/commit.ts @@ -1,10 +1,17 @@ import { GitSaga, type GitSagaInput } from "../git-saga"; +function buildPostHogTrailers(taskId?: string): string[] { + const trailers = ["Generated-By: PostHog Code"]; + if (taskId) trailers.push(`Task-Id: ${taskId}`); + return trailers; +} + export interface CommitInput extends GitSagaInput { message: string; paths?: string[]; allowEmpty?: boolean; stagedOnly?: boolean; + taskId?: string; } export interface CommitOutput { @@ -19,7 +26,7 @@ export class CommitSaga extends GitSaga { protected async executeGitOperations( input: CommitInput, ): Promise { - const { message, paths, allowEmpty, stagedOnly } = input; + const { message, paths, allowEmpty, stagedOnly, taskId } = input; const originalHead = await this.readOnlyStep("get-original-head", () => this.git.revparse(["HEAD"]), @@ -62,11 +69,19 @@ export class CommitSaga extends GitSaga { }); } + const trailers = buildPostHogTrailers(taskId); + + const commitOptions: Record = {}; + if (allowEmpty) commitOptions["--allow-empty"] = null; + if (trailers.length > 0) commitOptions["--trailer"] = trailers; + + const hasOptions = Object.keys(commitOptions).length > 0; + const commitResult = await this.step({ name: "commit", execute: () => - allowEmpty - ? this.git.commit(message, undefined, { "--allow-empty": null }) + hasOptions + ? this.git.commit(message, undefined, commitOptions) : this.git.commit(message), rollback: async () => { await this.git.reset(["--soft", originalHead]);