agent-dealer 1.1.3 → 1.1.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/bundle/server/dist/adapters/git-worktree.js +259 -10
- package/bundle/server/dist/adapters/git-worktree.test.js +566 -10
- package/bundle/server/dist/coordinator/admission.js +232 -9
- package/bundle/server/dist/coordinator/admission.test.js +227 -2
- package/bundle/server/dist/coordinator/agent-boundaries.js +88 -0
- package/bundle/server/dist/coordinator/commands.js +292 -10
- package/bundle/server/dist/coordinator/deck-outage.integration.test.js +5 -0
- package/bundle/server/dist/coordinator/developer-effect.js +130 -7
- package/bundle/server/dist/coordinator/developer-effect.test.js +249 -1
- package/bundle/server/dist/coordinator/execution-boundaries.test.js +493 -0
- package/bundle/server/dist/coordinator/execution-intervals.js +120 -0
- package/bundle/server/dist/coordinator/execution-intervals.test.js +109 -0
- package/bundle/server/dist/coordinator/failure-cause.js +478 -0
- package/bundle/server/dist/coordinator/failure-cause.test.js +287 -0
- package/bundle/server/dist/coordinator/failure-reason.test.js +21 -0
- package/bundle/server/dist/coordinator/human-resolution.js +17 -1
- package/bundle/server/dist/coordinator/latest-failure.js +51 -1
- package/bundle/server/dist/coordinator/latest-failure.test.js +55 -0
- package/bundle/server/dist/coordinator/projection.js +9 -1
- package/bundle/server/dist/coordinator/push-with-lease.test.js +271 -0
- package/bundle/server/dist/coordinator/queue-history.js +324 -0
- package/bundle/server/dist/coordinator/queue-history.test.js +287 -0
- package/bundle/server/dist/coordinator/recovery.js +38 -1
- package/bundle/server/dist/coordinator/recovery.test.js +63 -0
- package/bundle/server/dist/coordinator/reviewer-effect.js +77 -2
- package/bundle/server/dist/coordinator/routing.js +19 -1
- package/bundle/server/dist/db/index.js +30 -0
- package/bundle/server/dist/db/schema.sql +28 -0
- package/bundle/server/dist/repository/admission-settings.js +86 -0
- package/bundle/server/dist/repository/admission-settings.test.js +78 -0
- package/bundle/server/dist/repository/failure-causes.js +104 -0
- package/bundle/server/dist/repository/human-actions.js +19 -0
- package/bundle/server/dist/repository/queue-entries.js +86 -42
- package/bundle/server/dist/repository/workflow-events.js +8 -0
- package/bundle/server/dist/routes/execute-now.integration.test.js +305 -0
- package/bundle/server/dist/routes/issues.js +100 -17
- package/bundle/server/dist/routes/issues.test.js +159 -1
- package/bundle/server/dist/routes/queue.js +29 -2
- package/bundle/server/dist/routes/queue.test.js +65 -0
- package/bundle/server/package.json +2 -2
- package/bundle/server/static-ui/assets/index-Bva8TLlT.js +60 -0
- package/bundle/server/static-ui/assets/index-DDI4iqx0.css +1 -0
- package/bundle/server/static-ui/index.html +2 -2
- package/bundle/shared/dist/failure-cause.d.ts +67 -0
- package/bundle/shared/dist/failure-cause.js +67 -0
- package/bundle/shared/dist/index.d.ts +3 -2
- package/bundle/shared/dist/index.js +1 -0
- package/bundle/shared/dist/queue-entries.d.ts +67 -0
- package/bundle/shared/dist/queue-entries.js +25 -0
- package/bundle/shared/dist/workflow.d.ts +4 -4
- package/bundle/shared/dist/workflow.js +16 -0
- package/bundle/shared/dist/workflow.test.js +4 -0
- package/bundle/shared/package.json +1 -1
- package/dist/index.js +1 -0
- package/dist/issue.d.ts +3 -0
- package/dist/issue.js +15 -0
- package/dist/issue.test.js +44 -0
- package/package.json +1 -1
- package/bundle/server/static-ui/assets/index-CYZRXBZj.css +0 -1
- package/bundle/server/static-ui/assets/index-PU5ONp31.js +0 -60
|
@@ -88,6 +88,94 @@ export async function fetchFreshBase(repo, baseBranch, timeoutMs) {
|
|
|
88
88
|
return { ok: false, reason: `fetched origin ${baseBranch} but origin/${baseBranch} does not resolve: ${err.message}` };
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
+
/** `git fetch origin <branch>` output when the remote simply has no such branch. */
|
|
92
|
+
const NO_REMOTE_REF_PATTERNS = /couldn't find remote ref|remote ref .* does not exist/i;
|
|
93
|
+
function fetchErrorDetail(err) {
|
|
94
|
+
const e = err;
|
|
95
|
+
return typeof e?.stderr === "string" && e.stderr.trim() ? e.stderr.trim() : (e?.message ?? String(err));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* NOT-219: fetch `origin/<branch>` before reusing an existing issue branch for a
|
|
99
|
+
* repair round, so the worktree starts at exactly what Dealer pushed (and the
|
|
100
|
+
* reviewer reviewed) — never the managed clone's possibly-stale local ref. The
|
|
101
|
+
* stale-local case is real: a round-1 worker that commits on a side branch leaves
|
|
102
|
+
* the local issue branch at its creation point while Dealer pushes HEAD to
|
|
103
|
+
* `origin/<branch>`, and reusing the local ref then cherry-picks/push-rejects.
|
|
104
|
+
*
|
|
105
|
+
* A fetch that fails (network down, timeout) is returned — never silently replaced
|
|
106
|
+
* with the stale local branch: the caller defers the start like NOT-197's
|
|
107
|
+
* `base_unavailable`. The one fetch failure that is NOT a deferral is a remote with
|
|
108
|
+
* no such branch yet (a same-round retry whose branch was never pushed): there is
|
|
109
|
+
* nothing to catch up to, so the local branch is used as-is.
|
|
110
|
+
*/
|
|
111
|
+
export async function fetchReusedBranch(repo, branch, timeoutMs) {
|
|
112
|
+
const boundMs = clampFetchTimeoutMs(timeoutMs);
|
|
113
|
+
if (!(await hasOriginRemote(repo))) {
|
|
114
|
+
return { ok: true, remoteSha: null };
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
await run("git", ["fetch", "origin", branch], { cwd: repo, timeout: boundMs });
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const e = err;
|
|
121
|
+
const detail = fetchErrorDetail(err);
|
|
122
|
+
if (NO_REMOTE_REF_PATTERNS.test(detail)) {
|
|
123
|
+
return { ok: true, remoteSha: null };
|
|
124
|
+
}
|
|
125
|
+
const timedOut = e?.killed === true;
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
reason: timedOut
|
|
129
|
+
? `git fetch origin ${branch} timed out after ${boundMs}ms — network or VPN may be down`
|
|
130
|
+
: `git fetch origin ${branch} failed: ${detail}`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
const { stdout } = await git(repo, ["rev-parse", `origin/${branch}`]);
|
|
135
|
+
return { ok: true, remoteSha: stdout.trim() };
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
return { ok: false, reason: `fetched origin ${branch} but origin/${branch} does not resolve: ${err.message}` };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/** Whether `ancestor` is an ancestor of (or equal to) `descendant`. */
|
|
142
|
+
export async function isAncestor(repo, ancestor, descendant) {
|
|
143
|
+
try {
|
|
144
|
+
await git(repo, ["merge-base", "--is-ancestor", ancestor, descendant]);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* NOT-219: fast-forward the managed clone's local `branch` ref to `sha` (the SHA a
|
|
153
|
+
* developer push just published). Strictly a fast-forward — when the local ref has
|
|
154
|
+
* commits the pushed SHA lacks (diverged or ahead), it is preserved untouched and
|
|
155
|
+
* this returns false. Best-effort: returns false instead of throwing, so a caller
|
|
156
|
+
* can safely ignore bookkeeping it cannot complete — the next repair round's fetch
|
|
157
|
+
* heals a ref this missed anyway.
|
|
158
|
+
*/
|
|
159
|
+
export async function fastForwardLocalBranchToSha(opts) {
|
|
160
|
+
return withRepoLock(opts.repo, async () => {
|
|
161
|
+
try {
|
|
162
|
+
if (!(await branchExists(opts.repo, opts.branch))) {
|
|
163
|
+
await git(opts.repo, ["branch", opts.branch, opts.sha]);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
const localSha = await revParseRef(opts.repo, `refs/heads/${opts.branch}`);
|
|
167
|
+
if (localSha === opts.sha)
|
|
168
|
+
return true;
|
|
169
|
+
if (!(await isAncestor(opts.repo, localSha, opts.sha)))
|
|
170
|
+
return false;
|
|
171
|
+
await git(opts.repo, ["update-ref", `refs/heads/${opts.branch}`, opts.sha, localSha]);
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
91
179
|
export async function addWorktree(opts) {
|
|
92
180
|
const args = ["worktree", "add"];
|
|
93
181
|
if (opts.detach)
|
|
@@ -326,17 +414,98 @@ async function gatherPushRejectionFacts(opts) {
|
|
|
326
414
|
return null;
|
|
327
415
|
}
|
|
328
416
|
}
|
|
329
|
-
|
|
417
|
+
/**
|
|
418
|
+
* NOT-220: whether a rejected diverged push is provably safe to recover with a
|
|
419
|
+
* lease-pinned force push. Returns the remote SHA to pin the lease to, or null
|
|
420
|
+
* when today's `unpushed_commit` escalation must stand. ALL must hold:
|
|
421
|
+
*
|
|
422
|
+
* 1. The fetched remote tip is exactly the last head Dealer verified/pushed
|
|
423
|
+
* (`lastKnownHeadSha`) — anyone else having pushed since rules recovery out.
|
|
424
|
+
* 2. Every remote-only commit has a patch-equivalent local counterpart:
|
|
425
|
+
* `git cherry <local> <remote>` lists the remote-only non-merge commits, one
|
|
426
|
+
* line each, `-` when an equivalent patch exists locally. (Note the argument
|
|
427
|
+
* order: head must be the remote so the lines enumerate remote-only commits.)
|
|
428
|
+
* 3. The output is non-empty AND the remote-only range holds no merge commits —
|
|
429
|
+
* `git cherry` never lists merges, so a pure-merge range would otherwise be
|
|
430
|
+
* vacuously "all equivalent" with nothing actually proven.
|
|
431
|
+
*/
|
|
432
|
+
async function leasePushExpectedRemoteSha(opts) {
|
|
433
|
+
try {
|
|
434
|
+
if (!opts.lastKnownHeadSha || opts.remoteSha !== opts.lastKnownHeadSha)
|
|
435
|
+
return null;
|
|
436
|
+
const { stdout } = await git(opts.cwd, ["cherry", opts.localSha, opts.remoteSha]);
|
|
437
|
+
const lines = stdout
|
|
438
|
+
.split("\n")
|
|
439
|
+
.map((l) => l.trim())
|
|
440
|
+
.filter((l) => l.length > 0);
|
|
441
|
+
if (lines.length === 0)
|
|
442
|
+
return null;
|
|
443
|
+
if (!lines.every((l) => l.startsWith("-")))
|
|
444
|
+
return null;
|
|
445
|
+
const { stdout: merges } = await git(opts.cwd, [
|
|
446
|
+
"rev-list",
|
|
447
|
+
"--merges",
|
|
448
|
+
`${opts.localSha}..${opts.remoteSha}`,
|
|
449
|
+
]);
|
|
450
|
+
if (merges.trim().length > 0)
|
|
451
|
+
return null;
|
|
452
|
+
return opts.remoteSha;
|
|
453
|
+
}
|
|
454
|
+
catch {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* NOT-220: the single lease-pinned retry. Pins the lease to the fetched remote
|
|
460
|
+
* SHA — a race where origin moves between fetch and push makes this fail instead
|
|
461
|
+
* of overwriting anyone's work. Same `-u` upstream tracking as the plain push.
|
|
462
|
+
*/
|
|
463
|
+
export async function pushWithLease(opts) {
|
|
464
|
+
try {
|
|
465
|
+
await git(opts.cwd, [
|
|
466
|
+
"push",
|
|
467
|
+
"-u",
|
|
468
|
+
"origin",
|
|
469
|
+
`--force-with-lease=refs/heads/${opts.branch}:${opts.expectedRemoteSha}`,
|
|
470
|
+
`${opts.localRef}:refs/heads/${opts.branch}`,
|
|
471
|
+
]);
|
|
472
|
+
return { ok: true, newSha: await revParseRef(opts.cwd, opts.localRef) };
|
|
473
|
+
}
|
|
474
|
+
catch (err) {
|
|
475
|
+
return { ok: false, reason: err.message };
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
async function pushFailureResult(cwd, branch, localRef, message, lastKnownHeadSha) {
|
|
330
479
|
const rejected = PUSH_REJECTION_PATTERNS.test(message);
|
|
331
480
|
if (!rejected)
|
|
332
481
|
return { ok: false, reason: message, rejected: false };
|
|
333
482
|
const facts = await gatherPushRejectionFacts({ cwd, branch, localRef });
|
|
334
|
-
if (facts)
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
483
|
+
if (!facts)
|
|
484
|
+
return { ok: false, reason: message, rejected: true };
|
|
485
|
+
// NOT-220: a divergence Dealer can prove safe to resolve (remote tip is the
|
|
486
|
+
// last verified head, every remote-only commit patch-equivalent locally) is
|
|
487
|
+
// retried once with the lease pin — no human action needed.
|
|
488
|
+
if (facts.relationship === "diverged") {
|
|
489
|
+
const pin = await leasePushExpectedRemoteSha({
|
|
490
|
+
cwd,
|
|
491
|
+
localSha: facts.localSha,
|
|
492
|
+
remoteSha: facts.remoteSha,
|
|
493
|
+
lastKnownHeadSha,
|
|
494
|
+
});
|
|
495
|
+
if (pin !== null) {
|
|
496
|
+
const retried = await pushWithLease({ cwd, branch, localRef, expectedRemoteSha: pin });
|
|
497
|
+
if (retried.ok)
|
|
498
|
+
return { ok: true, leasePush: { oldSha: pin, newSha: retried.newSha } };
|
|
499
|
+
// The lease lost its race (or the retry failed another way): re-gather so
|
|
500
|
+
// the escalation names the tip that actually beat us, not the stale one.
|
|
501
|
+
const fresh = await gatherPushRejectionFacts({ cwd, branch, localRef });
|
|
502
|
+
if (fresh)
|
|
503
|
+
return { ok: false, reason: fresh.summary, rejected: true, facts: fresh };
|
|
504
|
+
}
|
|
338
505
|
}
|
|
339
|
-
|
|
506
|
+
// Prefer the classified summary over raw git stderr so the escalation never carries
|
|
507
|
+
// git's misleading `use 'git pull'` hint for a diverged branch (NOT-137).
|
|
508
|
+
return { ok: false, reason: facts.summary, rejected: true, facts };
|
|
340
509
|
}
|
|
341
510
|
/**
|
|
342
511
|
* The coordinator — never the developer worker — pushes the branch, after the worker's
|
|
@@ -352,7 +521,7 @@ export async function pushBranch(opts) {
|
|
|
352
521
|
return { ok: true };
|
|
353
522
|
}
|
|
354
523
|
catch (err) {
|
|
355
|
-
return pushFailureResult(opts.worktreePath, opts.branch, "HEAD", err.message);
|
|
524
|
+
return pushFailureResult(opts.worktreePath, opts.branch, "HEAD", err.message, opts.lastKnownHeadSha);
|
|
356
525
|
}
|
|
357
526
|
}
|
|
358
527
|
/**
|
|
@@ -367,7 +536,46 @@ export async function pushBranchRef(opts) {
|
|
|
367
536
|
return { ok: true };
|
|
368
537
|
}
|
|
369
538
|
catch (err) {
|
|
370
|
-
return pushFailureResult(opts.repo, opts.branch, `refs/heads/${opts.branch}`, err.message);
|
|
539
|
+
return pushFailureResult(opts.repo, opts.branch, `refs/heads/${opts.branch}`, err.message, opts.lastKnownHeadSha);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* NOT-221: publish already-finished commits over a diverged remote with a lease pinned
|
|
544
|
+
* to the exact remote tip the operator reviewed (`remoteSha`, stored in the
|
|
545
|
+
* push_with_lease action's evidence). The explicit `<localSha>:refs/heads/<branch>`
|
|
546
|
+
* refspec pushes the recorded local tip even when the checkout's HEAD has since moved
|
|
547
|
+
* (or the push runs from the repo rather than the preserved worktree).
|
|
548
|
+
*
|
|
549
|
+
* When origin no longer matches the pin the push fails and nothing is published — the
|
|
550
|
+
* caller leaves the action open and shows the freshly observed tip instead.
|
|
551
|
+
*/
|
|
552
|
+
export async function pushLeaseToSha(opts) {
|
|
553
|
+
try {
|
|
554
|
+
await git(opts.cwd, [
|
|
555
|
+
"push",
|
|
556
|
+
`--force-with-lease=refs/heads/${opts.branch}:${opts.remoteSha}`,
|
|
557
|
+
"origin",
|
|
558
|
+
`${opts.localSha}:refs/heads/${opts.branch}`,
|
|
559
|
+
]);
|
|
560
|
+
return { ok: true };
|
|
561
|
+
}
|
|
562
|
+
catch (err) {
|
|
563
|
+
return { ok: false, reason: err.message };
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* NOT-221: read origin's live tip for a branch without touching local refs — shown on
|
|
568
|
+
* the still-open action after a lease push fails, so the operator sees what moved.
|
|
569
|
+
* Null when the branch cannot be resolved remotely (deleted, renamed, or unreachable).
|
|
570
|
+
*/
|
|
571
|
+
export async function readRemoteTip(opts) {
|
|
572
|
+
try {
|
|
573
|
+
const { stdout } = await git(opts.cwd, ["ls-remote", "origin", `refs/heads/${opts.branch}`]);
|
|
574
|
+
const sha = stdout.trim().split(/\s+/)[0];
|
|
575
|
+
return sha && /^[0-9a-f]{40}$/i.test(sha) ? sha : null;
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
return null;
|
|
371
579
|
}
|
|
372
580
|
}
|
|
373
581
|
/** Commits on HEAD not on `baseRef` — zero means the developer produced nothing to push/PR. */
|
|
@@ -500,6 +708,18 @@ export function sessionIdFromRoleWorktreePath(worktreePath) {
|
|
|
500
708
|
export async function resolveDeveloperWorktree(opts) {
|
|
501
709
|
return withRepoLock(opts.repo, async () => {
|
|
502
710
|
await pruneWorktrees(opts.repo);
|
|
711
|
+
// NOT-219: a repair round reuses an existing issue branch, so first fetch
|
|
712
|
+
// `origin/<branch>` — the worktree must start at exactly what Dealer pushed,
|
|
713
|
+
// never the managed clone's possibly-stale local ref. A failed fetch defers the
|
|
714
|
+
// start (`base_unavailable`) instead of falling back to the stale local branch.
|
|
715
|
+
let reuseRemoteSha = null;
|
|
716
|
+
if (opts.reuseBranch) {
|
|
717
|
+
const reused = await fetchReusedBranch(opts.repo, opts.branchName, opts.fetchTimeoutMs);
|
|
718
|
+
if (!reused.ok) {
|
|
719
|
+
return { kind: "base_unavailable", reason: reused.reason };
|
|
720
|
+
}
|
|
721
|
+
reuseRemoteSha = reused.remoteSha;
|
|
722
|
+
}
|
|
503
723
|
const existing = await findWorktreeForBranch(opts.repo, opts.branchName);
|
|
504
724
|
if (existing) {
|
|
505
725
|
const managedRoot = opts.worktreePath
|
|
@@ -526,6 +746,18 @@ export async function resolveDeveloperWorktree(opts) {
|
|
|
526
746
|
}
|
|
527
747
|
const state = await inspectLeftoverWorktree(existing);
|
|
528
748
|
if (state === "clean") {
|
|
749
|
+
// NOT-219: a clean leftover holding a stale local branch advances to the
|
|
750
|
+
// fetched remote tip when that is a strict fast-forward (nothing unique to
|
|
751
|
+
// lose — the local tip is already an ancestor of the remote one). Any other
|
|
752
|
+
// relationship (ahead, diverged) keeps the leftover exactly as-is.
|
|
753
|
+
if (reuseRemoteSha !== null) {
|
|
754
|
+
const localSha = await revParseRef(opts.repo, `refs/heads/${opts.branchName}`).catch(() => null);
|
|
755
|
+
if (localSha !== null &&
|
|
756
|
+
localSha !== reuseRemoteSha &&
|
|
757
|
+
(await isAncestor(opts.repo, localSha, reuseRemoteSha))) {
|
|
758
|
+
await git(existing, ["merge", "--ff-only", "-q", `origin/${opts.branchName}`]).catch(() => null);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
529
761
|
return { kind: "reused", path: tryRealpath(existing) };
|
|
530
762
|
}
|
|
531
763
|
if (state === "dirty_or_unpushed") {
|
|
@@ -564,8 +796,7 @@ export async function resolveDeveloperWorktree(opts) {
|
|
|
564
796
|
// NOT-197: a fresh issue branch starts from the freshly fetched
|
|
565
797
|
// `origin/<baseBranch>` tip — never the cached clone's possibly-stale local base.
|
|
566
798
|
// A failed fetch returns `base_unavailable` (no branch is created) instead of
|
|
567
|
-
// silently falling back to the stale local branch.
|
|
568
|
-
// fetch — it checks out a branch that already exists — and is unchanged.
|
|
799
|
+
// silently falling back to the stale local branch.
|
|
569
800
|
let ref;
|
|
570
801
|
let newBranch;
|
|
571
802
|
let baseSha = null;
|
|
@@ -580,7 +811,25 @@ export async function resolveDeveloperWorktree(opts) {
|
|
|
580
811
|
baseSha = fresh.sha;
|
|
581
812
|
baseRef = fresh.ref;
|
|
582
813
|
}
|
|
814
|
+
else if (reuseRemoteSha !== null && !(await branchExists(opts.repo, opts.branchName))) {
|
|
815
|
+
// NOT-219: the remote has the issue branch but the clone has no local ref for
|
|
816
|
+
// it (e.g. round 1 committed on a side branch and never created the local
|
|
817
|
+
// issue branch) — cut it at the fetched `origin/<branch>` tip.
|
|
818
|
+
ref = `origin/${opts.branchName}`;
|
|
819
|
+
newBranch = opts.branchName;
|
|
820
|
+
}
|
|
583
821
|
else {
|
|
822
|
+
// NOT-219: when the remote tip is known and the local ref is strictly behind
|
|
823
|
+
// it (an ancestor of it), advance the local ref to the pushed tip before
|
|
824
|
+
// checking it out — the repair worktree then starts at HEAD == origin/<branch>.
|
|
825
|
+
// A local ref with commits the remote lacks (ahead or diverged) is preserved
|
|
826
|
+
// untouched: it is checked out as-is, exactly like the pre-NOT-219 behavior.
|
|
827
|
+
if (reuseRemoteSha !== null && (await branchExists(opts.repo, opts.branchName))) {
|
|
828
|
+
const localSha = await revParseRef(opts.repo, `refs/heads/${opts.branchName}`);
|
|
829
|
+
if (localSha !== reuseRemoteSha && (await isAncestor(opts.repo, localSha, reuseRemoteSha))) {
|
|
830
|
+
await git(opts.repo, ["update-ref", `refs/heads/${opts.branchName}`, reuseRemoteSha, localSha]);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
584
833
|
ref = opts.branchName;
|
|
585
834
|
newBranch = undefined;
|
|
586
835
|
}
|