Skip to content

Commit c726700

Browse files
authored
Merge pull request #166 from patroza/fix/layer-gates-green
fix(stack): rejoin fork VCS/UI APIs after main pingdotgg#4727 restore
2 parents 0338b5d + b5b7746 commit c726700

10 files changed

Lines changed: 296 additions & 125 deletions

File tree

apps/server/src/vcs/GitVcsDriverCore.ts

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,54 @@ const STATUS_UPSTREAM_REFRESH_ENV = Object.freeze({
7070
} satisfies NodeJS.ProcessEnv);
7171
const DEFAULT_BASE_BRANCH_CANDIDATES = ["main", "master"] as const;
7272
const GIT_LIST_BRANCHES_DEFAULT_LIMIT = 100;
73+
74+
const COMMIT_SIGNING_FAILURE_PATTERNS = [
75+
/gpg(?:2)?(?:\.exe)?: .*failed to sign/i,
76+
/gpg failed to sign the data/i,
77+
/signing failed:/i,
78+
/failed to sign the data/i,
79+
/pinentry.*(?:failed|error|not found|no such file|cancell?ed)/i,
80+
/(?:failed|error|no such file|cancell?ed).*pinentry/i,
81+
/inappropriate ioctl for device/i,
82+
/cannot open \/dev\/tty/i,
83+
/no secret key/i,
84+
/secret key not available/i,
85+
/ssh-keygen(?:\.exe)?:?.*(?:failed|error|couldn[']t).*sign/i,
86+
/couldn[']t sign (?:message|data)/i,
87+
/couldn[']t load public key/i,
88+
/no private key found for public key/i,
89+
/load key .*: (?:invalid format|no such file or directory|permission denied)/i,
90+
/agent refused operation/i,
91+
] as const;
92+
93+
export function isCommitSigningFailureStderr(stderr: string): boolean {
94+
return COMMIT_SIGNING_FAILURE_PATTERNS.some((pattern) => pattern.test(stderr));
95+
}
96+
97+
/** Longer than any real git error line, short enough to keep logs readable. */
98+
const GIT_STDERR_LOG_LIMIT = 2000;
99+
100+
/**
101+
* Strip credentials from git output so it can be logged.
102+
*
103+
* git echoes the remote URL it used, and those URLs routinely carry secrets
104+
* (`https://x-access-token:TOKEN@github.com/...`), so raw stderr must never
105+
* reach a log. Redacts the userinfo component of any URL plus bare tokens that
106+
* commonly appear on their own.
107+
*/
108+
export function redactGitOutput(stderr: string): string {
109+
return (
110+
stderr
111+
.slice(0, GIT_STDERR_LOG_LIMIT)
112+
.replace(/([a-zA-Z][\w+.-]*:\/\/)[^/@\s]*@/g, "$1<redacted>@")
113+
.replace(/\b(gh[pousr]_|github_pat_|glpat-)[A-Za-z0-9_-]+/g, "$1<redacted>")
114+
// Take the whole value, not just the scheme word: `Authorization: Bearer X`
115+
// must not redact `Bearer` and leave `X` behind.
116+
.replace(/\b(Authorization)\s*[:=]\s*.*/gi, "$1: <redacted>")
117+
.replace(/\b(Bearer|token)\s*[:=]?\s+\S+/gi, "$1 <redacted>")
118+
);
119+
}
120+
73121
const NON_REPOSITORY_STATUS_DETAILS = Object.freeze<GitVcsDriver.GitStatusDetails>({
74122
isRepo: false,
75123
hasOriginRemote: false,
@@ -379,6 +427,7 @@ function gitCommandContext(
379427
command: "git",
380428
cwd: input.cwd,
381429
argumentCount: input.args.length,
430+
failureKind: "unknown" as const,
382431
} as const;
383432
}
384433

@@ -1737,25 +1786,52 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
17371786
body,
17381787
options?: GitVcsDriver.GitCommitOptions,
17391788
) {
1740-
const args = ["commit", "-m", subject];
1789+
const args = ["commit"];
1790+
if (options?.disableSigning) {
1791+
args.push("--no-gpg-sign");
1792+
}
1793+
args.push("-m", subject);
17411794
const trimmedBody = body.trim();
17421795
if (trimmedBody.length > 0) {
17431796
args.push("-m", trimmedBody);
17441797
}
1745-
const progress =
1746-
options?.progress?.onOutputLine === undefined
1747-
? options?.progress
1748-
: {
1749-
...options.progress,
1798+
let hookFailed = false;
1799+
const progress: GitVcsDriver.ExecuteGitProgress = {
1800+
...(options?.progress?.onOutputLine
1801+
? {
17501802
onStdoutLine: (line: string) =>
17511803
options.progress?.onOutputLine?.({ stream: "stdout", text: line }) ?? Effect.void,
17521804
onStderrLine: (line: string) =>
17531805
options.progress?.onOutputLine?.({ stream: "stderr", text: line }) ?? Effect.void,
1754-
};
1755-
yield* executeGit("GitVcsDriver.commit.commit", cwd, args, {
1806+
}
1807+
: {}),
1808+
...(options?.progress?.onHookStarted
1809+
? { onHookStarted: options.progress.onHookStarted }
1810+
: {}),
1811+
onHookFinished: (input) => {
1812+
if (input.exitCode !== null && input.exitCode !== 0) {
1813+
hookFailed = true;
1814+
}
1815+
return options?.progress?.onHookFinished?.(input) ?? Effect.void;
1816+
},
1817+
};
1818+
const result = yield* executeGitWithStableDiagnostics("GitVcsDriver.commit.commit", cwd, args, {
1819+
allowNonZeroExit: true,
17561820
...(options?.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
1757-
...(progress ? { progress } : {}),
1758-
}).pipe(Effect.asVoid);
1821+
progress,
1822+
});
1823+
if (result.exitCode !== 0) {
1824+
return yield* new GitCommandError({
1825+
...gitCommandContext({ operation: "GitVcsDriver.commit.commit", cwd, args }),
1826+
detail: "Git command exited with a non-zero status.",
1827+
...(result.exitCode === null ? {} : { exitCode: result.exitCode }),
1828+
stdoutLength: result.stdout.length,
1829+
stderrLength: result.stderr.length,
1830+
...(!options?.disableSigning && !hookFailed && isCommitSigningFailureStderr(result.stderr)
1831+
? { failureKind: "commit_signing_failed" as const }
1832+
: {}),
1833+
});
1834+
}
17591835
const commitSha = yield* runGitStdout("GitVcsDriver.commit.revParseHead", cwd, [
17601836
"rev-parse",
17611837
"HEAD",
@@ -2107,6 +2183,7 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
21072183
operation: "GitVcsDriver.getReviewDiffPreview.hash",
21082184
command: "crypto.digest SHA-256",
21092185
cwd: input.cwd,
2186+
failureKind: "unknown",
21102187
detail: "Failed to hash review diff.",
21112188
cause,
21122189
}),

apps/server/src/ws.ts

Lines changed: 0 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ import {
4848
ProjectWriteFileError,
4949
RelayClientInstallFailedError,
5050
type RelayClientInstallProgressEvent,
51-
OrchestrationReplayEventsError,
5251
type FilesystemBrowseFailure,
5352
FilesystemBrowseError,
5453
AssetWorkspaceContextNotFoundError,
@@ -62,7 +61,6 @@ import {
6261
WS_METHODS,
6362
WsRpcGroup,
6463
} from "@t3tools/contracts";
65-
import { clamp } from "effect/Number";
6664
import { HttpRouter, HttpServerRequest, HttpServerRespondable } from "effect/unstable/http";
6765
import { RpcSerialization, RpcServer } from "effect/unstable/rpc";
6866

@@ -105,7 +103,6 @@ import * as VcsProvisioningService from "./vcs/VcsProvisioningService.ts";
105103
import * as GitWorkflowService from "./git/GitWorkflowService.ts";
106104
import * as ReviewService from "./review/ReviewService.ts";
107105
import * as ProjectSetupScriptRunner from "./project/ProjectSetupScriptRunner.ts";
108-
import * as RepositoryIdentityResolver from "./project/RepositoryIdentityResolver.ts";
109106
import * as ServerEnvironment from "./environment/ServerEnvironment.ts";
110107
import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";
111108
import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts";
@@ -356,7 +353,6 @@ const RPC_REQUIRED_SCOPE = new Map<string, AuthEnvironmentScope>([
356353
[ORCHESTRATION_WS_METHODS.getTurnDiff, AuthOrchestrationReadScope],
357354
[ORCHESTRATION_WS_METHODS.getThreadActivities, AuthOrchestrationReadScope],
358355
[ORCHESTRATION_WS_METHODS.getFullThreadDiff, AuthOrchestrationReadScope],
359-
[ORCHESTRATION_WS_METHODS.replayEvents, AuthOrchestrationReadScope],
360356
[ORCHESTRATION_WS_METHODS.subscribeShell, AuthOrchestrationReadScope],
361357
[ORCHESTRATION_WS_METHODS.getArchivedShellSnapshot, AuthOrchestrationReadScope],
362358
[ORCHESTRATION_WS_METHODS.subscribeThread, AuthOrchestrationReadScope],
@@ -507,8 +503,6 @@ const makeWsRpcLayer = (
507503
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
508504
const workspaceFileSystem = yield* WorkspaceFileSystem.WorkspaceFileSystem;
509505
const projectSetupScriptRunner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner;
510-
const repositoryIdentityResolver =
511-
yield* RepositoryIdentityResolver.RepositoryIdentityResolver;
512506
const serverEnvironment = yield* ServerEnvironment.ServerEnvironment;
513507
const serverAuth = yield* EnvironmentAuth.EnvironmentAuth;
514508
const sourceControlDiscovery = yield* SourceControlDiscovery.SourceControlDiscovery;
@@ -672,53 +666,6 @@ const makeWsRpcLayer = (
672666
});
673667
};
674668

675-
const enrichProjectEvent = (
676-
event: OrchestrationEvent,
677-
): Effect.Effect<OrchestrationEvent, never, never> => {
678-
switch (event.type) {
679-
case "project.created":
680-
return repositoryIdentityResolver.resolve(event.payload.workspaceRoot).pipe(
681-
Effect.map((repositoryIdentity) => ({
682-
...event,
683-
payload: {
684-
...event.payload,
685-
repositoryIdentity,
686-
},
687-
})),
688-
);
689-
case "project.meta-updated":
690-
return Effect.gen(function* () {
691-
const workspaceRoot =
692-
event.payload.workspaceRoot ??
693-
Option.match(
694-
yield* projectionSnapshotQuery.getProjectShellById(event.payload.projectId),
695-
{
696-
onNone: () => null,
697-
onSome: (project) => project.workspaceRoot,
698-
},
699-
) ??
700-
null;
701-
if (workspaceRoot === null) {
702-
return event;
703-
}
704-
705-
const repositoryIdentity = yield* repositoryIdentityResolver.resolve(workspaceRoot);
706-
return {
707-
...event,
708-
payload: {
709-
...event.payload,
710-
repositoryIdentity,
711-
},
712-
} satisfies OrchestrationEvent;
713-
}).pipe(Effect.orElseSucceed(() => event));
714-
default:
715-
return Effect.succeed(event);
716-
}
717-
};
718-
719-
const enrichOrchestrationEvents = (events: ReadonlyArray<OrchestrationEvent>) =>
720-
Effect.forEach(events, enrichProjectEvent, { concurrency: 4 });
721-
722669
const toShellStreamEvent = (
723670
event: OrchestrationEvent,
724671
): Effect.Effect<Option.Option<OrchestrationShellStreamEvent>, never, never> => {
@@ -1418,30 +1365,6 @@ const makeWsRpcLayer = (
14181365
),
14191366
{ "rpc.aggregate": "orchestration" },
14201367
),
1421-
[ORCHESTRATION_WS_METHODS.replayEvents]: (input) =>
1422-
observeRpcEffect(
1423-
ORCHESTRATION_WS_METHODS.replayEvents,
1424-
Stream.runCollect(
1425-
orchestrationEngine.readEvents(
1426-
clamp(input.fromSequenceExclusive, {
1427-
maximum: Number.MAX_SAFE_INTEGER,
1428-
minimum: 0,
1429-
}),
1430-
),
1431-
).pipe(
1432-
Effect.map((events) => Array.from(events)),
1433-
Effect.flatMap(enrichOrchestrationEvents),
1434-
Effect.map((events) => events.map(projectActivityEvent)),
1435-
Effect.mapError(
1436-
(cause) =>
1437-
new OrchestrationReplayEventsError({
1438-
message: "Failed to replay orchestration events",
1439-
cause,
1440-
}),
1441-
),
1442-
),
1443-
{ "rpc.aggregate": "orchestration" },
1444-
),
14451368
[ORCHESTRATION_WS_METHODS.subscribeShell]: (input) =>
14461369
observeRpcStreamEffect(
14471370
ORCHESTRATION_WS_METHODS.subscribeShell,

apps/web/src/components/BranchToolbar.logic.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,19 @@ describe("resolveBranchTriggerLabel", () => {
212212
).toBe("From main");
213213
});
214214

215+
it("shows the bare branch name when reusing the selected worktree base branch", () => {
216+
expect(
217+
resolveBranchTriggerLabel({
218+
activeWorktreePath: null,
219+
effectiveEnvMode: "worktree",
220+
resolvedActiveBranch: "main",
221+
resolvedActiveBranchIsRemote: false,
222+
startFromOrigin: true,
223+
reuseBaseBranch: true,
224+
}),
225+
).toBe("main");
226+
});
227+
215228
it("does not duplicate the origin prefix for an explicit remote ref", () => {
216229
expect(
217230
resolveBranchTriggerLabel({

apps/web/src/components/BranchToolbar.logic.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,18 +173,25 @@ export function resolveBranchTriggerLabel(input: {
173173
resolvedActiveBranch: string | null;
174174
resolvedActiveBranchIsRemote: boolean | null;
175175
startFromOrigin: boolean;
176+
reuseBaseBranch?: boolean;
176177
}): string {
177178
const {
178179
activeWorktreePath,
179180
effectiveEnvMode,
180181
resolvedActiveBranch,
181182
resolvedActiveBranchIsRemote,
182183
startFromOrigin,
184+
reuseBaseBranch = false,
183185
} = input;
184186
if (!resolvedActiveBranch) {
185187
return "Select ref";
186188
}
189+
// Reused base branch is checked out as-is (Tim #15); otherwise "From X" for
190+
// new worktree branches, with optional origin/ prefix (upstream #4680).
187191
if (effectiveEnvMode === "worktree" && !activeWorktreePath) {
192+
if (reuseBaseBranch) {
193+
return resolvedActiveBranch;
194+
}
188195
const baseRef =
189196
startFromOrigin && resolvedActiveBranchIsRemote === false
190197
? `origin/${resolvedActiveBranch}`

0 commit comments

Comments
 (0)