specrails-desktop 2.29.2 → 2.29.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/dist/assets/{ActivityFeedPage-ClpD5CFj.js → ActivityFeedPage-CFB4UBhT.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-CSPs9Ofc.js → AgentBrowserCapture-C-T0r16B.js} +1 -1
- package/client/dist/assets/{AgentModeCodePane-DwcyklA5.js → AgentModeCodePane-IljV5FOQ.js} +2 -2
- package/client/dist/assets/{AgentModeJobsPane-C69bqlMa.js → AgentModeJobsPane-B5miH10_.js} +1 -1
- package/client/dist/assets/{AgentsPage-pRiRr9-W.js → AgentsPage-BgYQrAcH.js} +1 -1
- package/client/dist/assets/{AnalyticsPage-C14exKsC.js → AnalyticsPage-BtVCuNjL.js} +1 -1
- package/client/dist/assets/{CodePage-CK76JKm9.js → CodePage-Cb_jAjSV.js} +1 -1
- package/client/dist/assets/{DesktopAnalyticsPage-Cnv0f3XQ.js → DesktopAnalyticsPage-s_Cc_JL1.js} +1 -1
- package/client/dist/assets/{DocsDialog-CHUm_Ym0.js → DocsDialog-Cr47QI9L.js} +1 -1
- package/client/dist/assets/{DocsPage-COI-oDok.js → DocsPage-C_WPRuG_.js} +1 -1
- package/client/dist/assets/{InteractiveJobComposer-CypM7R1F.js → InteractiveJobComposer-NfFOciRB.js} +1 -1
- package/client/dist/assets/{JobDetailModal-B_W0H9dw.js → JobDetailModal-DqEW7Z7K.js} +1 -1
- package/client/dist/assets/{JobDetailPage-CeeXOoKS.js → JobDetailPage-D4ppEpDE.js} +1 -1
- package/client/dist/assets/{JobsPage-CwhJG9jw.js → JobsPage-DStSoMyq.js} +1 -1
- package/client/dist/assets/{LoopBuilderPage-C51t1WOM.js → LoopBuilderPage-DltCwS4P.js} +1 -1
- package/client/dist/assets/{LoopPreviewModal-Y2HgtIJu.js → LoopPreviewModal-BWaiAbUq.js} +1 -1
- package/client/dist/assets/{LoopsPage-Bvn1Kw5V.js → LoopsPage-BqjbXuBZ.js} +1 -1
- package/client/dist/assets/{MinimizedChatsContext-DiFvZDPW.js → MinimizedChatsContext--o3JAM9F.js} +1 -1
- package/client/dist/assets/{PluginsPage-BGPRJpdf.js → PluginsPage-B-UOmzXw.js} +1 -1
- package/client/dist/assets/{ProjectSettingsDialog-D_GTyGLc.js → ProjectSettingsDialog-DkGajIxE.js} +1 -1
- package/client/dist/assets/{TemplatePreviewModal-szwBhd7h.js → TemplatePreviewModal-5CUXlkw7.js} +1 -1
- package/client/dist/assets/{TicketDetailModal-9UEyK974.js → TicketDetailModal-B_igqmti.js} +4 -4
- package/client/dist/assets/{Trans-B_vKZ-Hb.js → Trans-DDMIBBUd.js} +1 -1
- package/client/dist/assets/agent-BXc59ARK2.js +1 -0
- package/client/dist/assets/agent-BbIFXdDq.js +1 -0
- package/client/dist/assets/agent-BqEfA5u5.js +1 -0
- package/client/dist/assets/agent-BtGzgTC4.js +1 -0
- package/client/dist/assets/agent-CIhys_5d.js +1 -0
- package/client/dist/assets/agent-CJPaqsVR.js +1 -0
- package/client/dist/assets/agent-n-hY4GuL.js +1 -0
- package/client/dist/assets/agent-tJ-efa3_2.js +1 -0
- package/client/dist/assets/common-6MLbD-jM.js +1 -0
- package/client/dist/assets/common-BKGEu4rm.js +1 -0
- package/client/dist/assets/common-BNbnv8cE2.js +1 -0
- package/client/dist/assets/common-BqGWtDbt.js +1 -0
- package/client/dist/assets/common-DBtTzeYm2.js +1 -0
- package/client/dist/assets/common-DEW3Nr8n.js +1 -0
- package/client/dist/assets/{common-DVQuhs-m.js → common-DHsv9N5d.js} +1 -1
- package/client/dist/assets/common-mkrEcW-Y.js +1 -0
- package/client/dist/assets/dashboard-BYzxom0L.js +1 -0
- package/client/dist/assets/dashboard-BmDZmHyY.js +1 -0
- package/client/dist/assets/dashboard-CnNtJBQ5.js +1 -0
- package/client/dist/assets/dashboard-D4Uj-XWc2.js +1 -0
- package/client/dist/assets/dashboard-D57uRaGw2.js +1 -0
- package/client/dist/assets/dashboard-DAr6F9et.js +1 -0
- package/client/dist/assets/dashboard-vAcsn0L3.js +1 -0
- package/client/dist/assets/dashboard-vrjZvxWy.js +1 -0
- package/client/dist/assets/{dist-js-CcBCbb32.js → dist-js-BCUhGUXS.js} +1 -1
- package/client/dist/assets/{dist-js-VwztRkGj.js → dist-js-DcoXTnEI.js} +1 -1
- package/client/dist/assets/{formatDistanceToNow-ByHIauTQ.js → formatDistanceToNow-DP3Ab6fX.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-BxHAksaU.js → getTimezoneOffsetInMilliseconds-DdKKL8Fx.js} +1 -1
- package/client/dist/assets/{i18n-CgaRxpII.js → i18n-DREQCE2F.js} +2 -2
- package/client/dist/assets/index-B784m8RT.js +76 -0
- package/client/dist/assets/index-CQBIemiz.css +2 -0
- package/client/dist/assets/{spending-C4p06nen.js → spending-CtY7yYes.js} +1 -1
- package/client/dist/assets/{useBrowserCaptureSession-2OqX0vap.js → useBrowserCaptureSession-BFBdW59q.js} +1 -1
- package/client/dist/assets/{useDesktop-X8YB9xzv.js → useDesktop-uWVZ8eia.js} +1 -1
- package/client/dist/index.html +14 -14
- package/docs/internals/safe-pr-review-flow.md +221 -38
- package/package.json +1 -1
- package/server/dist/active-pr-continuation.js +237 -52
- package/server/dist/agent-chat-manager.js +29 -12
- package/server/dist/agent-store.js +19 -4
- package/server/dist/db.js +21 -0
- package/server/dist/pr-lifecycle.js +100 -8
- package/server/dist/project-git.js +62 -2
- package/server/dist/rail-isolated-launch.js +644 -108
- package/server/dist/rail-pr-decision.js +1065 -181
- package/server/dist/rail-pr-recovery-git.js +235 -0
- package/server/dist/rail-pr-store.js +120 -9
- package/server/dist/rail-worktree-release.js +290 -4
- package/server/dist/rails-router.js +97 -9
- package/server/dist/worktree-manager.js +120 -16
- package/server/dist/worktree-overlay.js +166 -4
- package/client/dist/assets/agent-4rmXoo_p.js +0 -1
- package/client/dist/assets/agent-BxZuNGih.js +0 -1
- package/client/dist/assets/agent-C3uXOYv_.js +0 -1
- package/client/dist/assets/agent-CA8Y4wbw.js +0 -1
- package/client/dist/assets/agent-CASKrhG12.js +0 -1
- package/client/dist/assets/agent-Cv-jud5n2.js +0 -1
- package/client/dist/assets/agent-DitUxYh2.js +0 -1
- package/client/dist/assets/agent-Doow4X7o.js +0 -1
- package/client/dist/assets/common-CD1QHriA2.js +0 -1
- package/client/dist/assets/common-CwwPaRxx.js +0 -1
- package/client/dist/assets/common-Dg2jaX7d.js +0 -1
- package/client/dist/assets/common-DuBwj6tK.js +0 -1
- package/client/dist/assets/common-MRj9MHcc.js +0 -1
- package/client/dist/assets/common-O4ZpnMLp.js +0 -1
- package/client/dist/assets/common-fyeF0v7C2.js +0 -1
- package/client/dist/assets/dashboard-B_NkSHYm.js +0 -1
- package/client/dist/assets/dashboard-ClBb_3pq.js +0 -1
- package/client/dist/assets/dashboard-CpCmJvMB.js +0 -1
- package/client/dist/assets/dashboard-Czohu35O2.js +0 -1
- package/client/dist/assets/dashboard-DcjbD0UK.js +0 -1
- package/client/dist/assets/dashboard-Dvx5F3we.js +0 -1
- package/client/dist/assets/dashboard-R1bOPjBh2.js +0 -1
- package/client/dist/assets/dashboard-owcDucDi.js +0 -1
- package/client/dist/assets/index-BflYQpWm.css +0 -2
- package/client/dist/assets/index-C1_-BTXy.js +0 -72
|
@@ -1,16 +1,97 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PR_LIFECYCLE_JSON_FIELDS = void 0;
|
|
4
|
+
exports.verifyPushRemoteForPr = verifyPushRemoteForPr;
|
|
4
5
|
exports.observePrLifecycle = observePrLifecycle;
|
|
6
|
+
exports.matchesRecordedPrIdentity = matchesRecordedPrIdentity;
|
|
5
7
|
exports.isExactOpenPr = isExactOpenPr;
|
|
6
8
|
const SHA_RE = /^[0-9a-f]{40,64}$/i;
|
|
7
|
-
exports.PR_LIFECYCLE_JSON_FIELDS = 'state,isDraft,headRefName,baseRefName,headRefOid,mergeCommit,commits';
|
|
9
|
+
exports.PR_LIFECYCLE_JSON_FIELDS = 'state,isDraft,headRefName,baseRefName,isCrossRepository,headRefOid,mergeCommit,commits';
|
|
8
10
|
function firstLine(result) {
|
|
9
11
|
return (result.stderr.trim() || result.stdout.trim()).split('\n')[0].trim().slice(0, 512);
|
|
10
12
|
}
|
|
11
13
|
function oid(value) {
|
|
12
14
|
return typeof value === 'string' && SHA_RE.test(value) ? value.toLowerCase() : null;
|
|
13
15
|
}
|
|
16
|
+
function githubRepositoryIdentity(rawUrl, kind) {
|
|
17
|
+
const raw = rawUrl.trim();
|
|
18
|
+
if (!raw || raw.includes('\n') || raw.includes('\r'))
|
|
19
|
+
return null;
|
|
20
|
+
let host = '';
|
|
21
|
+
let pathname = '';
|
|
22
|
+
try {
|
|
23
|
+
const parsed = new URL(raw);
|
|
24
|
+
if (!['http:', 'https:', 'ssh:', 'git:'].includes(parsed.protocol))
|
|
25
|
+
return null;
|
|
26
|
+
// The verified target is later passed as an argv element. Never carry
|
|
27
|
+
// embedded credentials or token-like URL suffixes into a child process or
|
|
28
|
+
// its timeout/error diagnostics; credential helpers remain the only
|
|
29
|
+
// supported authentication channel. SSH's conventional `git` username is
|
|
30
|
+
// identity, not a secret, and is safe to retain.
|
|
31
|
+
if (parsed.password || parsed.search || parsed.hash)
|
|
32
|
+
return null;
|
|
33
|
+
if (parsed.username && !(parsed.protocol === 'ssh:' && parsed.username === 'git'))
|
|
34
|
+
return null;
|
|
35
|
+
host = parsed.hostname.toLowerCase();
|
|
36
|
+
pathname = parsed.pathname;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// SCP-style Git URL: git@github.com:owner/repository.git
|
|
40
|
+
const scp = /^(?:git@)?([^@:/\s]+):(.+)$/.exec(raw);
|
|
41
|
+
if (!scp)
|
|
42
|
+
return null;
|
|
43
|
+
host = scp[1].toLowerCase();
|
|
44
|
+
pathname = scp[2];
|
|
45
|
+
if (pathname.includes('?') || pathname.includes('#'))
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const segments = pathname.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
|
|
49
|
+
if (kind === 'pr') {
|
|
50
|
+
if (segments.length !== 4 || segments[2] !== 'pull' || !/^\d+$/.test(segments[3]))
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
else if (segments.length !== 2) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const owner = segments[0]?.toLowerCase();
|
|
57
|
+
const repository = segments[1]?.replace(/\.git$/i, '').toLowerCase();
|
|
58
|
+
return host && owner && repository ? `${host}/${owner}/${repository}` : null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Prove that the local push remote names the repository that owns a same-repo
|
|
62
|
+
* PR. Branch names alone are insufficient: another clone may call its fork
|
|
63
|
+
* `origin`, which would otherwise receive the exact recovery refspec.
|
|
64
|
+
*/
|
|
65
|
+
async function verifyPushRemoteForPr(exec, repoDir, prUrl, remote = 'origin') {
|
|
66
|
+
const expected = githubRepositoryIdentity(prUrl, 'pr');
|
|
67
|
+
if (!expected)
|
|
68
|
+
return { ok: false, detail: 'the recorded PR URL does not identify an exact GitHub repository' };
|
|
69
|
+
let result;
|
|
70
|
+
try {
|
|
71
|
+
result = await exec.run('git', ['remote', 'get-url', '--push', '--all', remote], repoDir);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
return { ok: false, detail: `the ${remote} push remote could not be inspected: ${err instanceof Error ? err.message : String(err)}` };
|
|
75
|
+
}
|
|
76
|
+
if (result.code !== 0) {
|
|
77
|
+
return { ok: false, detail: `the ${remote} push remote could not be inspected: ${firstLine(result) || `exit ${result.code}`}` };
|
|
78
|
+
}
|
|
79
|
+
const pushTargets = result.stdout.split(/\r?\n/).map((value) => value.trim()).filter(Boolean);
|
|
80
|
+
if (pushTargets.length !== 1) {
|
|
81
|
+
return {
|
|
82
|
+
ok: false,
|
|
83
|
+
detail: `the ${remote} push remote must resolve to exactly one URL; found ${pushTargets.length}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const pushTarget = pushTargets[0];
|
|
87
|
+
const actual = githubRepositoryIdentity(pushTarget, 'remote');
|
|
88
|
+
if (!actual)
|
|
89
|
+
return { ok: false, detail: `the ${remote} push remote is not an unambiguous GitHub repository URL` };
|
|
90
|
+
if (actual !== expected) {
|
|
91
|
+
return { ok: false, detail: `the ${remote} push remote does not own the recorded PR repository` };
|
|
92
|
+
}
|
|
93
|
+
return { ok: true, identity: actual, pushTarget };
|
|
94
|
+
}
|
|
14
95
|
/**
|
|
15
96
|
* Observe one exact PR and, when requested, whether its immutable commit/merge
|
|
16
97
|
* evidence contains a delivery SHA. The helper never guesses from a ledger row:
|
|
@@ -42,11 +123,15 @@ async function observePrLifecycle(exec, repoDir, prUrl, expectedSha) {
|
|
|
42
123
|
// A merged PR's live head branch can advance after the merge. Its current
|
|
43
124
|
// headRefOid is therefore NOT proof that the merge included that object.
|
|
44
125
|
// Only the PR's commit snapshot / merge commit are valid MERGED evidence.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
126
|
+
// While OPEN, delivery verification is intentionally stronger than mere
|
|
127
|
+
// ancestry: the remote PR head itself must still be the frozen delivery
|
|
128
|
+
// object. Otherwise a concurrent writer could advance/substitute the head
|
|
129
|
+
// and a stale card would incorrectly report our exact push as verified.
|
|
130
|
+
// CLOSED/MERGED no longer have a mutable review head, so their immutable PR
|
|
131
|
+
// commit snapshot (or merge object) is the relevant inclusion evidence.
|
|
132
|
+
const observedOids = new Set((normalizedState === 'OPEN'
|
|
133
|
+
? [headRefOid]
|
|
134
|
+
: [mergeCommitOid, ...commitOids]).filter((value) => value !== null));
|
|
50
135
|
const includesExpectedSha = expected === null
|
|
51
136
|
? null
|
|
52
137
|
: observedOids.has(expected)
|
|
@@ -60,6 +145,7 @@ async function observePrLifecycle(exec, repoDir, prUrl, expectedSha) {
|
|
|
60
145
|
isDraft: parsed.isDraft === true,
|
|
61
146
|
headRefName: typeof parsed.headRefName === 'string' ? parsed.headRefName : null,
|
|
62
147
|
baseRefName: typeof parsed.baseRefName === 'string' ? parsed.baseRefName : null,
|
|
148
|
+
isCrossRepository: typeof parsed.isCrossRepository === 'boolean' ? parsed.isCrossRepository : null,
|
|
63
149
|
headRefOid,
|
|
64
150
|
mergeCommitOid,
|
|
65
151
|
commitOids,
|
|
@@ -70,8 +156,14 @@ async function observePrLifecycle(exec, repoDir, prUrl, expectedSha) {
|
|
|
70
156
|
return { ok: false, detail: 'unparseable gh pr view output' };
|
|
71
157
|
}
|
|
72
158
|
}
|
|
159
|
+
/** Head/base identity is required in every lifecycle state. A PR retargeted to
|
|
160
|
+
* another base must never satisfy delivery merely because it still lists the
|
|
161
|
+
* expected commit. */
|
|
162
|
+
function matchesRecordedPrIdentity(observation, branch, baseBranch) {
|
|
163
|
+
return observation.ok && observation.isCrossRepository === false &&
|
|
164
|
+
observation.headRefName === branch && observation.baseRefName === baseBranch;
|
|
165
|
+
}
|
|
73
166
|
/** OPEN is continuation-safe only for the same recorded head/base identity. */
|
|
74
167
|
function isExactOpenPr(observation, branch, baseBranch) {
|
|
75
|
-
return observation.ok && observation.state === 'OPEN'
|
|
76
|
-
observation.headRefName === branch && observation.baseRefName === baseBranch;
|
|
168
|
+
return observation.ok && matchesRecordedPrIdentity(observation, branch, baseBranch) && observation.state === 'OPEN';
|
|
77
169
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.parseWorktreePorcelain = parseWorktreePorcelain;
|
|
4
|
+
exports.inspectProjectCheckoutCleanliness = inspectProjectCheckoutCleanliness;
|
|
4
5
|
exports.getProjectGitInfo = getProjectGitInfo;
|
|
5
6
|
exports.compactCheckoutError = compactCheckoutError;
|
|
6
7
|
exports.checkoutProjectBranch = checkoutProjectBranch;
|
|
@@ -17,6 +18,7 @@ const integration_branch_1 = require("./integration-branch");
|
|
|
17
18
|
// against `git branch` output, so no user string can smuggle flags or refs.
|
|
18
19
|
const GIT_TIMEOUT_MS = 10_000;
|
|
19
20
|
const GIT_MAX_BUFFER = 4 * 1024 * 1024;
|
|
21
|
+
const COMMIT_SHA_RE = /^[0-9a-f]{40,64}$/i;
|
|
20
22
|
function git(repoDir, args) {
|
|
21
23
|
return new Promise((resolve, reject) => {
|
|
22
24
|
(0, child_process_1.execFile)('git', args, { cwd: repoDir, env: file_provenance_1.GIT_EXEC_ENV, timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_BUFFER, windowsHide: true }, (err, stdout, stderr) => {
|
|
@@ -57,6 +59,21 @@ async function listLocalBranches(repoDir) {
|
|
|
57
59
|
const out = await git(repoDir, ['branch', '--format=%(refname:short)']);
|
|
58
60
|
return out.split('\n').map((b) => b.trim()).filter(Boolean);
|
|
59
61
|
}
|
|
62
|
+
/** Lossless checkout preflight. Unlike the dashboard-oriented aggregate info,
|
|
63
|
+
* this result is tri-state: an unreadable status is never interpreted as a
|
|
64
|
+
* clean checkout. */
|
|
65
|
+
async function inspectProjectCheckoutCleanliness(repoDir) {
|
|
66
|
+
try {
|
|
67
|
+
const status = await git(repoDir, ['status', '--porcelain', '--untracked-files=all']);
|
|
68
|
+
return { ok: true, clean: status === '' };
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
detail: `Working tree cleanliness could not be verified: ${err instanceof Error ? err.message : String(err)}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
60
77
|
async function getProjectGitInfo(repoDir) {
|
|
61
78
|
try {
|
|
62
79
|
await git(repoDir, ['rev-parse', '--is-inside-work-tree']);
|
|
@@ -159,11 +176,18 @@ async function remoteBranchExists(repoDir, branch) {
|
|
|
159
176
|
/** Checkout a PR/review branch into the user's main repo. Unlike the small
|
|
160
177
|
* AgentGitBar switcher, this path may materialize a local tracking branch from
|
|
161
178
|
* `origin/<branch>` because a PR delivery can be remote-only after worktree
|
|
162
|
-
* cleanup or a fresh clone.
|
|
163
|
-
|
|
179
|
+
* cleanup or a fresh clone. When supplied, `expectedSha` is an immutable
|
|
180
|
+
* delivery lease: a divergent local/remote branch is preserved and checkout
|
|
181
|
+
* succeeds only when the final branch HEAD equals that exact object. It never
|
|
182
|
+
* overwrites dirty local work. */
|
|
183
|
+
async function checkoutProjectReviewBranch(repoDir, branch, expectedSha) {
|
|
164
184
|
const target = branch.trim();
|
|
165
185
|
if (!(0, integration_branch_1.isValidBranchName)(target))
|
|
166
186
|
return { ok: false, error: `Invalid branch name: ${branch}` };
|
|
187
|
+
const expected = expectedSha?.trim().toLowerCase() ?? null;
|
|
188
|
+
if (expected !== null && !COMMIT_SHA_RE.test(expected)) {
|
|
189
|
+
return { ok: false, error: 'The verified delivery commit is invalid; checkout was not attempted.' };
|
|
190
|
+
}
|
|
167
191
|
let dirty = false;
|
|
168
192
|
try {
|
|
169
193
|
dirty = (await git(repoDir, ['status', '--porcelain'])) !== '';
|
|
@@ -185,9 +209,27 @@ async function checkoutProjectReviewBranch(repoDir, branch) {
|
|
|
185
209
|
}
|
|
186
210
|
try {
|
|
187
211
|
if (branches.includes(target)) {
|
|
212
|
+
if (expected) {
|
|
213
|
+
const localSha = (await git(repoDir, ['rev-parse', '--verify', `refs/heads/${target}`])).toLowerCase();
|
|
214
|
+
if (localSha !== expected) {
|
|
215
|
+
return {
|
|
216
|
+
ok: false,
|
|
217
|
+
error: `Local branch '${target}' points to ${localSha.slice(0, 8)}, not verified delivery ${expected.slice(0, 8)}. It was preserved and not checked out.`,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
188
221
|
await git(repoDir, ['checkout', target]);
|
|
189
222
|
}
|
|
190
223
|
else if (await remoteBranchExists(repoDir, target)) {
|
|
224
|
+
if (expected) {
|
|
225
|
+
const remoteSha = (await git(repoDir, ['rev-parse', '--verify', `refs/remotes/origin/${target}`])).toLowerCase();
|
|
226
|
+
if (remoteSha !== expected) {
|
|
227
|
+
return {
|
|
228
|
+
ok: false,
|
|
229
|
+
error: `Remote branch 'origin/${target}' points to ${remoteSha.slice(0, 8)}, not verified delivery ${expected.slice(0, 8)}. Checkout was not attempted.`,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
191
233
|
await git(repoDir, ['checkout', '-b', target, '--track', `origin/${target}`]);
|
|
192
234
|
}
|
|
193
235
|
else {
|
|
@@ -197,6 +239,24 @@ async function checkoutProjectReviewBranch(repoDir, branch) {
|
|
|
197
239
|
catch (err) {
|
|
198
240
|
return { ok: false, error: compactCheckoutError(err instanceof Error ? err.message : 'git checkout failed') };
|
|
199
241
|
}
|
|
242
|
+
if (expected) {
|
|
243
|
+
try {
|
|
244
|
+
const [currentBranch, currentSha] = await Promise.all([
|
|
245
|
+
git(repoDir, ['rev-parse', '--abbrev-ref', 'HEAD']),
|
|
246
|
+
git(repoDir, ['rev-parse', '--verify', 'HEAD']),
|
|
247
|
+
]);
|
|
248
|
+
if (currentBranch !== target || currentSha.toLowerCase() !== expected) {
|
|
249
|
+
return {
|
|
250
|
+
ok: false,
|
|
251
|
+
error: `Checkout changed concurrently and did not land on verified delivery ${expected.slice(0, 8)}. No reset or cleanup was performed.`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
return { ok: true };
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
return { ok: false, error: compactCheckoutError(err instanceof Error ? err.message : 'git checkout verification failed') };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
200
260
|
// A failed ff-only pull should not undo a successful checkout; the branch is
|
|
201
261
|
// already where the user can inspect/fix it. Surface only hard checkout errors.
|
|
202
262
|
await git(repoDir, ['pull', '--ff-only']).catch(() => { });
|