@henryqw/pi-pr 6.0.8 → 6.2.0
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/README.md +151 -247
- package/docs/pr-routing.svg +13 -13
- package/extensions/pr-command.ts +68 -94
- package/extensions/pr-comment-sweep.ts +245 -53
- package/extensions/pr-create.ts +50 -80
- package/extensions/pr-feedback-attention.ts +69 -0
- package/extensions/pr-feedback.ts +23 -1
- package/extensions/pr-publish-work.ts +143 -0
- package/extensions/pr-routing.ts +4 -44
- package/extensions/pr-ui.ts +2 -0
- package/extensions/pr-update-branch.ts +187 -63
- package/extensions/pr.ts +154 -50
- package/package.json +1 -1
- package/skills/pi-pr-comment-sweep/SKILL.md +9 -46
- package/skills/pi-pr-comment-sweep/references/recovery.md +5 -24
- package/skills/pi-pr-comment-sweep/references/thread-triage.md +3 -5
- package/skills/pi-pr-create/SKILL.md +4 -6
- package/skills/pi-pr-publish-work/SKILL.md +12 -0
- package/skills/pi-pr-update-branch/SKILL.md +5 -7
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { extensionConfigDir, readTextFileBounded, writePrivateTextFileAtomically } from "@henryqw/pi-config-store";
|
|
1
5
|
import { spawnBounded, type Exec, type ExecOptions } from "@henryqw/pi-process";
|
|
2
6
|
import {
|
|
3
7
|
cloneCurrentPullRequest,
|
|
@@ -13,6 +17,8 @@ import {
|
|
|
13
17
|
isAncestor,
|
|
14
18
|
parseNulPaths,
|
|
15
19
|
parseSingleOutputLine,
|
|
20
|
+
inspectGitOperation,
|
|
21
|
+
isRecord,
|
|
16
22
|
readHead,
|
|
17
23
|
readRemoteOid,
|
|
18
24
|
requiredOid,
|
|
@@ -27,7 +33,7 @@ type UpdateBranchPhase = "ready" | "conflict-awaiting-user" | "verified" | "publ
|
|
|
27
33
|
type UpdateBranchState = {
|
|
28
34
|
phase: UpdateBranchPhase;
|
|
29
35
|
verifiedHead?: string;
|
|
30
|
-
conflict?: { paths: string[]; statusBaseline: string };
|
|
36
|
+
conflict?: { paths: string[]; statusBaseline: string; head: string };
|
|
31
37
|
};
|
|
32
38
|
|
|
33
39
|
type UpdateBranchResult =
|
|
@@ -48,7 +54,59 @@ export type UpdateBranchOptions = {
|
|
|
48
54
|
|
|
49
55
|
function sameAuthority(frozen: CurrentPullRequest, fresh: CurrentPullRequest): boolean {
|
|
50
56
|
return samePullRequestSnapshot(frozen, fresh) && frozen.base.oid === fresh.base.oid &&
|
|
51
|
-
fresh.lifecycle === "open" &&
|
|
57
|
+
fresh.lifecycle === "open" && fresh.conditions.conflict;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type RebaseRecovery = { version: 1; phase: "pending" | "verified" | "published"; identity: string;
|
|
61
|
+
original: string; remote: string; verified: string | null };
|
|
62
|
+
|
|
63
|
+
function recoveryIdentity(pr: CurrentPullRequest): string {
|
|
64
|
+
return JSON.stringify([pr.id, pr.number, pr.url.href, pr.host, pr.base.repository, pr.base.ref, pr.base.oid,
|
|
65
|
+
pr.head.repository, pr.head.ref, pr.target.provenance, pr.target.branch, pr.target.remote,
|
|
66
|
+
pr.target.ref, pr.target.repository, pr.target.host, pr.target.fetchSource]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function recoveryPath(cwd: string, pr: CurrentPullRequest, agentDir?: string): string {
|
|
70
|
+
const key = createHash("sha256").update(`${resolve(cwd)}\n${pr.url.href}`).digest("hex");
|
|
71
|
+
return join(extensionConfigDir("pi-pr", agentDir), "update-branch", `${key}.json`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function readRecovery(path: string, signal?: AbortSignal): Promise<RebaseRecovery | null> {
|
|
75
|
+
let raw: string;
|
|
76
|
+
try { raw = await readTextFileBounded(path, 4096, { signal }); }
|
|
77
|
+
catch (error) {
|
|
78
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
let record: unknown;
|
|
82
|
+
try { record = JSON.parse(raw); } catch { throw new Error(`Invalid branch update recovery is preserved at ${path}`); }
|
|
83
|
+
if (!isRecord(record) || Object.keys(record).sort().join(",") !== "identity,original,phase,remote,verified,version" ||
|
|
84
|
+
record.version !== 1 || !["pending", "verified", "published"].includes(record.phase as string) ||
|
|
85
|
+
typeof record.identity !== "string" || record.identity.length > 2048 ||
|
|
86
|
+
typeof record.original !== "string" || !/^[0-9a-f]{40}$|^[0-9a-f]{64}$/.test(record.original) ||
|
|
87
|
+
typeof record.remote !== "string" || !/^[0-9a-f]{40}$|^[0-9a-f]{64}$/.test(record.remote) ||
|
|
88
|
+
(record.verified !== null && (typeof record.verified !== "string" || !/^[0-9a-f]{40}$|^[0-9a-f]{64}$/.test(record.verified))) ||
|
|
89
|
+
(record.phase === "pending" && record.verified !== null) ||
|
|
90
|
+
(record.phase !== "pending" && record.verified === null)) {
|
|
91
|
+
throw new Error(`Invalid branch update recovery is preserved at ${path}`);
|
|
92
|
+
}
|
|
93
|
+
return record as RebaseRecovery;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function assertVerifiedRecovery(record: RebaseRecovery, pr: CurrentPullRequest, path: string): void {
|
|
97
|
+
if (record.identity !== recoveryIdentity(pr) || record.remote !== record.original ||
|
|
98
|
+
![record.remote, record.verified].includes(pr.head.oid) || pr.target.remoteOid !== pr.head.oid) {
|
|
99
|
+
throw new Error(`Branch update recovery authority changed; record preserved at ${path}`);
|
|
100
|
+
}
|
|
101
|
+
if (record.phase === "pending") throw new Error(`Branch rebase outcome is unverified; recover manually from ${path}; do not replay it`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function inspectVerifiedRebaseRecovery(pr: CurrentPullRequest, options: { cwd: string; agentDir?: string; signal?: AbortSignal }): Promise<boolean> {
|
|
105
|
+
const path = recoveryPath(options.cwd, pr, options.agentDir);
|
|
106
|
+
const record = await readRecovery(path, options.signal);
|
|
107
|
+
if (!record || record.phase === "published") return false;
|
|
108
|
+
assertVerifiedRecovery(record, pr, path);
|
|
109
|
+
return true;
|
|
52
110
|
}
|
|
53
111
|
|
|
54
112
|
export class PullRequestBranchUpdater {
|
|
@@ -60,6 +118,8 @@ export class PullRequestBranchUpdater {
|
|
|
60
118
|
private readonly agentDir?: string;
|
|
61
119
|
private readonly exec: Exec;
|
|
62
120
|
private readonly load: Load;
|
|
121
|
+
private recovered = false;
|
|
122
|
+
private recoveredRecord?: RebaseRecovery;
|
|
63
123
|
|
|
64
124
|
constructor(options: UpdateBranchOptions) {
|
|
65
125
|
if (!options.authority || options.authority.target.provenance !== "configured") {
|
|
@@ -85,9 +145,17 @@ export class PullRequestBranchUpdater {
|
|
|
85
145
|
return { cwd: this.cwd, signal: this.signal ?? new AbortController().signal };
|
|
86
146
|
}
|
|
87
147
|
|
|
88
|
-
private async freshAuthority(expectedHead: string, requireClean: boolean): Promise<CurrentPullRequest> {
|
|
148
|
+
private async freshAuthority(expectedHead: string, requireClean: boolean, published = false): Promise<CurrentPullRequest> {
|
|
89
149
|
const discovery = await this.load(this.pi(), this.context());
|
|
90
|
-
|
|
150
|
+
const comparison = published ? {
|
|
151
|
+
...this.authority,
|
|
152
|
+
head: { ...this.authority.head, oid: expectedHead },
|
|
153
|
+
target: { ...this.authority.target, remoteOid: expectedHead },
|
|
154
|
+
} : this.authority;
|
|
155
|
+
if (discovery.kind !== "current" || (published
|
|
156
|
+
? !samePullRequestSnapshot(comparison, discovery.pullRequest) ||
|
|
157
|
+
this.authority.base.oid !== discovery.pullRequest.base.oid || discovery.pullRequest.lifecycle !== "open"
|
|
158
|
+
: !sameAuthority(this.authority, discovery.pullRequest))) {
|
|
91
159
|
throw new Error("Branch update cancelled: frozen pull request authority changed");
|
|
92
160
|
}
|
|
93
161
|
const branch = parseSingleOutputLine((await runChecked(this.exec, "git", ["branch", "--show-current"], this.execOptions())).stdout, "current branch");
|
|
@@ -100,39 +168,71 @@ export class PullRequestBranchUpdater {
|
|
|
100
168
|
return discovery.pullRequest;
|
|
101
169
|
}
|
|
102
170
|
|
|
103
|
-
private async
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
throw new Error(
|
|
171
|
+
private async writeRecovery(phase: RebaseRecovery["phase"], verified: string | null): Promise<void> {
|
|
172
|
+
const path = recoveryPath(this.cwd, this.authority, this.agentDir);
|
|
173
|
+
const existing = await readRecovery(path, this.signal);
|
|
174
|
+
const identity = recoveryIdentity(this.authority);
|
|
175
|
+
const original = this.recoveredRecord?.original ?? this.authority.head.oid;
|
|
176
|
+
const remote = this.recoveredRecord?.remote ?? this.authority.target.remoteOid;
|
|
177
|
+
if (!remote) throw new Error("Branch update remote ref is absent");
|
|
178
|
+
if (existing && existing.phase !== "published" &&
|
|
179
|
+
(existing.identity !== identity || existing.original !== original || existing.remote !== remote || phase === "pending")) {
|
|
180
|
+
throw new Error(`Branch update recovery must be inspected before replacement: ${path}`);
|
|
113
181
|
}
|
|
114
|
-
|
|
115
|
-
|
|
182
|
+
await writePrivateTextFileAtomically(path, `${JSON.stringify({ version: 1, phase, identity,
|
|
183
|
+
original, remote, verified })}\n`, { signal: this.signal });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async recoveryLaunchAction(): Promise<"rebase"> {
|
|
187
|
+
const path = recoveryPath(this.cwd, this.authority, this.agentDir);
|
|
188
|
+
const recovery = await readRecovery(path, this.signal);
|
|
189
|
+
if (!recovery || recovery.phase === "published") return "rebase";
|
|
190
|
+
assertVerifiedRecovery(recovery, this.authority, path);
|
|
191
|
+
const head = recovery.verified!;
|
|
192
|
+
const branch = parseSingleOutputLine((await runChecked(this.exec, "git", ["branch", "--show-current"], this.execOptions())).stdout, "current branch");
|
|
193
|
+
if (branch !== this.authority.target.branch || await inspectWorktree(this.exec, this.execOptions()) !== "clean" ||
|
|
194
|
+
await readHead(this.exec, this.execOptions()) !== head ||
|
|
195
|
+
!(await isAncestor(this.exec, this.execOptions(), this.authority.base.oid, head))) {
|
|
196
|
+
throw new Error("Branch update recovery does not match a clean verified branch; do not replay the rebase");
|
|
197
|
+
}
|
|
198
|
+
this.state.phase = "verified";
|
|
199
|
+
this.state.verifiedHead = head;
|
|
200
|
+
this.recoveredRecord = recovery;
|
|
201
|
+
this.recovered = true;
|
|
202
|
+
return "rebase"; // The prompted action returns the verified result without replaying Git.
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private async verifyRebase(): Promise<UpdateBranchResult> {
|
|
206
|
+
const head = await readHead(this.exec, this.execOptions());
|
|
207
|
+
if (await inspectWorktree(this.exec, this.execOptions()) !== "clean" ||
|
|
208
|
+
!(await isAncestor(this.exec, this.execOptions(), this.authority.base.oid, head))) {
|
|
209
|
+
throw new Error("Rebase did not leave a clean branch based on the frozen base");
|
|
116
210
|
}
|
|
211
|
+
await this.writeRecovery("verified", head);
|
|
117
212
|
this.state.phase = "verified";
|
|
118
213
|
this.state.verifiedHead = head;
|
|
119
214
|
delete this.state.conflict;
|
|
120
|
-
return { head, fastForward };
|
|
215
|
+
return { kind: "verified", head, fastForward: false };
|
|
121
216
|
}
|
|
122
217
|
|
|
123
|
-
private async captureConflict(): Promise<
|
|
124
|
-
|
|
125
|
-
|
|
218
|
+
private async captureConflict(): Promise<UpdateBranchResult> {
|
|
219
|
+
if (await inspectGitOperation(this.exec, this.execOptions()) === null) {
|
|
220
|
+
throw new Error("Failed rebase did not retain an in-progress Git operation");
|
|
221
|
+
}
|
|
126
222
|
const paths = parseNulPaths((await runChecked(this.exec, "git", ["diff", "--name-only", "-z", "--diff-filter=U"], this.execOptions())).stdout, "Unmerged paths");
|
|
127
|
-
if (!paths.length) throw new Error("git
|
|
223
|
+
if (!paths.length) throw new Error("git rebase failed without bounded unmerged paths");
|
|
128
224
|
const status = await runChecked(this.exec, "git", ["status", "--porcelain=v2", "-z", "--untracked-files=all"], this.execOptions());
|
|
129
225
|
this.state.phase = "conflict-awaiting-user";
|
|
130
|
-
this.state.conflict = { paths, statusBaseline: status.stdout };
|
|
131
|
-
return paths;
|
|
226
|
+
this.state.conflict = { paths, statusBaseline: status.stdout, head: await readHead(this.exec, this.execOptions()) };
|
|
227
|
+
return { kind: "conflict", paths };
|
|
132
228
|
}
|
|
133
229
|
|
|
134
|
-
async
|
|
135
|
-
if (this.state.phase
|
|
230
|
+
async rebase(): Promise<UpdateBranchResult> {
|
|
231
|
+
if (this.recovered && this.state.phase === "verified" && this.state.verifiedHead) {
|
|
232
|
+
this.recovered = false;
|
|
233
|
+
return { kind: "verified", head: this.state.verifiedHead, fastForward: false };
|
|
234
|
+
}
|
|
235
|
+
if (this.state.phase !== "ready") throw new Error("Branch conflict rebase action was already consumed");
|
|
136
236
|
return await withWorktreeLock(this.cwd, async () => {
|
|
137
237
|
await this.freshAuthority(this.authority.head.oid, true);
|
|
138
238
|
const source = await resolveRepositoryFetchSource(this.exec, this.execOptions(), {
|
|
@@ -146,47 +246,53 @@ export class PullRequestBranchUpdater {
|
|
|
146
246
|
await runChecked(this.exec, "git", ["cat-file", "-e", `${this.authority.base.oid}^{commit}`], this.execOptions());
|
|
147
247
|
await this.freshAuthority(this.authority.head.oid, true);
|
|
148
248
|
if (await isAncestor(this.exec, this.execOptions(), this.authority.base.oid, this.authority.head.oid)) {
|
|
149
|
-
this.
|
|
150
|
-
this.state.verifiedHead = this.authority.head.oid;
|
|
151
|
-
return { kind: "verified", head: this.authority.head.oid, fastForward: false };
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const result = await this.exec("git", ["merge", "--no-edit", this.authority.base.oid], this.execOptions());
|
|
155
|
-
if (result.killed) throw new Error("git merge was killed; its outcome is unknown");
|
|
156
|
-
if (result.code === 0) {
|
|
157
|
-
const verified = await this.verifyMerge(this.authority.head.oid);
|
|
158
|
-
return { kind: "verified", ...verified };
|
|
249
|
+
return await this.verifyRebase();
|
|
159
250
|
}
|
|
251
|
+
const mergeBase = requiredOid(parseSingleOutputLine((await runChecked(this.exec, "git", [
|
|
252
|
+
"merge-base", this.authority.base.oid, this.authority.head.oid,
|
|
253
|
+
], this.execOptions())).stdout, "rebase fork point"), "rebase fork point");
|
|
254
|
+
const mergeCommits = (await runChecked(this.exec, "git", [
|
|
255
|
+
"rev-list", "--max-count=1", "--min-parents=2", `${mergeBase}..${this.authority.head.oid}`,
|
|
256
|
+
], this.execOptions())).stdout.trim();
|
|
257
|
+
if (mergeCommits) throw new Error("Branch update cannot rebase a branch with merge commits; preserve its resolutions manually");
|
|
258
|
+
await this.freshAuthority(this.authority.head.oid, true);
|
|
259
|
+
await this.writeRecovery("pending", null);
|
|
260
|
+
const result = await this.exec("git", ["-c", "core.editor=true", "-c", "rebase.backend=merge", "rebase", "--no-autostash", "--onto", this.authority.base.oid, mergeBase], this.execOptions());
|
|
261
|
+
if (result.killed) throw new Error("git rebase was killed; its outcome is unknown");
|
|
262
|
+
if (result.code === 0) return await this.verifyRebase();
|
|
160
263
|
try {
|
|
161
|
-
|
|
162
|
-
return { kind: "conflict", paths: [...paths] };
|
|
264
|
+
return await this.captureConflict();
|
|
163
265
|
} catch (error) {
|
|
164
|
-
|
|
165
|
-
throw new Error(`git merge failed: ${detail}; ${error instanceof Error ? error.message : String(error)}`);
|
|
266
|
+
throw new Error(`git rebase failed: ${result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`}; ${error instanceof Error ? error.message : String(error)}`);
|
|
166
267
|
}
|
|
167
268
|
}, { agentDir: this.agentDir, signal: this.signal });
|
|
168
269
|
}
|
|
169
270
|
|
|
170
271
|
async continue(resolvedPaths: readonly string[]): Promise<UpdateBranchResult> {
|
|
171
272
|
if (this.state.phase !== "conflict-awaiting-user" || !this.state.conflict) {
|
|
172
|
-
throw new Error("Branch
|
|
273
|
+
throw new Error("Branch rebase has no conflict awaiting continuation");
|
|
173
274
|
}
|
|
174
275
|
const paths = validateResolvedConflictPaths(resolvedPaths, this.state.conflict.paths);
|
|
175
276
|
return await withWorktreeLock(this.cwd, async () => {
|
|
176
|
-
await this.
|
|
177
|
-
|
|
178
|
-
|
|
277
|
+
const discovery = await this.load(this.pi(), this.context());
|
|
278
|
+
if (discovery.kind !== "current" || !sameAuthority(this.authority, discovery.pullRequest)) {
|
|
279
|
+
throw new Error("Branch rebase authority changed");
|
|
280
|
+
}
|
|
281
|
+
const path = parseSingleOutputLine((await runChecked(this.exec, "git", ["rev-parse", "--git-path", "rebase-merge/head-name"], this.execOptions())).stdout, "rebase branch marker");
|
|
282
|
+
if ((await readFile(resolve(this.cwd, path), "utf8")).trim() !== `refs/heads/${this.authority.target.branch}` ||
|
|
283
|
+
await readHead(this.exec, this.execOptions()) !== this.state.conflict!.head) {
|
|
284
|
+
throw new Error("Branch rebase context changed");
|
|
285
|
+
}
|
|
179
286
|
const status = await runChecked(this.exec, "git", ["status", "--porcelain=v2", "-z", "--untracked-files=all"], this.execOptions());
|
|
180
287
|
assertOnlyDeclaredStatusChanged(this.state.conflict!.statusBaseline, status.stdout, paths);
|
|
181
|
-
|
|
182
288
|
this.state.phase = "blocked";
|
|
183
|
-
await runChecked(this.exec, "git", ["add", "--", ...paths], this.execOptions());
|
|
289
|
+
await runChecked(this.exec, "git", ["--literal-pathspecs", "add", "--", ...paths], this.execOptions());
|
|
184
290
|
const unmerged = parseNulPaths((await runChecked(this.exec, "git", ["diff", "--name-only", "-z", "--diff-filter=U"], this.execOptions())).stdout, "Unmerged paths");
|
|
185
291
|
if (unmerged.length) throw new Error(`Conflict paths remain unresolved: ${unmerged.join(", ")}`);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return
|
|
292
|
+
const result = await this.exec("git", ["-c", "core.editor=true", "rebase", "--continue"], this.execOptions());
|
|
293
|
+
if (result.killed) throw new Error("git rebase continuation was killed; its outcome is unknown");
|
|
294
|
+
if (result.code === 0) return await this.verifyRebase();
|
|
295
|
+
return await this.captureConflict();
|
|
190
296
|
}, { agentDir: this.agentDir, signal: this.signal });
|
|
191
297
|
}
|
|
192
298
|
|
|
@@ -196,26 +302,44 @@ export class PullRequestBranchUpdater {
|
|
|
196
302
|
}
|
|
197
303
|
const head = this.state.verifiedHead;
|
|
198
304
|
return await withWorktreeLock(this.cwd, async () => {
|
|
199
|
-
|
|
305
|
+
const original = this.recoveredRecord?.remote ?? this.authority.target.remoteOid;
|
|
306
|
+
if (original === null) throw new Error("Current pull request remote ref is absent");
|
|
307
|
+
const remoteBefore = await readRemoteOid(this.exec, this.execOptions(), this.authority.target.fetchSource, this.authority.target.ref);
|
|
308
|
+
if (remoteBefore !== original && remoteBefore !== head) throw new Error("Rebase push outcome is unknown; remote ref has an unexpected OID; do not retry");
|
|
309
|
+
await this.freshAuthority(head, true, remoteBefore === head && head !== original);
|
|
200
310
|
if (!(await isAncestor(this.exec, this.execOptions(), this.authority.base.oid, head))) {
|
|
201
311
|
throw new Error("Verified branch no longer contains the frozen base");
|
|
202
312
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
313
|
+
if (remoteBefore === head) {
|
|
314
|
+
await this.writeRecovery("published", head);
|
|
315
|
+
this.state.phase = "published";
|
|
316
|
+
return { kind: "published", head };
|
|
207
317
|
}
|
|
208
318
|
await this.freshAuthority(head, true);
|
|
209
319
|
this.state.phase = "blocked";
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
320
|
+
let pushError: unknown;
|
|
321
|
+
try {
|
|
322
|
+
await runChecked(this.exec, "git", [
|
|
323
|
+
"push", "--porcelain", `--force-with-lease=refs/heads/${this.authority.target.ref}:${original}`,
|
|
324
|
+
"--recurse-submodules=no", "--", this.authority.target.fetchSource,
|
|
325
|
+
`${head}:refs/heads/${this.authority.target.ref}`,
|
|
326
|
+
], this.execOptions());
|
|
327
|
+
} catch (error) {
|
|
328
|
+
pushError = error;
|
|
329
|
+
}
|
|
330
|
+
let remote: string | null;
|
|
331
|
+
try {
|
|
332
|
+
remote = await readRemoteOid(this.exec, this.execOptions(), this.authority.target.fetchSource, this.authority.target.ref);
|
|
333
|
+
} catch {
|
|
334
|
+
throw new Error("Rebase push outcome is unknown; do not retry");
|
|
335
|
+
}
|
|
336
|
+
if (remote === head) {
|
|
337
|
+
await this.writeRecovery("published", head);
|
|
338
|
+
this.state.phase = "published";
|
|
339
|
+
return { kind: "published", head };
|
|
340
|
+
}
|
|
341
|
+
if (remote === original) throw new Error(`Rebase push was not applied${pushError ? ": " + String(pushError) : ""}`);
|
|
342
|
+
throw new Error("Rebase push outcome is unknown; remote ref has an unexpected OID; do not retry");
|
|
219
343
|
}, { agentDir: this.agentDir, signal: this.signal });
|
|
220
344
|
}
|
|
221
345
|
}
|