@henryqw/pi-pr 4.0.2 → 4.0.4
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 +12 -10
- package/extensions/pr-ci.ts +0 -1
- package/extensions/pr-command.ts +36 -27
- package/extensions/pr-comment-sweep.ts +0 -27
- package/extensions/pr-create.ts +76 -230
- package/extensions/pr-execution.ts +28 -163
- package/extensions/pr-github.ts +339 -193
- package/extensions/pr-merge.ts +24 -111
- package/extensions/pr-routing.ts +6 -2
- package/extensions/pr-ui.ts +1 -2
- package/extensions/pr-update-branch.ts +28 -90
- package/extensions/pr.ts +36 -58
- package/package.json +6 -2
- package/skills/pi-pr-create/SKILL.md +5 -5
package/extensions/pr-create.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
findExactHeadPullRequests,
|
|
6
6
|
loadCurrentPullRequest,
|
|
7
7
|
loadPullRequestPublication,
|
|
8
|
+
preflightPullRequestCreation,
|
|
8
9
|
readPullRequestBaseRefOid,
|
|
9
10
|
readTrackingOid,
|
|
10
11
|
readValidatedRemoteAuthority,
|
|
@@ -24,17 +25,13 @@ import {
|
|
|
24
25
|
readRemoteOid,
|
|
25
26
|
requiredOid,
|
|
26
27
|
requiredText,
|
|
27
|
-
resolveRepositoryFetchSource,
|
|
28
28
|
runChecked,
|
|
29
29
|
spawnBounded,
|
|
30
30
|
withWorktreeLock,
|
|
31
|
-
type AttemptState,
|
|
32
31
|
type Exec,
|
|
33
32
|
type ExecOptions,
|
|
34
33
|
} from "./pr-execution.ts";
|
|
35
34
|
|
|
36
|
-
const BASE_RERUN = "Rerun /pr --base=<host>/<owner>/<repository>:<ref>";
|
|
37
|
-
const MAX_REMOTE_BRANCHES = 128;
|
|
38
35
|
const MAX_TITLE_BYTES = 256;
|
|
39
36
|
const MAX_BODY_BYTES = 64 * 1024;
|
|
40
37
|
const CREATE_AUTHORITY_QUERY = "query($baseOwner:String!,$baseName:String!,$headOwner:String!,$headName:String!){base:repository(owner:$baseOwner,name:$baseName){id nameWithOwner}head:repository(owner:$headOwner,name:$headName){id nameWithOwner owner{__typename}}createInput:__type(name:\"CreatePullRequestInput\"){inputFields{name}}}";
|
|
@@ -60,16 +57,6 @@ type CrossRepositoryCreateAuthority = {
|
|
|
60
57
|
|
|
61
58
|
export type CreatePullRequestState = {
|
|
62
59
|
phase: CreatePhase;
|
|
63
|
-
attempts: {
|
|
64
|
-
fetchBase: AttemptState;
|
|
65
|
-
merge: AttemptState;
|
|
66
|
-
stage: AttemptState;
|
|
67
|
-
continueMerge: AttemptState;
|
|
68
|
-
push: AttemptState;
|
|
69
|
-
fetchTracking: AttemptState;
|
|
70
|
-
setUpstream: AttemptState;
|
|
71
|
-
pullRequest: AttemptState;
|
|
72
|
-
};
|
|
73
60
|
base?: CreateBaseAuthority;
|
|
74
61
|
createAuthority?: CrossRepositoryCreateAuthority;
|
|
75
62
|
mergeHead?: string;
|
|
@@ -107,16 +94,6 @@ function line(output: string, label: string): string {
|
|
|
107
94
|
return values[0];
|
|
108
95
|
}
|
|
109
96
|
|
|
110
|
-
function configuredValues(output: string): string[] {
|
|
111
|
-
if (output === "") return [];
|
|
112
|
-
const normalized = output.replace(/\r\n/g, "\n");
|
|
113
|
-
const values = normalized.endsWith("\n") ? normalized.slice(0, -1).split("\n") : normalized.split("\n");
|
|
114
|
-
if (values.some((value) => !value) || new Set(values).size !== values.length) {
|
|
115
|
-
throw new Error("Git configuration returned invalid values");
|
|
116
|
-
}
|
|
117
|
-
return values;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
97
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
121
98
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
122
99
|
}
|
|
@@ -191,14 +168,6 @@ function parseCreatedUrl(output: string, host: string, baseRepository: string):
|
|
|
191
168
|
return url;
|
|
192
169
|
}
|
|
193
170
|
|
|
194
|
-
function parseExplicitBase(value: string): Omit<CreateBaseAuthority, "oid" | "fetchSource"> {
|
|
195
|
-
const match = /^([a-z0-9](?:[a-z0-9.-]*[a-z0-9])?)\/([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+):(.+)$/.exec(value);
|
|
196
|
-
if (!match || match[2] === "." || match[2] === ".." || match[3] === "." || match[3] === "..") {
|
|
197
|
-
throw new Error(`Invalid base authority. ${BASE_RERUN}`);
|
|
198
|
-
}
|
|
199
|
-
return { host: match[1]!.toLowerCase(), repository: `${match[2]}/${match[3]}`, ref: requiredText(match[4], "base ref") };
|
|
200
|
-
}
|
|
201
|
-
|
|
202
171
|
function resolvedPaths(paths: readonly string[], expected: readonly string[]): string[] {
|
|
203
172
|
if (!Array.isArray(paths)) throw new TypeError("resolvedPaths must be an array");
|
|
204
173
|
const parsed = parseNulPaths(`${paths.join("\0")}${paths.length ? "\0" : ""}`, "Resolved conflict paths");
|
|
@@ -209,21 +178,17 @@ function resolvedPaths(paths: readonly string[], expected: readonly string[]): s
|
|
|
209
178
|
}
|
|
210
179
|
|
|
211
180
|
export class PullRequestCreator {
|
|
212
|
-
readonly state: CreatePullRequestState = {
|
|
213
|
-
phase: "unprepared",
|
|
214
|
-
attempts: {
|
|
215
|
-
fetchBase: "none", merge: "none", stage: "none", continueMerge: "none", push: "none",
|
|
216
|
-
fetchTracking: "none", setUpstream: "none", pullRequest: "none",
|
|
217
|
-
},
|
|
218
|
-
};
|
|
181
|
+
readonly state: CreatePullRequestState = { phase: "unprepared" };
|
|
219
182
|
|
|
220
183
|
private readonly cwd: string;
|
|
221
184
|
private readonly target: PullRequestTarget;
|
|
222
185
|
private readonly noTarget: boolean;
|
|
186
|
+
private noTargetUpstreamConfigured = false;
|
|
223
187
|
private readonly signal?: AbortSignal;
|
|
224
188
|
private readonly agentDir?: string;
|
|
225
189
|
private readonly exec: Exec;
|
|
226
190
|
private readonly load: Load;
|
|
191
|
+
private explicitBase?: string;
|
|
227
192
|
|
|
228
193
|
constructor(options: CreatePullRequestOptions) {
|
|
229
194
|
if (!options.target || options.target.remoteOid !== null && !requiredOid(options.target.remoteOid, "remote OID")) {
|
|
@@ -257,7 +222,7 @@ export class PullRequestCreator {
|
|
|
257
222
|
}
|
|
258
223
|
|
|
259
224
|
private async freshNone(): Promise<void> {
|
|
260
|
-
const discovery = await this.load(this.pi(), this.context());
|
|
225
|
+
const discovery = await this.load(this.pi(), this.context(), undefined, undefined, this.explicitBase);
|
|
261
226
|
if (discovery.kind !== "none" || !sameTarget(this.target, discovery.creationTarget)) {
|
|
262
227
|
throw new Error("PR creation cancelled: fresh complete discovery is no longer none");
|
|
263
228
|
}
|
|
@@ -265,71 +230,6 @@ export class PullRequestCreator {
|
|
|
265
230
|
if (branch !== this.target.branch) throw new Error("PR creation cancelled: current branch changed");
|
|
266
231
|
}
|
|
267
232
|
|
|
268
|
-
private async originAuthority() {
|
|
269
|
-
return await readValidatedRemoteAuthority(this.pi(), this.context(), "origin");
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
private async inferBaseRef(): Promise<{ authority: Awaited<ReturnType<PullRequestCreator["originAuthority"]>>; ref: string }> {
|
|
273
|
-
const authority = await this.originAuthority();
|
|
274
|
-
await runChecked(this.exec, "git", [
|
|
275
|
-
"fetch", "--prune", "--no-write-fetch-head", "--no-tags", "--no-recurse-submodules",
|
|
276
|
-
authority.fetchSource, "+refs/heads/*:refs/remotes/origin/*",
|
|
277
|
-
], this.options());
|
|
278
|
-
const refs = await runChecked(this.exec, "git", [
|
|
279
|
-
"for-each-ref", "--format=%(refname)%09%(objectname)%09%(symref)", "refs/remotes/origin",
|
|
280
|
-
], this.options());
|
|
281
|
-
const rows = configuredValues(refs.stdout);
|
|
282
|
-
if (rows.length > MAX_REMOTE_BRANCHES) throw new Error(`${BASE_RERUN}; more than ${MAX_REMOTE_BRANCHES} base candidates exist`);
|
|
283
|
-
const originOwnsTarget = authority.host === this.target.host &&
|
|
284
|
-
authority.repository.toLowerCase() === this.target.repository.toLowerCase();
|
|
285
|
-
const candidates: Array<{ ref: string; score: number }> = [];
|
|
286
|
-
for (const row of rows) {
|
|
287
|
-
const parts = row.split("\t");
|
|
288
|
-
if (parts.length !== 3) throw new Error(`Base inference is unsafe. ${BASE_RERUN}`);
|
|
289
|
-
const prefix = "refs/remotes/origin/";
|
|
290
|
-
if (!parts[0]!.startsWith(prefix)) throw new Error(`Base inference is unsafe. ${BASE_RERUN}`);
|
|
291
|
-
const ref = parts[0]!.slice(prefix.length);
|
|
292
|
-
if (ref === "HEAD" || originOwnsTarget && ref === this.target.ref) continue;
|
|
293
|
-
if (!ref || parts[2] !== "") throw new Error(`Base inference is unsafe. ${BASE_RERUN}`);
|
|
294
|
-
const oid = requiredOid(parts[1], "remote base OID");
|
|
295
|
-
const distance = line((await runChecked(this.exec, "git", ["rev-list", "--left-right", "--count", `HEAD...${oid}`], this.options())).stdout, "base distance");
|
|
296
|
-
const counts = /^(\d+)\s+(\d+)$/.exec(distance);
|
|
297
|
-
if (!counts) throw new Error(`Base inference is unsafe. ${BASE_RERUN}`);
|
|
298
|
-
const score = Number(counts[1]) + Number(counts[2]);
|
|
299
|
-
if (!Number.isSafeInteger(score)) throw new Error(`Base inference is unsafe. ${BASE_RERUN}`);
|
|
300
|
-
candidates.push({ ref, score });
|
|
301
|
-
}
|
|
302
|
-
if (!candidates.length) throw new Error(`Base cannot be inferred. ${BASE_RERUN}`);
|
|
303
|
-
const minimum = Math.min(...candidates.map(({ score }) => score));
|
|
304
|
-
const nearest = candidates.filter(({ score }) => score === minimum);
|
|
305
|
-
if (nearest.length !== 1) throw new Error(`Base is ambiguous. ${BASE_RERUN}`);
|
|
306
|
-
return { authority, ref: nearest[0]!.ref };
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
private async resolveBase(explicit?: string): Promise<Omit<CreateBaseAuthority, "oid">> {
|
|
310
|
-
if (explicit !== undefined) {
|
|
311
|
-
const parsed = parseExplicitBase(explicit);
|
|
312
|
-
if (parsed.host !== this.target.host.toLowerCase()) {
|
|
313
|
-
throw new Error("PR creation base and head must use the same GitHub host");
|
|
314
|
-
}
|
|
315
|
-
await runChecked(this.exec, "git", ["check-ref-format", "--branch", parsed.ref], this.options());
|
|
316
|
-
return { ...parsed, fetchSource: await resolveRepositoryFetchSource(this.exec, this.options(), parsed) };
|
|
317
|
-
}
|
|
318
|
-
const configured = await runChecked(this.exec, "git", [
|
|
319
|
-
"config", "--get-all", `branch.${this.target.branch}.gh-merge-base`,
|
|
320
|
-
], this.options(), [0, 1]);
|
|
321
|
-
const values = configured.code === 1 && configured.stdout === "" ? [] : configuredValues(configured.stdout);
|
|
322
|
-
if (values.length > 1) throw new Error(`Configured base is ambiguous. ${BASE_RERUN}`);
|
|
323
|
-
if (values.length === 1) {
|
|
324
|
-
await runChecked(this.exec, "git", ["check-ref-format", "--branch", values[0]!], this.options());
|
|
325
|
-
const authority = await this.originAuthority();
|
|
326
|
-
return { ...authority, ref: values[0]! };
|
|
327
|
-
}
|
|
328
|
-
const inferred = await this.inferBaseRef();
|
|
329
|
-
await runChecked(this.exec, "git", ["check-ref-format", "--branch", inferred.ref], this.options());
|
|
330
|
-
return { ...inferred.authority, ref: inferred.ref };
|
|
331
|
-
}
|
|
332
|
-
|
|
333
233
|
private async liveBase(): Promise<string> {
|
|
334
234
|
if (!this.state.base) throw new Error("PR creation base is unavailable");
|
|
335
235
|
return await readPullRequestBaseRefOid(this.pi(), this.context(), this.state.base);
|
|
@@ -358,40 +258,48 @@ export class PullRequestCreator {
|
|
|
358
258
|
}
|
|
359
259
|
|
|
360
260
|
async prepare(explicitBase?: string): Promise<CreatePullRequestResult> {
|
|
361
|
-
if (this.state.phase !== "unprepared"
|
|
261
|
+
if (this.state.phase !== "unprepared") {
|
|
362
262
|
throw new Error("PR creation prepare action was already consumed");
|
|
363
263
|
}
|
|
364
264
|
return await withWorktreeLock(this.cwd, async () => {
|
|
265
|
+
this.explicitBase = explicitBase;
|
|
365
266
|
await this.freshNone();
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
await runChecked(this.exec, "git", [
|
|
375
|
-
"fetch", "--no-write-fetch-head", "--no-tags", "--no-recurse-submodules", base.fetchSource, oid,
|
|
376
|
-
], this.options());
|
|
377
|
-
await runChecked(this.exec, "git", ["cat-file", "-e", `${oid}^{commit}`], this.options());
|
|
378
|
-
this.state.attempts.fetchBase = "applied";
|
|
379
|
-
} catch (error) {
|
|
380
|
-
this.state.attempts.fetchBase = "unknown";
|
|
381
|
-
throw error;
|
|
267
|
+
const preflight = await preflightPullRequestCreation(
|
|
268
|
+
this.pi(),
|
|
269
|
+
this.context(),
|
|
270
|
+
this.target,
|
|
271
|
+
this.explicitBase,
|
|
272
|
+
);
|
|
273
|
+
if (preflight.ahead === 0) {
|
|
274
|
+
throw new Error("PR creation requires at least one commit ahead of the selected base");
|
|
382
275
|
}
|
|
276
|
+
const { base } = preflight;
|
|
277
|
+
this.state.base = {
|
|
278
|
+
host: base.host,
|
|
279
|
+
repository: base.repository,
|
|
280
|
+
ref: base.ref,
|
|
281
|
+
oid: base.oid,
|
|
282
|
+
fetchSource: base.fetchSource,
|
|
283
|
+
};
|
|
284
|
+
this.state.phase = "blocked";
|
|
285
|
+
await runChecked(this.exec, "git", [
|
|
286
|
+
"fetch", "--no-write-fetch-head", "--no-tags", "--no-recurse-submodules", base.fetchSource, base.oid,
|
|
287
|
+
], this.options());
|
|
288
|
+
await runChecked(this.exec, "git", ["cat-file", "-e", `${base.oid}^{commit}`], this.options());
|
|
383
289
|
await this.freshNone();
|
|
384
|
-
if (await this.
|
|
385
|
-
|
|
290
|
+
if (await readHead(this.exec, this.options()) !== preflight.head) {
|
|
291
|
+
throw new Error("PR creation cancelled: local HEAD changed during prepare");
|
|
292
|
+
}
|
|
293
|
+
if (await this.liveBase() !== base.oid) throw new Error("PR creation cancelled: base ref moved during prepare");
|
|
386
294
|
this.state.phase = "prepared";
|
|
387
|
-
return { kind: "prepared", base: { ...this.state.base }, mergeBase };
|
|
295
|
+
return { kind: "prepared", base: { ...this.state.base }, mergeBase: base.mergeBase };
|
|
388
296
|
}, { agentDir: this.agentDir, signal: this.signal });
|
|
389
297
|
}
|
|
390
298
|
|
|
391
299
|
private async verifyMerge(originalHead: string): Promise<{ head: string; fastForward: boolean }> {
|
|
392
300
|
const base = this.state.base!;
|
|
393
301
|
const head = await readHead(this.exec, this.options());
|
|
394
|
-
const commits = line((await runChecked(this.exec, "git", ["rev-list", "--parents", "-n", "1",
|
|
302
|
+
const commits = line((await runChecked(this.exec, "git", ["rev-list", "--parents", "-n", "1", head], this.options())).stdout, "merge parents")
|
|
395
303
|
.split(" ").map((value, index) => requiredOid(value, index ? "merge parent" : "merged HEAD"));
|
|
396
304
|
if (commits[0] !== head) throw new Error("PR creation merge verification returned a different HEAD");
|
|
397
305
|
let fastForward = false;
|
|
@@ -419,7 +327,7 @@ export class PullRequestCreator {
|
|
|
419
327
|
}
|
|
420
328
|
|
|
421
329
|
async merge(): Promise<CreatePullRequestResult> {
|
|
422
|
-
if (this.state.phase !== "prepared" || !this.state.base
|
|
330
|
+
if (this.state.phase !== "prepared" || !this.state.base) {
|
|
423
331
|
throw new Error("PR creation is not prepared for merge");
|
|
424
332
|
}
|
|
425
333
|
return await withWorktreeLock(this.cwd, async () => {
|
|
@@ -431,35 +339,17 @@ export class PullRequestCreator {
|
|
|
431
339
|
this.state.mergeHead = originalHead;
|
|
432
340
|
return { kind: "verified", head: originalHead, fastForward: false };
|
|
433
341
|
}
|
|
434
|
-
this.state.
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
result = await this.exec("git", ["merge", "--no-edit", this.state.base!.oid], this.options());
|
|
438
|
-
} catch (error) {
|
|
439
|
-
this.state.attempts.merge = "unknown";
|
|
440
|
-
throw error;
|
|
441
|
-
}
|
|
442
|
-
if (result.killed) {
|
|
443
|
-
this.state.attempts.merge = "unknown";
|
|
444
|
-
throw new Error("git merge was killed; its outcome is unknown");
|
|
445
|
-
}
|
|
342
|
+
this.state.phase = "blocked";
|
|
343
|
+
const result = await this.exec("git", ["merge", "--no-edit", this.state.base!.oid], this.options());
|
|
344
|
+
if (result.killed) throw new Error("git merge was killed; its outcome is unknown");
|
|
446
345
|
if (result.code === 0) {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
this.state.attempts.merge = "applied";
|
|
450
|
-
return { kind: "verified", ...verified };
|
|
451
|
-
} catch (error) {
|
|
452
|
-
this.state.attempts.merge = "unknown";
|
|
453
|
-
throw error;
|
|
454
|
-
}
|
|
346
|
+
const verified = await this.verifyMerge(originalHead);
|
|
347
|
+
return { kind: "verified", ...verified };
|
|
455
348
|
}
|
|
456
349
|
try {
|
|
457
350
|
const paths = await this.captureConflict(originalHead);
|
|
458
|
-
this.state.attempts.merge = "applied";
|
|
459
351
|
return { kind: "conflict", paths };
|
|
460
352
|
} catch (error) {
|
|
461
|
-
this.state.attempts.merge = "blocked";
|
|
462
|
-
this.state.phase = "blocked";
|
|
463
353
|
throw new Error(`git merge failed: ${result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`}; ${error instanceof Error ? error.message : String(error)}`);
|
|
464
354
|
}
|
|
465
355
|
}, { agentDir: this.agentDir, signal: this.signal });
|
|
@@ -469,9 +359,6 @@ export class PullRequestCreator {
|
|
|
469
359
|
if (this.state.phase !== "conflict-awaiting-user" || !this.state.conflict || !this.state.base) {
|
|
470
360
|
throw new Error("PR creation has no conflict awaiting continuation");
|
|
471
361
|
}
|
|
472
|
-
if (this.state.attempts.stage !== "none" || this.state.attempts.continueMerge !== "none") {
|
|
473
|
-
throw new Error("PR creation conflict continuation was already consumed");
|
|
474
|
-
}
|
|
475
362
|
const paths = resolvedPaths(pathsInput, this.state.conflict.paths);
|
|
476
363
|
return await withWorktreeLock(this.cwd, async () => {
|
|
477
364
|
await this.freshNone();
|
|
@@ -481,26 +368,13 @@ export class PullRequestCreator {
|
|
|
481
368
|
if (mergeHead !== this.state.base!.oid) throw new Error("PR creation merge context changed");
|
|
482
369
|
const status = await runChecked(this.exec, "git", ["status", "--porcelain=v2", "-z", "--untracked-files=all"], this.options());
|
|
483
370
|
assertOnlyDeclaredStatusChanged(this.state.conflict!.statusBaseline, status.stdout, paths);
|
|
484
|
-
this.state.
|
|
485
|
-
|
|
486
|
-
await runChecked(this.exec, "git", ["add", "--", ...paths], this.options());
|
|
487
|
-
this.state.attempts.stage = "applied";
|
|
488
|
-
} catch (error) {
|
|
489
|
-
this.state.attempts.stage = "unknown";
|
|
490
|
-
throw error;
|
|
491
|
-
}
|
|
371
|
+
this.state.phase = "blocked";
|
|
372
|
+
await runChecked(this.exec, "git", ["add", "--", ...paths], this.options());
|
|
492
373
|
const unmerged = parseNulPaths((await runChecked(this.exec, "git", ["diff", "--name-only", "-z", "--diff-filter=U"], this.options())).stdout, "Unmerged paths");
|
|
493
374
|
if (unmerged.length) throw new Error(`Conflict paths remain unresolved: ${unmerged.join(", ")}`);
|
|
494
|
-
this.
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const verified = await this.verifyMerge(this.state.conflict!.originalHead);
|
|
498
|
-
this.state.attempts.continueMerge = "applied";
|
|
499
|
-
return { kind: "verified", ...verified };
|
|
500
|
-
} catch (error) {
|
|
501
|
-
this.state.attempts.continueMerge = "unknown";
|
|
502
|
-
throw error;
|
|
503
|
-
}
|
|
375
|
+
await runChecked(this.exec, "git", ["-c", "core.editor=true", "merge", "--continue"], this.options());
|
|
376
|
+
const verified = await this.verifyMerge(this.state.conflict!.originalHead);
|
|
377
|
+
return { kind: "verified", ...verified };
|
|
504
378
|
}, { agentDir: this.agentDir, signal: this.signal });
|
|
505
379
|
}
|
|
506
380
|
|
|
@@ -512,30 +386,16 @@ export class PullRequestCreator {
|
|
|
512
386
|
fetchSource: this.target.fetchSource,
|
|
513
387
|
remoteOid: head,
|
|
514
388
|
};
|
|
515
|
-
this.
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
if (await readTrackingOid(this.pi(), this.context(), branchTrackingRef(target)) !== head) {
|
|
519
|
-
throw new Error("Fetched tracking ref did not match published HEAD");
|
|
520
|
-
}
|
|
521
|
-
this.state.attempts.fetchTracking = "applied";
|
|
522
|
-
} catch (error) {
|
|
523
|
-
this.state.attempts.fetchTracking = "unknown";
|
|
524
|
-
throw error;
|
|
525
|
-
}
|
|
526
|
-
this.state.attempts.setUpstream = "attempting";
|
|
527
|
-
try {
|
|
528
|
-
await setBranchUpstream(this.pi(), this.context(), target);
|
|
529
|
-
await verifyBranchUpstream(this.pi(), this.context(), target);
|
|
530
|
-
this.state.attempts.setUpstream = "applied";
|
|
531
|
-
} catch (error) {
|
|
532
|
-
this.state.attempts.setUpstream = "unknown";
|
|
533
|
-
throw error;
|
|
389
|
+
await fetchBranchTrackingRef(this.pi(), this.context(), target);
|
|
390
|
+
if (await readTrackingOid(this.pi(), this.context(), branchTrackingRef(target)) !== head) {
|
|
391
|
+
throw new Error("Fetched tracking ref did not match published HEAD");
|
|
534
392
|
}
|
|
393
|
+
await setBranchUpstream(this.pi(), this.context(), target);
|
|
394
|
+
await verifyBranchUpstream(this.pi(), this.context(), target);
|
|
535
395
|
}
|
|
536
396
|
|
|
537
397
|
async push(): Promise<CreatePullRequestResult> {
|
|
538
|
-
if (this.state.phase !== "verified" || !this.state.base
|
|
398
|
+
if (this.state.phase !== "verified" || !this.state.base) {
|
|
539
399
|
throw new Error("PR creation is not ready to push");
|
|
540
400
|
}
|
|
541
401
|
return await withWorktreeLock(this.cwd, async () => {
|
|
@@ -554,29 +414,26 @@ export class PullRequestCreator {
|
|
|
554
414
|
if (await this.liveBase() !== this.state.base!.oid) throw new Error("PR creation cancelled: frozen base moved");
|
|
555
415
|
if (await this.requireCleanHead() !== head) throw new Error("PR creation cancelled: local HEAD changed before push");
|
|
556
416
|
this.state.publicationHead = head;
|
|
557
|
-
this.state.
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
throw new Error("Published remote ref did not match captured HEAD");
|
|
565
|
-
}
|
|
566
|
-
this.state.attempts.push = "applied";
|
|
567
|
-
} catch (error) {
|
|
568
|
-
this.state.attempts.push = "unknown";
|
|
569
|
-
throw error;
|
|
417
|
+
this.state.phase = "blocked";
|
|
418
|
+
await runChecked(this.exec, "git", [
|
|
419
|
+
"push", "--porcelain", `--force-with-lease=refs/heads/${this.target.ref}:${original ?? ""}`,
|
|
420
|
+
"--recurse-submodules=no", "--", this.target.fetchSource, `${head}:refs/heads/${this.target.ref}`,
|
|
421
|
+
], this.options());
|
|
422
|
+
if (await readRemoteOid(this.exec, this.options(), this.target.fetchSource, this.target.ref) !== head) {
|
|
423
|
+
throw new Error("Published remote ref did not match captured HEAD");
|
|
570
424
|
}
|
|
571
425
|
this.state.phase = "pushed";
|
|
572
|
-
if (this.noTarget)
|
|
426
|
+
if (this.noTarget) {
|
|
427
|
+
await this.configureNoTargetUpstream(head);
|
|
428
|
+
this.noTargetUpstreamConfigured = true;
|
|
429
|
+
}
|
|
573
430
|
return { kind: "pushed", head };
|
|
574
431
|
}, { agentDir: this.agentDir, signal: this.signal });
|
|
575
432
|
}
|
|
576
433
|
|
|
577
434
|
private async publishedAuthority(): Promise<void> {
|
|
578
435
|
const head = this.state.publicationHead!;
|
|
579
|
-
if (this.noTarget && this.
|
|
436
|
+
if (this.noTarget && !this.noTargetUpstreamConfigured) {
|
|
580
437
|
const branch = line((await runChecked(this.exec, "git", ["branch", "--show-current"], this.options())).stdout, "current branch");
|
|
581
438
|
const authority = await readValidatedRemoteAuthority(this.pi(), this.context(), this.target.remote);
|
|
582
439
|
if (branch !== this.target.branch || authority.host !== this.target.host ||
|
|
@@ -587,7 +444,7 @@ export class PullRequestCreator {
|
|
|
587
444
|
}
|
|
588
445
|
return;
|
|
589
446
|
}
|
|
590
|
-
const discovery = await this.load(this.pi(), this.context());
|
|
447
|
+
const discovery = await this.load(this.pi(), this.context(), undefined, undefined, this.explicitBase);
|
|
591
448
|
if (discovery.kind === "none") {
|
|
592
449
|
if (!sameTarget(this.target, discovery.creationTarget, head)) throw new Error("Published target authority changed");
|
|
593
450
|
return;
|
|
@@ -614,7 +471,7 @@ export class PullRequestCreator {
|
|
|
614
471
|
}
|
|
615
472
|
|
|
616
473
|
async publish(titleInput: string, body: string): Promise<CreatePullRequestResult> {
|
|
617
|
-
if (this.state.phase !== "pushed" || !this.state.base || !this.state.publicationHead
|
|
474
|
+
if (this.state.phase !== "pushed" || !this.state.base || !this.state.publicationHead) {
|
|
618
475
|
throw new Error("PR creation is not ready to publish metadata");
|
|
619
476
|
}
|
|
620
477
|
const title = requiredText(titleInput, "pull request title");
|
|
@@ -655,30 +512,19 @@ export class PullRequestCreator {
|
|
|
655
512
|
sameRepository ? this.target.ref : `${this.target.repository.split("/")[0]}:${this.target.ref}`,
|
|
656
513
|
"--base", this.state.base!.ref, "--title", title, "--body-file", "-",
|
|
657
514
|
];
|
|
658
|
-
this.state.
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
parseCreatedUrl(created.stdout, this.state.base!.host, this.state.base!.repository);
|
|
663
|
-
}
|
|
664
|
-
} catch (error) {
|
|
665
|
-
this.state.attempts.pullRequest = "unknown";
|
|
666
|
-
throw error;
|
|
515
|
+
this.state.phase = "blocked";
|
|
516
|
+
const created = await runChecked(this.exec, "gh", args, this.options({ stdin: organizationAuthority ? undefined : body }));
|
|
517
|
+
if (!before && organizationAuthority) {
|
|
518
|
+
parseCreatedUrl(created.stdout, this.state.base!.host, this.state.base!.repository);
|
|
667
519
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
throw new Error("Published pull request did not retain canonical identity, title, and body");
|
|
673
|
-
}
|
|
674
|
-
this.state.attempts.pullRequest = "applied";
|
|
675
|
-
this.state.phase = "published";
|
|
676
|
-
this.state.url = after.url.href;
|
|
677
|
-
return { kind: "published", url: after.url.href };
|
|
678
|
-
} catch (error) {
|
|
679
|
-
this.state.attempts.pullRequest = "unknown";
|
|
680
|
-
throw error;
|
|
520
|
+
const after = await this.exactCandidate();
|
|
521
|
+
if (!after || after.base.repository.toLowerCase() !== this.state.base!.repository.toLowerCase() ||
|
|
522
|
+
after.base.ref !== this.state.base!.ref || after.title !== title || after.body !== body) {
|
|
523
|
+
throw new Error("Published pull request did not retain canonical identity, title, and body");
|
|
681
524
|
}
|
|
525
|
+
this.state.phase = "published";
|
|
526
|
+
this.state.url = after.url.href;
|
|
527
|
+
return { kind: "published", url: after.url.href };
|
|
682
528
|
}, { agentDir: this.agentDir, signal: this.signal });
|
|
683
529
|
}
|
|
684
530
|
}
|