@goodtek/vibeops 3.0.0 → 3.1.1
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/CHANGELOG.md +30 -0
- package/README.md +41 -3
- package/dist/commands/pull.js +16 -0
- package/dist/commands/task-add.js +175 -97
- package/dist/commands/task-del.js +7 -0
- package/dist/commands/task-ship.js +7 -0
- package/dist/commands/task-sync.js +60 -28
- package/dist/lib/git-integration-sync.js +77 -71
- package/dist/lib/git-worktree.js +214 -0
- package/dist/lib/git.js +128 -105
- package/dist/lib/task-del.js +7 -5
- package/dist/lib/task-git-commit.js +9 -3
- package/dist/lib/task-id-allocation.js +192 -0
- package/dist/lib/task-lock.js +356 -0
- package/dist/lib/task-new-cycle.js +12 -1
- package/dist/lib/task-push-pr.js +7 -0
- package/dist/lib/task-reship.js +27 -12
- package/dist/lib/task-start.js +35 -26
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { gitBranchExists, gitFetch, gitGovernanceOnlyDirty, gitLeftRightCount, gitRemoteBranchExists, gitRemoteUrl, gitRevParse, readGitInfo, } from "./git.js";
|
|
2
|
+
import { fastForwardLocalIntegration, logLocalIntegrationUpdate, resolveIntegrationBaseRef, } from "./git-worktree.js";
|
|
2
3
|
import { cyan, dim, log } from "./logger.js";
|
|
3
4
|
function remoteRef(remote, branch) {
|
|
4
5
|
return `${remote}/${branch}`;
|
|
@@ -43,7 +44,7 @@ export async function diagnoseIntegrationSync(cwd, remote, integrationBranch) {
|
|
|
43
44
|
"git status",
|
|
44
45
|
`# Blocking (non-governance): ${blocking.join(", ")}${more}`,
|
|
45
46
|
"git stash push -u -m \"vibeops: before sync\"",
|
|
46
|
-
`git
|
|
47
|
+
`git switch ${integrationBranch} # or run in the worktree that has it (git worktree list)`,
|
|
47
48
|
`git pull --ff-only ${remote} ${integrationBranch}`,
|
|
48
49
|
"git stash pop # if you stashed",
|
|
49
50
|
"vibeops task add",
|
|
@@ -52,8 +53,20 @@ export async function diagnoseIntegrationSync(cwd, remote, integrationBranch) {
|
|
|
52
53
|
}
|
|
53
54
|
// Governance-only dirty (.vibeops.json after init, docs, …) — proceed.
|
|
54
55
|
}
|
|
55
|
-
const localSha = await gitRevParse(cwd, integrationBranch);
|
|
56
56
|
const remoteSha = await gitRevParse(cwd, remoteRef(remote, integrationBranch));
|
|
57
|
+
const localSha = (await gitBranchExists(cwd, integrationBranch))
|
|
58
|
+
? await gitRevParse(cwd, `refs/heads/${integrationBranch}`)
|
|
59
|
+
: null;
|
|
60
|
+
if (localSha === null && remoteSha !== null) {
|
|
61
|
+
// No local integration branch (e.g. a fresh worktree) — task branches start
|
|
62
|
+
// from the remote-tracking ref, nothing to fast-forward.
|
|
63
|
+
return {
|
|
64
|
+
ok: true,
|
|
65
|
+
kind: "ok",
|
|
66
|
+
summary: `No local ${integrationBranch} — using ${remote}/${integrationBranch}.`,
|
|
67
|
+
fixes: [],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
57
70
|
if (localSha === null || remoteSha === null) {
|
|
58
71
|
return {
|
|
59
72
|
ok: false,
|
|
@@ -61,7 +74,7 @@ export async function diagnoseIntegrationSync(cwd, remote, integrationBranch) {
|
|
|
61
74
|
summary: `Could not resolve ${integrationBranch} or ${remote}/${integrationBranch}.`,
|
|
62
75
|
fixes: [
|
|
63
76
|
`git fetch ${remote}`,
|
|
64
|
-
`git
|
|
77
|
+
`git switch ${integrationBranch} # or run in the worktree that has it (git worktree list)`,
|
|
65
78
|
`git pull --ff-only ${remote} ${integrationBranch}`,
|
|
66
79
|
],
|
|
67
80
|
};
|
|
@@ -84,7 +97,7 @@ export async function diagnoseIntegrationSync(cwd, remote, integrationBranch) {
|
|
|
84
97
|
summary: `Local ${integrationBranch} and ${remote}/${integrationBranch} have diverged (local +${ahead}, remote +${behind}).`,
|
|
85
98
|
fixes: [
|
|
86
99
|
`git fetch ${remote}`,
|
|
87
|
-
`git
|
|
100
|
+
`git switch ${integrationBranch} # or run in the worktree that has it (git worktree list)`,
|
|
88
101
|
`# Prefer remote (discards local-only commits on ${integrationBranch}):`,
|
|
89
102
|
`git reset --hard ${remote}/${integrationBranch}`,
|
|
90
103
|
`# Or keep local commits: git pull --rebase ${remote} ${integrationBranch}`,
|
|
@@ -100,7 +113,7 @@ export async function diagnoseIntegrationSync(cwd, remote, integrationBranch) {
|
|
|
100
113
|
kind: "ahead",
|
|
101
114
|
summary: `Local ${integrationBranch} is ${ahead} commit(s) ahead of ${remote}/${integrationBranch} — fast-forward pull cannot apply.`,
|
|
102
115
|
fixes: [
|
|
103
|
-
`git
|
|
116
|
+
`git switch ${integrationBranch} # or run in the worktree that has it (git worktree list)`,
|
|
104
117
|
`# Push local commits, or reset to remote if they should not exist:`,
|
|
105
118
|
`git push -u ${remote} ${integrationBranch}`,
|
|
106
119
|
`# or: git reset --hard ${remote}/${integrationBranch}`,
|
|
@@ -139,94 +152,87 @@ export function printIntegrationSyncDiagnosis(d) {
|
|
|
139
152
|
}
|
|
140
153
|
}
|
|
141
154
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
155
|
+
* Worktree-safe integration preflight (D-006). Never checks out the integration
|
|
156
|
+
* branch (another worktree may own it) and never force-resets:
|
|
157
|
+
* 1. `git fetch <remote> <integration>`;
|
|
158
|
+
* 2. diagnose (app dirt / local ahead / diverged → refuse with fixes);
|
|
159
|
+
* 3. fast-forward the local integration ref only where safe
|
|
160
|
+
* ({@link fastForwardLocalIntegration});
|
|
161
|
+
* 4. return the base ref for the task branch (`<remote>/<integration>`).
|
|
145
162
|
*/
|
|
146
163
|
export async function ensureIntegrationSynced(opts) {
|
|
147
164
|
const { cwd, remote, integrationBranch } = opts;
|
|
148
165
|
const doFetch = opts.fetch !== false;
|
|
149
166
|
if (opts.dryRun === true) {
|
|
150
167
|
const d = await diagnoseIntegrationSync(cwd, remote, integrationBranch);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
168
|
+
const ok = d.ok || d.kind === "no_remote_branch";
|
|
169
|
+
return {
|
|
170
|
+
ok,
|
|
171
|
+
diagnosis: d,
|
|
172
|
+
pulled: false,
|
|
173
|
+
baseRef: ok ? await resolveIntegrationBaseRef(cwd, remote, integrationBranch) : null,
|
|
174
|
+
};
|
|
156
175
|
}
|
|
157
|
-
if (doFetch) {
|
|
176
|
+
if ((await gitRemoteUrl(cwd, remote)) !== null && doFetch) {
|
|
158
177
|
try {
|
|
159
|
-
await
|
|
178
|
+
await gitFetch(cwd, remote, integrationBranch);
|
|
160
179
|
}
|
|
161
180
|
catch (e) {
|
|
162
181
|
const msg = e instanceof Error ? e.message : String(e);
|
|
163
182
|
log.warn(dim(`git fetch failed (${msg}) — diagnosing with local refs.`));
|
|
164
183
|
}
|
|
165
184
|
}
|
|
166
|
-
const
|
|
167
|
-
if (git.branch !== integrationBranch) {
|
|
168
|
-
const switched = await gitSwitchToBranch(cwd, integrationBranch, remote);
|
|
169
|
-
if (!switched) {
|
|
170
|
-
return {
|
|
171
|
-
ok: false,
|
|
172
|
-
diagnosis: {
|
|
173
|
-
ok: false,
|
|
174
|
-
kind: "pull_failed",
|
|
175
|
-
summary: `Could not switch to integration branch "${integrationBranch}".`,
|
|
176
|
-
fixes: [
|
|
177
|
-
"git status",
|
|
178
|
-
`git checkout ${integrationBranch}`,
|
|
179
|
-
`# If checkout fails due to local changes: git stash -u`,
|
|
180
|
-
`git pull --ff-only ${remote} ${integrationBranch}`,
|
|
181
|
-
"vibeops task add",
|
|
182
|
-
],
|
|
183
|
-
},
|
|
184
|
-
pulled: false,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
let diagnosis = await diagnoseIntegrationSync(cwd, remote, integrationBranch);
|
|
185
|
+
const diagnosis = await diagnoseIntegrationSync(cwd, remote, integrationBranch);
|
|
189
186
|
if (!diagnosis.ok) {
|
|
190
|
-
return { ok: false, diagnosis, pulled: false };
|
|
187
|
+
return { ok: false, diagnosis, pulled: false, baseRef: null };
|
|
191
188
|
}
|
|
192
|
-
|
|
193
|
-
|
|
189
|
+
const baseRef = await resolveIntegrationBaseRef(cwd, remote, integrationBranch);
|
|
190
|
+
if (baseRef === null) {
|
|
191
|
+
return {
|
|
192
|
+
ok: false,
|
|
193
|
+
diagnosis: {
|
|
194
|
+
ok: false,
|
|
195
|
+
kind: "pull_failed",
|
|
196
|
+
summary: `Integration branch "${integrationBranch}" not found locally or on ${remote}.`,
|
|
197
|
+
fixes: ["vibeops init # or create and push the integration branch"],
|
|
198
|
+
},
|
|
199
|
+
pulled: false,
|
|
200
|
+
baseRef: null,
|
|
201
|
+
};
|
|
194
202
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (localSha !== null && remoteSha !== null && localSha === remoteSha) {
|
|
198
|
-
return { ok: true, diagnosis, pulled: false };
|
|
203
|
+
if (diagnosis.kind === "no_remote" || diagnosis.kind === "no_remote_branch") {
|
|
204
|
+
return { ok: true, diagnosis, pulled: false, baseRef };
|
|
199
205
|
}
|
|
200
|
-
// Stash governance dirt so ff-only pull cannot be blocked by `.vibeops.json` etc.
|
|
201
|
-
const stashed = await stashGovernanceIfBlocking(cwd);
|
|
202
206
|
try {
|
|
203
|
-
await
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
207
|
+
const update = await fastForwardLocalIntegration(cwd, remote, integrationBranch);
|
|
208
|
+
logLocalIntegrationUpdate(update, remote, integrationBranch);
|
|
209
|
+
if (update.kind === "fast_forwarded") {
|
|
210
|
+
return {
|
|
211
|
+
ok: true,
|
|
212
|
+
diagnosis: {
|
|
213
|
+
ok: true,
|
|
214
|
+
kind: "ok",
|
|
215
|
+
summary: `Fast-forwarded ${integrationBranch} to ${remote}/${integrationBranch}.`,
|
|
216
|
+
fixes: [],
|
|
217
|
+
},
|
|
218
|
+
pulled: true,
|
|
219
|
+
baseRef,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return { ok: true, diagnosis, pulled: false, baseRef };
|
|
211
223
|
}
|
|
212
224
|
catch (e) {
|
|
213
225
|
const msg = e instanceof Error ? e.message : String(e);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
diagnosis
|
|
226
|
+
return {
|
|
227
|
+
ok: false,
|
|
228
|
+
diagnosis: {
|
|
217
229
|
ok: false,
|
|
218
230
|
kind: "pull_failed",
|
|
219
|
-
summary: `
|
|
220
|
-
fixes: [
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
return { ok: false, diagnosis, pulled: false };
|
|
228
|
-
}
|
|
229
|
-
finally {
|
|
230
|
-
await restoreGovernanceStashAfterSwitch(cwd, stashed);
|
|
231
|
+
summary: `Could not fast-forward ${integrationBranch}: ${msg}`,
|
|
232
|
+
fixes: [`git merge --ff-only ${remote}/${integrationBranch} # in the worktree that has ${integrationBranch}`],
|
|
233
|
+
},
|
|
234
|
+
pulled: false,
|
|
235
|
+
baseRef: null,
|
|
236
|
+
};
|
|
231
237
|
}
|
|
232
238
|
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { gitBranchExists, gitRemoteBranchExists, gitRemoteUrl, gitRevParse, restoreGovernanceStashAfterSwitch, runGit, stashGovernanceIfBlocking, } from "./git.js";
|
|
4
|
+
import { dim, log } from "./logger.js";
|
|
5
|
+
/** Parse `git worktree list --porcelain` output. */
|
|
6
|
+
export function parseWorktreePorcelain(stdout) {
|
|
7
|
+
const out = [];
|
|
8
|
+
for (const block of stdout.split(/\n\s*\n/)) {
|
|
9
|
+
const lines = block.split("\n").filter((l) => l.length > 0);
|
|
10
|
+
if (lines.length === 0)
|
|
11
|
+
continue;
|
|
12
|
+
let path = null;
|
|
13
|
+
let head = null;
|
|
14
|
+
let branch = null;
|
|
15
|
+
let bare = false;
|
|
16
|
+
let detached = false;
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
if (line.startsWith("worktree "))
|
|
19
|
+
path = line.slice("worktree ".length);
|
|
20
|
+
else if (line.startsWith("HEAD "))
|
|
21
|
+
head = line.slice("HEAD ".length);
|
|
22
|
+
else if (line.startsWith("branch ")) {
|
|
23
|
+
const ref = line.slice("branch ".length);
|
|
24
|
+
branch = ref.startsWith("refs/heads/") ? ref.slice("refs/heads/".length) : ref;
|
|
25
|
+
}
|
|
26
|
+
else if (line === "bare")
|
|
27
|
+
bare = true;
|
|
28
|
+
else if (line === "detached")
|
|
29
|
+
detached = true;
|
|
30
|
+
}
|
|
31
|
+
if (path !== null)
|
|
32
|
+
out.push({ path, head, branch, bare, detached });
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
export async function listWorktrees(cwd) {
|
|
37
|
+
const { stdout } = await runGit(cwd, ["worktree", "list", "--porcelain"]);
|
|
38
|
+
return parseWorktreePorcelain(stdout);
|
|
39
|
+
}
|
|
40
|
+
async function canonical(p) {
|
|
41
|
+
try {
|
|
42
|
+
return await realpath(p);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return resolve(p);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Absolute top-level path of the worktree containing `cwd`. */
|
|
49
|
+
export async function currentWorktreeRoot(cwd) {
|
|
50
|
+
const { stdout } = await runGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
51
|
+
return canonical(stdout.trim());
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Path of another worktree (not the one containing `cwd`) that has `branch`
|
|
55
|
+
* checked out, or null.
|
|
56
|
+
*/
|
|
57
|
+
export async function branchCheckedOutElsewhere(cwd, branch) {
|
|
58
|
+
const here = await currentWorktreeRoot(cwd);
|
|
59
|
+
for (const wt of await listWorktrees(cwd)) {
|
|
60
|
+
if (wt.branch !== branch)
|
|
61
|
+
continue;
|
|
62
|
+
if ((await canonical(wt.path)) === here)
|
|
63
|
+
continue;
|
|
64
|
+
return wt.path;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
/** Branch checked out in the worktree containing `cwd` (null when detached). */
|
|
69
|
+
async function currentBranch(cwd) {
|
|
70
|
+
try {
|
|
71
|
+
const { stdout } = await runGit(cwd, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
|
|
72
|
+
const b = stdout.trim();
|
|
73
|
+
return b.length > 0 ? b : null;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Ref new task branches start from: `<remote>/<integration>` when the
|
|
81
|
+
* remote-tracking ref exists, else the local integration branch (no remote yet).
|
|
82
|
+
* Null when neither exists.
|
|
83
|
+
*/
|
|
84
|
+
export async function resolveIntegrationBaseRef(cwd, remote, integrationBranch) {
|
|
85
|
+
if ((await gitRemoteUrl(cwd, remote)) !== null &&
|
|
86
|
+
(await gitRemoteBranchExists(cwd, remote, integrationBranch))) {
|
|
87
|
+
return `${remote}/${integrationBranch}`;
|
|
88
|
+
}
|
|
89
|
+
if (await gitBranchExists(cwd, integrationBranch))
|
|
90
|
+
return integrationBranch;
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Fast-forward the local integration branch to `<remote>/<integration>` without
|
|
95
|
+
* ever forcing and without touching another worktree:
|
|
96
|
+
* - checked out in this worktree → `git merge --ff-only <remote>/<integration>`;
|
|
97
|
+
* - otherwise → `git fetch . refs/remotes/<remote>/<integration>:refs/heads/<integration>`. git itself
|
|
98
|
+
* refuses a non-fast-forward update and refuses to update a branch checked out
|
|
99
|
+
* in any worktree; those refusals are reported (`not_fast_forward` /
|
|
100
|
+
* `owned_elsewhere`), not treated as failures. No check-then-update race.
|
|
101
|
+
* A missing local branch is not created.
|
|
102
|
+
*/
|
|
103
|
+
export async function fastForwardLocalIntegration(cwd, remote, integrationBranch) {
|
|
104
|
+
const remoteRef = `${remote}/${integrationBranch}`;
|
|
105
|
+
if (!(await gitRemoteBranchExists(cwd, remote, integrationBranch))) {
|
|
106
|
+
return { kind: "no_remote_branch" };
|
|
107
|
+
}
|
|
108
|
+
if (!(await gitBranchExists(cwd, integrationBranch)))
|
|
109
|
+
return { kind: "no_local_branch" };
|
|
110
|
+
if ((await currentBranch(cwd)) === integrationBranch) {
|
|
111
|
+
const before = await gitRevParse(cwd, `refs/heads/${integrationBranch}`);
|
|
112
|
+
const remoteSha = await gitRevParse(cwd, `refs/remotes/${remoteRef}`);
|
|
113
|
+
if (before === remoteSha)
|
|
114
|
+
return { kind: "up_to_date" };
|
|
115
|
+
const stashed = await stashGovernanceIfBlocking(cwd);
|
|
116
|
+
try {
|
|
117
|
+
await runGit(cwd, ["merge", "--ff-only", remoteRef]);
|
|
118
|
+
}
|
|
119
|
+
catch (e) {
|
|
120
|
+
if (/not possible to fast-forward|diverg|non-fast-forward/i.test(errorText(e))) {
|
|
121
|
+
return { kind: "not_fast_forward" };
|
|
122
|
+
}
|
|
123
|
+
throw e;
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
await restoreGovernanceStashAfterSwitch(cwd, stashed);
|
|
127
|
+
}
|
|
128
|
+
return { kind: "fast_forwarded", where: "here" };
|
|
129
|
+
}
|
|
130
|
+
const before = await gitRevParse(cwd, `refs/heads/${integrationBranch}`);
|
|
131
|
+
try {
|
|
132
|
+
// Fetch from this repository (`.`) into the local branch: same git safety as
|
|
133
|
+
// `git fetch <remote> <b>:<b>` (refuses non-fast-forward, refuses a branch
|
|
134
|
+
// checked out in any worktree) using the remote-tracking ref the caller just
|
|
135
|
+
// fetched — no second network round-trip.
|
|
136
|
+
await runGit(cwd, [
|
|
137
|
+
"fetch",
|
|
138
|
+
".",
|
|
139
|
+
`refs/remotes/${remoteRef}:refs/heads/${integrationBranch}`,
|
|
140
|
+
]);
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
const text = errorText(e);
|
|
144
|
+
if (/refusing to fetch into branch|checked out at/i.test(text)) {
|
|
145
|
+
const owner = await branchCheckedOutElsewhere(cwd, integrationBranch);
|
|
146
|
+
return { kind: "owned_elsewhere", worktree: owner ?? "(another worktree)" };
|
|
147
|
+
}
|
|
148
|
+
if (/non-fast-forward|rejected/i.test(text))
|
|
149
|
+
return { kind: "not_fast_forward" };
|
|
150
|
+
throw e;
|
|
151
|
+
}
|
|
152
|
+
const after = await gitRevParse(cwd, `refs/heads/${integrationBranch}`);
|
|
153
|
+
return after === before ? { kind: "up_to_date" } : { kind: "fast_forwarded", where: "ref" };
|
|
154
|
+
}
|
|
155
|
+
function errorText(e) {
|
|
156
|
+
if (e && typeof e === "object") {
|
|
157
|
+
const o = e;
|
|
158
|
+
return `${typeof o.stderr === "string" ? o.stderr : ""}\n${typeof o.message === "string" ? o.message : ""}`;
|
|
159
|
+
}
|
|
160
|
+
return String(e);
|
|
161
|
+
}
|
|
162
|
+
/** One-line log for {@link fastForwardLocalIntegration} results. */
|
|
163
|
+
export function logLocalIntegrationUpdate(r, remote, integrationBranch) {
|
|
164
|
+
switch (r.kind) {
|
|
165
|
+
case "fast_forwarded":
|
|
166
|
+
log.ok(`Fast-forwarded local ${integrationBranch} to ${remote}/${integrationBranch}`);
|
|
167
|
+
return;
|
|
168
|
+
case "owned_elsewhere":
|
|
169
|
+
log.info(dim(`Local ${integrationBranch} is checked out in worktree ${r.worktree} — left unchanged (update it there).`));
|
|
170
|
+
return;
|
|
171
|
+
case "not_fast_forward":
|
|
172
|
+
log.warn(`Local ${integrationBranch} has commits not on ${remote}/${integrationBranch} — left unchanged (never forced).`);
|
|
173
|
+
return;
|
|
174
|
+
default:
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Move HEAD off the task branch so it can be deleted. Switches to the integration
|
|
180
|
+
* branch when no other worktree has it checked out; otherwise detaches at
|
|
181
|
+
* `<remote>/<integration>` (or the local integration ref without a remote).
|
|
182
|
+
*/
|
|
183
|
+
export async function leaveTaskBranch(cwd, remote, integrationBranch) {
|
|
184
|
+
const owner = await branchCheckedOutElsewhere(cwd, integrationBranch);
|
|
185
|
+
const base = await resolveIntegrationBaseRef(cwd, remote, integrationBranch);
|
|
186
|
+
if (base === null) {
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
message: `Integration branch "${integrationBranch}" not found locally or on ${remote}.`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const stashed = await stashGovernanceIfBlocking(cwd);
|
|
193
|
+
try {
|
|
194
|
+
if (owner === null) {
|
|
195
|
+
if (await gitBranchExists(cwd, integrationBranch)) {
|
|
196
|
+
await runGit(cwd, ["switch", integrationBranch]);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
await runGit(cwd, ["switch", "-c", integrationBranch, "--track", base]);
|
|
200
|
+
}
|
|
201
|
+
return { ok: true, on: "integration", ref: integrationBranch };
|
|
202
|
+
}
|
|
203
|
+
await runGit(cwd, ["switch", "--detach", base]);
|
|
204
|
+
log.info(dim(`${integrationBranch} is checked out in worktree ${owner} — detached this worktree at ${base}.`));
|
|
205
|
+
return { ok: true, on: "detached", ref: base };
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
209
|
+
return { ok: false, message: `Could not leave the task branch: ${msg}` };
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
await restoreGovernanceStashAfterSwitch(cwd, stashed);
|
|
213
|
+
}
|
|
214
|
+
}
|