@imdeadpool/guardex 7.0.41 → 7.1.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 +94 -13
- package/package.json +3 -1
- package/skills/gitguardex/SKILL.md +13 -0
- package/skills/guardex-merge-skills-to-dev/SKILL.md +59 -0
- package/skills/gx-act/SKILL.md +82 -0
- package/src/agents/cleanup-sessions.js +126 -0
- package/src/agents/finish.js +172 -0
- package/src/agents/inspect.js +202 -0
- package/src/agents/launch.js +249 -0
- package/src/agents/registry.js +133 -0
- package/src/agents/selection-panel.js +571 -0
- package/src/agents/sessions.js +151 -0
- package/src/agents/start.js +591 -0
- package/src/agents/status.js +146 -0
- package/src/agents/terminal.js +152 -0
- package/src/budget/index.js +344 -0
- package/src/ci-init/index.js +265 -0
- package/src/cli/args.js +357 -3
- package/src/cli/commands/agents.js +364 -0
- package/src/cli/commands/bootstrap.js +92 -0
- package/src/cli/commands/branch.js +127 -0
- package/src/cli/commands/claude.js +674 -0
- package/src/cli/commands/doctor.js +268 -0
- package/src/cli/commands/finish.js +26 -0
- package/src/cli/commands/mcp.js +122 -0
- package/src/cli/commands/misc.js +304 -0
- package/src/cli/commands/pr.js +439 -0
- package/src/cli/commands/prompt.js +92 -0
- package/src/cli/commands/release.js +305 -0
- package/src/cli/commands/report.js +244 -0
- package/src/cli/commands/review.js +32 -0
- package/src/cli/commands/setup.js +242 -0
- package/src/cli/commands/status.js +338 -0
- package/src/cli/commands/watch.js +234 -0
- package/src/cli/main.js +85 -3613
- package/src/cli/shared/repo-env.js +161 -0
- package/src/cli/shared/sandbox.js +417 -0
- package/src/cli/shared/scaffolding.js +535 -0
- package/src/cli/shared/toolchain-shims.js +420 -0
- package/src/cockpit/action-runner.js +3 -0
- package/src/cockpit/actions.js +80 -0
- package/src/cockpit/control.js +1121 -0
- package/src/cockpit/index.js +426 -0
- package/src/cockpit/kitty-layout.js +549 -0
- package/src/cockpit/kitty-tree.js +144 -0
- package/src/cockpit/logs-reader.js +182 -0
- package/src/cockpit/menu.js +204 -0
- package/src/cockpit/pane-actions.js +597 -0
- package/src/cockpit/pane-menu.js +387 -0
- package/src/cockpit/projects-finder.js +178 -0
- package/src/cockpit/render.js +215 -0
- package/src/cockpit/settings-render.js +128 -0
- package/src/cockpit/settings.js +124 -0
- package/src/cockpit/shortcuts.js +24 -0
- package/src/cockpit/sidebar.js +311 -0
- package/src/cockpit/state.js +72 -0
- package/src/cockpit/theme.js +128 -0
- package/src/cockpit/welcome.js +266 -0
- package/src/context.js +304 -43
- package/src/core/runtime.js +6 -1
- package/src/doctor/index.js +45 -15
- package/src/finish/index.js +186 -7
- package/src/finish/preflight.js +177 -0
- package/src/finish/review-gate.js +182 -0
- package/src/git/index.js +511 -4
- package/src/hooks/index.js +0 -64
- package/src/kitty/command.js +101 -0
- package/src/kitty/runtime.js +250 -0
- package/src/mcp/collect.js +370 -0
- package/src/mcp/server.js +157 -0
- package/src/output/index.js +68 -2
- package/src/pr-review.js +264 -0
- package/src/pr.js +381 -0
- package/src/sandbox/index.js +13 -2
- package/src/scaffold/agent-worktree-prep.js +213 -0
- package/src/scaffold/index.js +127 -10
- package/src/speckit/index.js +226 -0
- package/src/submodule/index.js +288 -0
- package/src/terminal/index.js +45 -0
- package/src/terminal/kitty.js +622 -0
- package/src/terminal/tmux.js +125 -0
- package/src/tmux/command.js +27 -0
- package/src/tmux/session.js +89 -0
- package/src/toolchain/index.js +20 -0
- package/templates/AGENTS.monorepo-apps.md +26 -0
- package/templates/AGENTS.multiagent-safety.md +63 -323
- package/templates/AGENTS.multiagent-safety.min.md +11 -0
- package/templates/codex/skills/gitguardex/SKILL.md +2 -0
- package/templates/codex/skills/gx-act/SKILL.md +82 -0
- package/templates/githooks/pre-commit +44 -20
- package/templates/github/workflows/README.md +87 -0
- package/templates/github/workflows/ci-full.yml +55 -0
- package/templates/github/workflows/ci.yml +56 -0
- package/templates/github/workflows/cr.yml +20 -1
- package/templates/scripts/agent-branch-finish.sh +519 -23
- package/templates/scripts/agent-branch-merge.sh +4 -1
- package/templates/scripts/agent-branch-start.sh +176 -24
- package/templates/scripts/agent-preflight.sh +115 -0
- package/templates/scripts/agent-worktree-prune.sh +96 -5
- package/templates/scripts/codex-agent.sh +41 -97
- package/templates/scripts/openspec/init-plan-workspace.sh +43 -0
- package/templates/scripts/review-bot-watch.sh +31 -2
- package/templates/scripts/agent-session-state.js +0 -171
- package/templates/scripts/install-vscode-active-agents-extension.js +0 -135
- package/templates/vscode/guardex-active-agents/README.md +0 -34
- package/templates/vscode/guardex-active-agents/extension.js +0 -3782
- package/templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json +0 -54
- package/templates/vscode/guardex-active-agents/fileicons/icons/agent.svg +0 -5
- package/templates/vscode/guardex-active-agents/fileicons/icons/branch.svg +0 -7
- package/templates/vscode/guardex-active-agents/fileicons/icons/config.svg +0 -4
- package/templates/vscode/guardex-active-agents/fileicons/icons/hook.svg +0 -4
- package/templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg +0 -5
- package/templates/vscode/guardex-active-agents/fileicons/icons/plan.svg +0 -4
- package/templates/vscode/guardex-active-agents/fileicons/icons/spec.svg +0 -5
- package/templates/vscode/guardex-active-agents/icon.png +0 -0
- package/templates/vscode/guardex-active-agents/media/active-agents-hivemind.svg +0 -14
- package/templates/vscode/guardex-active-agents/package.json +0 -169
- package/templates/vscode/guardex-active-agents/session-schema.js +0 -1348
package/src/git/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
const fs = require('node:fs');
|
|
2
3
|
const {
|
|
3
4
|
path,
|
|
5
|
+
cachedSpawn,
|
|
4
6
|
TOOL_NAME,
|
|
5
7
|
GIT_PROTECTED_BRANCHES_KEY,
|
|
6
8
|
GIT_BASE_BRANCH_KEY,
|
|
@@ -11,8 +13,109 @@ const {
|
|
|
11
13
|
COMPOSE_HINT_FILES,
|
|
12
14
|
LOCK_FILE_RELATIVE,
|
|
13
15
|
} = require('../context');
|
|
14
|
-
const { run } = require('../core/runtime');
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Result of a synchronous child-process spawn.
|
|
19
|
+
*
|
|
20
|
+
* Mirrors the relevant subset of Node's `child_process.SpawnSyncReturns`
|
|
21
|
+
* without pulling in `@types/node`, so this typedef stays usable under
|
|
22
|
+
* `@ts-check` even when type definitions are not installed.
|
|
23
|
+
*
|
|
24
|
+
* @typedef {Object} SpawnResult
|
|
25
|
+
* @property {number|null} status Exit code, or `null` if the process was killed by a signal.
|
|
26
|
+
* @property {string} [stdout] Captured stdout (utf-8).
|
|
27
|
+
* @property {string} [stderr] Captured stderr (utf-8).
|
|
28
|
+
* @property {Error} [error] Spawn-level error (e.g., ENOENT, timeout).
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Options accepted by {@link run}.
|
|
33
|
+
*
|
|
34
|
+
* @typedef {Object} RunOptions
|
|
35
|
+
* @property {string} [stdio] Stdio mode passed to spawnSync ('pipe' by default).
|
|
36
|
+
* @property {string} [cwd] Working directory for the child process.
|
|
37
|
+
* @property {Record<string, string|undefined>} [env] Extra env vars merged on top of `process.env`.
|
|
38
|
+
* @property {number} [timeout] Kill the child after this many milliseconds.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Outcome of an additive setup operation reported by setup-style helpers.
|
|
43
|
+
*
|
|
44
|
+
* @typedef {Object} SetupOperation
|
|
45
|
+
* @property {string} status Operation outcome (e.g., 'unchanged', 'updated', 'set', 'would-set', 'synced', 'failed').
|
|
46
|
+
* @property {string} file Human-readable identifier for the touched resource (often `git config <key>`).
|
|
47
|
+
* @property {string} [note] Optional explanation surfaced in setup output.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Porcelain status of the agent file-lock registry inside the working tree.
|
|
52
|
+
*
|
|
53
|
+
* @typedef {Object} LockRegistryStatus
|
|
54
|
+
* @property {boolean} dirty Lock file has uncommitted or untracked changes.
|
|
55
|
+
* @property {boolean} untracked Lock file is present but not tracked by git.
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Ahead/behind counts between a branch and a base ref.
|
|
60
|
+
*
|
|
61
|
+
* @typedef {Object} AheadBehindCounts
|
|
62
|
+
* @property {number} ahead Commits on `branchRef` not on `baseRef`.
|
|
63
|
+
* @property {number} behind Commits on `baseRef` not on `branchRef`.
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Outcome of attempting to switch the primary checkout onto a branch.
|
|
68
|
+
*
|
|
69
|
+
* @typedef {Object} EnsureRepoBranchResult
|
|
70
|
+
* @property {boolean} ok True when the working tree is on (or moved to) the requested branch.
|
|
71
|
+
* @property {boolean} changed True only when this call performed a checkout.
|
|
72
|
+
* @property {string} [stdout] Checkout stdout when the operation failed.
|
|
73
|
+
* @property {string} [stderr] Checkout stderr when the operation failed.
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Worktree entry describing an `agent/*` branch checked out on disk.
|
|
78
|
+
*
|
|
79
|
+
* @typedef {Object} AgentWorktreeEntry
|
|
80
|
+
* @property {string} worktreePath Absolute filesystem path of the worktree.
|
|
81
|
+
* @property {string} branch Branch name (without the `refs/heads/` prefix).
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Spawn `cmd` synchronously and capture its output as utf-8 text.
|
|
86
|
+
*
|
|
87
|
+
* Routes every call through the process-scoped probe cache in `context.js`
|
|
88
|
+
* so repeated read-only probes (e.g., `git rev-parse`, `git config --get`)
|
|
89
|
+
* answer from cache. `cachedSpawn` falls through to `cp.spawnSync` for any
|
|
90
|
+
* command not on its read-only allowlist (writes like `git commit`,
|
|
91
|
+
* `git push`, `git checkout`, `git worktree add/remove`), so this is a
|
|
92
|
+
* strict perf optimization preserving the `(cmd, args, opts) -> spawnSync
|
|
93
|
+
* result` signature.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} cmd Executable to invoke.
|
|
96
|
+
* @param {ReadonlyArray<string>} args Argument vector.
|
|
97
|
+
* @param {RunOptions} [options] Spawn options (see {@link RunOptions}).
|
|
98
|
+
* @returns {SpawnResult} Result of the spawn.
|
|
99
|
+
*/
|
|
100
|
+
function run(cmd, args, options = {}) {
|
|
101
|
+
return cachedSpawn(cmd, args, {
|
|
102
|
+
encoding: 'utf8',
|
|
103
|
+
stdio: options.stdio || 'pipe',
|
|
104
|
+
cwd: options.cwd,
|
|
105
|
+
env: options.env ? { ...process.env, ...options.env } : process.env,
|
|
106
|
+
timeout: options.timeout,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Run `git -C <repoRoot> <args>` and throw unless `allowFailure` is set.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} repoRoot Repository root the command should target.
|
|
114
|
+
* @param {ReadonlyArray<string>} args Argument vector passed after `-C <repoRoot>`.
|
|
115
|
+
* @param {{ allowFailure?: boolean }} [opts] When `allowFailure` is true, non-zero exits are returned to the caller instead of throwing.
|
|
116
|
+
* @returns {SpawnResult} The spawn result (always returned; only thrown on failure when `allowFailure` is false).
|
|
117
|
+
* @throws {Error} When git exits non-zero and `allowFailure` is not set.
|
|
118
|
+
*/
|
|
16
119
|
function gitRun(repoRoot, args, { allowFailure = false } = {}) {
|
|
17
120
|
const result = run('git', ['-C', repoRoot, ...args]);
|
|
18
121
|
if (!allowFailure && result.status !== 0) {
|
|
@@ -21,6 +124,13 @@ function gitRun(repoRoot, args, { allowFailure = false } = {}) {
|
|
|
21
124
|
return result;
|
|
22
125
|
}
|
|
23
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Resolve the absolute git toplevel for `targetPath` (or `process.cwd()`).
|
|
129
|
+
*
|
|
130
|
+
* @param {string} [targetPath] Filesystem path inside a git repo. Defaults to the current working directory.
|
|
131
|
+
* @returns {string} Absolute path to the git toplevel.
|
|
132
|
+
* @throws {Error} When `targetPath` is not inside a git repository.
|
|
133
|
+
*/
|
|
24
134
|
function resolveRepoRoot(targetPath) {
|
|
25
135
|
const resolvedTarget = path.resolve(targetPath || process.cwd());
|
|
26
136
|
const result = run('git', ['-C', resolvedTarget, 'rev-parse', '--show-toplevel']);
|
|
@@ -33,6 +143,12 @@ function resolveRepoRoot(targetPath) {
|
|
|
33
143
|
return result.stdout.trim();
|
|
34
144
|
}
|
|
35
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Test whether `targetPath` (or cwd) resolves inside a git repository.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} [targetPath] Filesystem path to probe. Defaults to the current working directory.
|
|
150
|
+
* @returns {boolean} True if the path is inside a git repo.
|
|
151
|
+
*/
|
|
36
152
|
function isGitRepo(targetPath) {
|
|
37
153
|
const resolvedTarget = path.resolve(targetPath || process.cwd());
|
|
38
154
|
const result = run('git', ['-C', resolvedTarget, 'rev-parse', '--show-toplevel']);
|
|
@@ -61,6 +177,21 @@ function resolveGitCommonDir(repoPath) {
|
|
|
61
177
|
return path.resolve(repoPath, raw);
|
|
62
178
|
}
|
|
63
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Walk `rootPath` and return every distinct git working tree found within it.
|
|
182
|
+
*
|
|
183
|
+
* Skips common heavy/disposable directories (`node_modules`, `dist`, etc.),
|
|
184
|
+
* excludes the root's own `.git`, and by default treats submodules
|
|
185
|
+
* (where `.git` is a file, not a directory) as part of the root unless
|
|
186
|
+
* `includeSubmodules` is set. Filters out worktree children that share the
|
|
187
|
+
* root's `git-common-dir` so additional worktrees of the same repo do not
|
|
188
|
+
* show up as nested repos.
|
|
189
|
+
*
|
|
190
|
+
* @param {string} rootPath Path to search.
|
|
191
|
+
* @param {{ maxDepth?: number, extraSkip?: ReadonlyArray<string>, includeSubmodules?: boolean, skipRelativeDirs?: ReadonlyArray<string> }} [opts] Walk tuning knobs.
|
|
192
|
+
* @returns {string[]} Sorted list of repo paths, with `rootPath` first.
|
|
193
|
+
* @throws {Error} When `rootPath` is not itself a git repository.
|
|
194
|
+
*/
|
|
64
195
|
function discoverNestedGitRepos(rootPath, opts = {}) {
|
|
65
196
|
const maxDepth = Number.isFinite(opts.maxDepth)
|
|
66
197
|
? Math.max(1, opts.maxDepth)
|
|
@@ -124,6 +255,12 @@ function discoverNestedGitRepos(rootPath, opts = {}) {
|
|
|
124
255
|
return root ? [root, ...rest] : [];
|
|
125
256
|
}
|
|
126
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Split a whitespace/comma-delimited branch list into a deduped array.
|
|
260
|
+
*
|
|
261
|
+
* @param {unknown} rawValue Raw config value (string-coerced; non-strings yield `[]`).
|
|
262
|
+
* @returns {string[]} Branch names with empty entries removed.
|
|
263
|
+
*/
|
|
127
264
|
function parseBranchList(rawValue) {
|
|
128
265
|
return String(rawValue || '')
|
|
129
266
|
.split(/[\s,]+/)
|
|
@@ -131,8 +268,17 @@ function parseBranchList(rawValue) {
|
|
|
131
268
|
.filter(Boolean);
|
|
132
269
|
}
|
|
133
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Return `items` with duplicates removed, preserving first-seen order.
|
|
273
|
+
*
|
|
274
|
+
* @template T
|
|
275
|
+
* @param {ReadonlyArray<T>} items Input list.
|
|
276
|
+
* @returns {T[]} Deduped list.
|
|
277
|
+
*/
|
|
134
278
|
function uniquePreserveOrder(items) {
|
|
279
|
+
/** @type {Set<T>} */
|
|
135
280
|
const seen = new Set();
|
|
281
|
+
/** @type {T[]} */
|
|
136
282
|
const result = [];
|
|
137
283
|
for (const item of items) {
|
|
138
284
|
if (seen.has(item)) continue;
|
|
@@ -142,6 +288,12 @@ function uniquePreserveOrder(items) {
|
|
|
142
288
|
return result;
|
|
143
289
|
}
|
|
144
290
|
|
|
291
|
+
/**
|
|
292
|
+
* Read the `GIT_PROTECTED_BRANCHES_KEY` config value as a deduped list.
|
|
293
|
+
*
|
|
294
|
+
* @param {string} repoRoot Repo to inspect.
|
|
295
|
+
* @returns {string[]|null} Parsed branches, or `null` when the key is unset / empty.
|
|
296
|
+
*/
|
|
145
297
|
function readConfiguredProtectedBranches(repoRoot) {
|
|
146
298
|
const result = gitRun(repoRoot, ['config', '--get', GIT_PROTECTED_BRANCHES_KEY], { allowFailure: true });
|
|
147
299
|
if (result.status !== 0) {
|
|
@@ -154,6 +306,14 @@ function readConfiguredProtectedBranches(repoRoot) {
|
|
|
154
306
|
return parsed;
|
|
155
307
|
}
|
|
156
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Enumerate local branches that look like user branches (not `agent/*`, not
|
|
311
|
+
* in `DEFAULT_PROTECTED_BRANCHES`). Falls back to the currently checked-out
|
|
312
|
+
* branch when no other user branches exist.
|
|
313
|
+
*
|
|
314
|
+
* @param {string} repoRoot Repo to inspect.
|
|
315
|
+
* @returns {string[]} User branch names.
|
|
316
|
+
*/
|
|
157
317
|
function listLocalUserBranches(repoRoot) {
|
|
158
318
|
const result = gitRun(repoRoot, ['for-each-ref', '--format=%(refname:short)', 'refs/heads'], { allowFailure: true });
|
|
159
319
|
const branchNames = result.status === 0
|
|
@@ -191,6 +351,12 @@ function listLocalUserBranches(repoRoot) {
|
|
|
191
351
|
return [branchName];
|
|
192
352
|
}
|
|
193
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Enumerate local branches under `refs/heads/agent/`.
|
|
356
|
+
*
|
|
357
|
+
* @param {string} repoRoot Repo to inspect.
|
|
358
|
+
* @returns {string[]} Agent branch names (deduped, in for-each-ref order).
|
|
359
|
+
*/
|
|
194
360
|
function listLocalAgentBranches(repoRoot) {
|
|
195
361
|
const result = gitRun(
|
|
196
362
|
repoRoot,
|
|
@@ -208,6 +374,12 @@ function listLocalAgentBranches(repoRoot) {
|
|
|
208
374
|
);
|
|
209
375
|
}
|
|
210
376
|
|
|
377
|
+
/**
|
|
378
|
+
* Build a `branch -> worktreePath` map from `git worktree list --porcelain`.
|
|
379
|
+
*
|
|
380
|
+
* @param {string} repoRoot Repo to inspect.
|
|
381
|
+
* @returns {Map<string, string>} Branch -> absolute worktree path; empty on failure.
|
|
382
|
+
*/
|
|
211
383
|
function mapWorktreePathsByBranch(repoRoot) {
|
|
212
384
|
const result = gitRun(repoRoot, ['worktree', 'list', '--porcelain'], { allowFailure: true });
|
|
213
385
|
const map = new Map();
|
|
@@ -232,10 +404,24 @@ function mapWorktreePathsByBranch(repoRoot) {
|
|
|
232
404
|
return map;
|
|
233
405
|
}
|
|
234
406
|
|
|
407
|
+
/**
|
|
408
|
+
* Test whether `ref` resolves via `git show-ref --verify`.
|
|
409
|
+
*
|
|
410
|
+
* @param {string} repoRoot Repo to inspect.
|
|
411
|
+
* @param {string} ref Fully-qualified ref (e.g., `refs/heads/main`).
|
|
412
|
+
* @returns {boolean} True when the ref exists.
|
|
413
|
+
*/
|
|
235
414
|
function gitRefExists(repoRoot, ref) {
|
|
236
415
|
return run('git', ['-C', repoRoot, 'show-ref', '--verify', '--quiet', ref]).status === 0;
|
|
237
416
|
}
|
|
238
417
|
|
|
418
|
+
/**
|
|
419
|
+
* Report whether `worktreePath` has working-tree changes the finish flow
|
|
420
|
+
* would consider significant, ignoring noise from the lock-registry file.
|
|
421
|
+
*
|
|
422
|
+
* @param {string} worktreePath Worktree to inspect.
|
|
423
|
+
* @returns {boolean} True when meaningful changes are present.
|
|
424
|
+
*/
|
|
239
425
|
function hasSignificantWorkingTreeChanges(worktreePath) {
|
|
240
426
|
const result = run('git', [
|
|
241
427
|
'-C',
|
|
@@ -265,6 +451,12 @@ function hasSignificantWorkingTreeChanges(worktreePath) {
|
|
|
265
451
|
return false;
|
|
266
452
|
}
|
|
267
453
|
|
|
454
|
+
/**
|
|
455
|
+
* Resolve the effective protected-branches list (config override or default).
|
|
456
|
+
*
|
|
457
|
+
* @param {string} repoRoot Repo to inspect.
|
|
458
|
+
* @returns {string[]} Protected branch names.
|
|
459
|
+
*/
|
|
268
460
|
function readProtectedBranches(repoRoot) {
|
|
269
461
|
const result = gitRun(repoRoot, ['config', '--get', GIT_PROTECTED_BRANCHES_KEY], { allowFailure: true });
|
|
270
462
|
if (result.status !== 0) {
|
|
@@ -278,6 +470,14 @@ function readProtectedBranches(repoRoot) {
|
|
|
278
470
|
return parsed;
|
|
279
471
|
}
|
|
280
472
|
|
|
473
|
+
/**
|
|
474
|
+
* Add any local user branches that are missing from the protected-branches
|
|
475
|
+
* config. In dry-run mode the config is left untouched.
|
|
476
|
+
*
|
|
477
|
+
* @param {string} repoRoot Repo to inspect.
|
|
478
|
+
* @param {boolean} dryRun When true, report intended changes without writing.
|
|
479
|
+
* @returns {SetupOperation} Operation outcome (`unchanged`, `would-update`, or `updated`).
|
|
480
|
+
*/
|
|
281
481
|
function ensureSetupProtectedBranches(repoRoot, dryRun) {
|
|
282
482
|
const localUserBranches = listLocalUserBranches(repoRoot);
|
|
283
483
|
if (localUserBranches.length === 0) {
|
|
@@ -311,6 +511,14 @@ function ensureSetupProtectedBranches(repoRoot, dryRun) {
|
|
|
311
511
|
};
|
|
312
512
|
}
|
|
313
513
|
|
|
514
|
+
/**
|
|
515
|
+
* Persist the protected-branches list into git config, replacing the prior
|
|
516
|
+
* value. An empty array unsets the key entirely.
|
|
517
|
+
*
|
|
518
|
+
* @param {string} repoRoot Repo to update.
|
|
519
|
+
* @param {ReadonlyArray<string>} branches Branch names to record.
|
|
520
|
+
* @returns {void}
|
|
521
|
+
*/
|
|
314
522
|
function writeProtectedBranches(repoRoot, branches) {
|
|
315
523
|
if (branches.length === 0) {
|
|
316
524
|
gitRun(repoRoot, ['config', '--unset-all', GIT_PROTECTED_BRANCHES_KEY], { allowFailure: true });
|
|
@@ -319,6 +527,86 @@ function writeProtectedBranches(repoRoot, branches) {
|
|
|
319
527
|
gitRun(repoRoot, ['config', GIT_PROTECTED_BRANCHES_KEY, branches.join(' ')]);
|
|
320
528
|
}
|
|
321
529
|
|
|
530
|
+
const SUBMODULE_AUTO_SYNC_CONFIGS = [
|
|
531
|
+
{
|
|
532
|
+
key: 'pull.recurseSubmodules',
|
|
533
|
+
value: 'true',
|
|
534
|
+
note: 'auto-update submodule working dirs on `git pull`',
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
key: 'fetch.recurseSubmodules',
|
|
538
|
+
value: 'on-demand',
|
|
539
|
+
note: 'fetch submodule commits as parent pointers move',
|
|
540
|
+
},
|
|
541
|
+
];
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Wire git config so submodules auto-update on pull/fetch and snap working
|
|
545
|
+
* dirs to the parent index. No-op (returns `[]`) when the repo has no
|
|
546
|
+
* `.gitmodules` file. Existing config values are respected, not overwritten.
|
|
547
|
+
*
|
|
548
|
+
* @param {string} repoRoot Repo to configure.
|
|
549
|
+
* @param {boolean} dryRun When true, report intended changes without writing.
|
|
550
|
+
* @returns {SetupOperation[]} One operation per config key plus the recursive submodule sync result.
|
|
551
|
+
*/
|
|
552
|
+
function ensureSubmoduleAutoSync(repoRoot, dryRun) {
|
|
553
|
+
const gitmodulesPath = path.join(repoRoot, '.gitmodules');
|
|
554
|
+
if (!fs.existsSync(gitmodulesPath)) {
|
|
555
|
+
return [];
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const operations = [];
|
|
559
|
+
for (const { key, value, note } of SUBMODULE_AUTO_SYNC_CONFIGS) {
|
|
560
|
+
const existing = readGitConfig(repoRoot, key);
|
|
561
|
+
if (existing) {
|
|
562
|
+
operations.push({
|
|
563
|
+
status: 'unchanged',
|
|
564
|
+
file: `git config ${key}`,
|
|
565
|
+
note: `respected pre-existing value: ${existing}`,
|
|
566
|
+
});
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (!dryRun) {
|
|
570
|
+
gitRun(repoRoot, ['config', key, value]);
|
|
571
|
+
}
|
|
572
|
+
operations.push({
|
|
573
|
+
status: dryRun ? 'would-set' : 'set',
|
|
574
|
+
file: `git config ${key}`,
|
|
575
|
+
note: `${value} (${note})`,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (dryRun) {
|
|
580
|
+
operations.push({
|
|
581
|
+
status: 'would-sync',
|
|
582
|
+
file: 'git submodule update --init --recursive',
|
|
583
|
+
note: 'snap submodule working dirs to parent index',
|
|
584
|
+
});
|
|
585
|
+
return operations;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const result = gitRun(
|
|
589
|
+
repoRoot,
|
|
590
|
+
['submodule', 'update', '--init', '--recursive'],
|
|
591
|
+
{ allowFailure: true },
|
|
592
|
+
);
|
|
593
|
+
operations.push({
|
|
594
|
+
status: result.status === 0 ? 'synced' : 'failed',
|
|
595
|
+
file: 'git submodule update --init --recursive',
|
|
596
|
+
note: result.status === 0
|
|
597
|
+
? 'submodule working dirs snapped to parent index'
|
|
598
|
+
: `failed: ${(result.stderr || '').trim().split('\n')[0] || 'unknown'}`,
|
|
599
|
+
});
|
|
600
|
+
return operations;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Read a single git config value as a trimmed string.
|
|
605
|
+
*
|
|
606
|
+
* @param {string} repoRoot Repo to inspect.
|
|
607
|
+
* @param {string} key Config key to read.
|
|
608
|
+
* @returns {string} Trimmed value, or '' when the key is unset.
|
|
609
|
+
*/
|
|
322
610
|
function readGitConfig(repoRoot, key) {
|
|
323
611
|
const result = gitRun(repoRoot, ['config', '--get', key], { allowFailure: true });
|
|
324
612
|
if (result.status !== 0) {
|
|
@@ -327,14 +615,65 @@ function readGitConfig(repoRoot, key) {
|
|
|
327
615
|
return (result.stdout || '').trim();
|
|
328
616
|
}
|
|
329
617
|
|
|
618
|
+
/**
|
|
619
|
+
* Detect the repository's real default branch when nothing is configured.
|
|
620
|
+
*
|
|
621
|
+
* Prefers the symbolic `origin/HEAD` (what the remote calls its default),
|
|
622
|
+
* then the first existing branch among `main`, `master`, `dev` (local or
|
|
623
|
+
* on origin), and only then falls back to the hardcoded `DEFAULT_BASE_BRANCH`.
|
|
624
|
+
* This stops the finish/PR flow from targeting a non-existent `dev` on repos
|
|
625
|
+
* whose base is actually `main`.
|
|
626
|
+
*
|
|
627
|
+
* @param {string} repoRoot Repo to inspect.
|
|
628
|
+
* @returns {string} Detected default base branch name.
|
|
629
|
+
*/
|
|
630
|
+
function detectDefaultBaseBranch(repoRoot) {
|
|
631
|
+
const symbolic = gitRun(repoRoot, ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD'], {
|
|
632
|
+
allowFailure: true,
|
|
633
|
+
});
|
|
634
|
+
if (symbolic.status === 0) {
|
|
635
|
+
const ref = String(symbolic.stdout || '').trim().replace(/^origin\//, '');
|
|
636
|
+
if (ref) {
|
|
637
|
+
return ref;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
for (const candidate of ['main', 'master', 'dev']) {
|
|
642
|
+
if (
|
|
643
|
+
gitRefExists(repoRoot, `refs/heads/${candidate}`) ||
|
|
644
|
+
gitRefExists(repoRoot, `refs/remotes/origin/${candidate}`)
|
|
645
|
+
) {
|
|
646
|
+
return candidate;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return DEFAULT_BASE_BRANCH;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Resolve the base branch to use (explicit CLI value wins; otherwise config
|
|
655
|
+
* key `GIT_BASE_BRANCH_KEY`; otherwise the repo's detected default branch).
|
|
656
|
+
*
|
|
657
|
+
* @param {string} repoRoot Repo to inspect.
|
|
658
|
+
* @param {string} [explicitBase] Value passed on the CLI, if any.
|
|
659
|
+
* @returns {string} Resolved base branch name.
|
|
660
|
+
*/
|
|
330
661
|
function resolveBaseBranch(repoRoot, explicitBase) {
|
|
331
662
|
if (explicitBase) {
|
|
332
663
|
return explicitBase;
|
|
333
664
|
}
|
|
334
665
|
const configured = readGitConfig(repoRoot, GIT_BASE_BRANCH_KEY);
|
|
335
|
-
return configured ||
|
|
666
|
+
return configured || detectDefaultBaseBranch(repoRoot);
|
|
336
667
|
}
|
|
337
668
|
|
|
669
|
+
/**
|
|
670
|
+
* Resolve the sync strategy to use, validated against the allowed set.
|
|
671
|
+
*
|
|
672
|
+
* @param {string} repoRoot Repo to inspect.
|
|
673
|
+
* @param {string} [explicitStrategy] Value passed on the CLI, if any.
|
|
674
|
+
* @returns {'rebase'|'merge'} Resolved strategy (lower-cased).
|
|
675
|
+
* @throws {Error} When the resolved value is not `rebase` or `merge`.
|
|
676
|
+
*/
|
|
338
677
|
function resolveSyncStrategy(repoRoot, explicitStrategy) {
|
|
339
678
|
const strategy = (explicitStrategy || readGitConfig(repoRoot, GIT_SYNC_STRATEGY_KEY) || DEFAULT_SYNC_STRATEGY)
|
|
340
679
|
.trim()
|
|
@@ -345,6 +684,13 @@ function resolveSyncStrategy(repoRoot, explicitStrategy) {
|
|
|
345
684
|
return strategy;
|
|
346
685
|
}
|
|
347
686
|
|
|
687
|
+
/**
|
|
688
|
+
* Return the currently checked-out branch name.
|
|
689
|
+
*
|
|
690
|
+
* @param {string} repoRoot Repo to inspect.
|
|
691
|
+
* @returns {string} Current branch name.
|
|
692
|
+
* @throws {Error} When `git branch --show-current` fails or HEAD is detached.
|
|
693
|
+
*/
|
|
348
694
|
function currentBranchName(repoRoot) {
|
|
349
695
|
const result = gitRun(repoRoot, ['branch', '--show-current'], { allowFailure: true });
|
|
350
696
|
if (result.status !== 0) {
|
|
@@ -357,10 +703,23 @@ function currentBranchName(repoRoot) {
|
|
|
357
703
|
return branch;
|
|
358
704
|
}
|
|
359
705
|
|
|
706
|
+
/**
|
|
707
|
+
* Test whether `HEAD` resolves to a commit (i.e., repo is not unborn).
|
|
708
|
+
*
|
|
709
|
+
* @param {string} repoRoot Repo to inspect.
|
|
710
|
+
* @returns {boolean} True when HEAD has a commit.
|
|
711
|
+
*/
|
|
360
712
|
function repoHasHeadCommit(repoRoot) {
|
|
361
713
|
return gitRun(repoRoot, ['rev-parse', '--verify', 'HEAD'], { allowFailure: true }).status === 0;
|
|
362
714
|
}
|
|
363
715
|
|
|
716
|
+
/**
|
|
717
|
+
* Produce a human-readable description of HEAD: branch name, branch name
|
|
718
|
+
* annotated as unborn, a detached short SHA, or `(unknown)`.
|
|
719
|
+
*
|
|
720
|
+
* @param {string} repoRoot Repo to inspect.
|
|
721
|
+
* @returns {string} Display label for the current HEAD.
|
|
722
|
+
*/
|
|
364
723
|
function readBranchDisplayName(repoRoot) {
|
|
365
724
|
const symbolic = gitRun(repoRoot, ['symbolic-ref', '--quiet', '--short', 'HEAD'], { allowFailure: true });
|
|
366
725
|
if (symbolic.status === 0) {
|
|
@@ -378,14 +737,35 @@ function readBranchDisplayName(repoRoot) {
|
|
|
378
737
|
return '(unknown)';
|
|
379
738
|
}
|
|
380
739
|
|
|
740
|
+
/**
|
|
741
|
+
* Test whether the repo has an `origin` remote configured.
|
|
742
|
+
*
|
|
743
|
+
* @param {string} repoRoot Repo to inspect.
|
|
744
|
+
* @returns {boolean} True when `git remote get-url origin` succeeds.
|
|
745
|
+
*/
|
|
381
746
|
function hasOriginRemote(repoRoot) {
|
|
382
747
|
return gitRun(repoRoot, ['remote', 'get-url', 'origin'], { allowFailure: true }).status === 0;
|
|
383
748
|
}
|
|
384
749
|
|
|
750
|
+
/**
|
|
751
|
+
* Return the subset of `COMPOSE_HINT_FILES` that exist under `repoRoot`.
|
|
752
|
+
*
|
|
753
|
+
* @param {string} repoRoot Repo to inspect.
|
|
754
|
+
* @returns {string[]} Relative paths of compose-hint files present on disk.
|
|
755
|
+
*/
|
|
385
756
|
function detectComposeHintFiles(repoRoot) {
|
|
386
757
|
return COMPOSE_HINT_FILES.filter((relativePath) => fs.existsSync(path.join(repoRoot, relativePath)));
|
|
387
758
|
}
|
|
388
759
|
|
|
760
|
+
/**
|
|
761
|
+
* Print onboarding hints for fresh repos (no HEAD commit, no origin, or
|
|
762
|
+
* docker-compose detected). Silent when none of those conditions apply.
|
|
763
|
+
*
|
|
764
|
+
* @param {string} repoRoot Repo to describe.
|
|
765
|
+
* @param {string} baseBranch Base branch to reference in suggested commands.
|
|
766
|
+
* @param {string} [repoLabel] Optional label injected into log lines (useful when iterating multiple repos).
|
|
767
|
+
* @returns {void}
|
|
768
|
+
*/
|
|
389
769
|
function printSetupRepoHints(repoRoot, baseBranch, repoLabel = '') {
|
|
390
770
|
const branchDisplay = readBranchDisplayName(repoRoot);
|
|
391
771
|
const hasHeadCommit = repoHasHeadCommit(repoRoot);
|
|
@@ -417,6 +797,14 @@ function printSetupRepoHints(repoRoot, baseBranch, repoLabel = '') {
|
|
|
417
797
|
}
|
|
418
798
|
}
|
|
419
799
|
|
|
800
|
+
/**
|
|
801
|
+
* Test whether the working tree at `repoRoot` has changes outside the
|
|
802
|
+
* lock-registry file.
|
|
803
|
+
*
|
|
804
|
+
* @param {string} repoRoot Repo to inspect.
|
|
805
|
+
* @returns {boolean} True when meaningful changes are present.
|
|
806
|
+
* @throws {Error} When `git status --porcelain` fails.
|
|
807
|
+
*/
|
|
420
808
|
function workingTreeIsDirty(repoRoot) {
|
|
421
809
|
const result = gitRun(repoRoot, ['status', '--porcelain'], { allowFailure: true });
|
|
422
810
|
if (result.status !== 0) {
|
|
@@ -434,6 +822,15 @@ function workingTreeIsDirty(repoRoot) {
|
|
|
434
822
|
return significant.length > 0;
|
|
435
823
|
}
|
|
436
824
|
|
|
825
|
+
/**
|
|
826
|
+
* Ensure `repoRoot` is checked out on `branch`, performing a checkout if
|
|
827
|
+
* needed. Failures are reported via the returned object rather than thrown
|
|
828
|
+
* so callers can decide how to surface them.
|
|
829
|
+
*
|
|
830
|
+
* @param {string} repoRoot Repo to operate on.
|
|
831
|
+
* @param {string} branch Branch to check out.
|
|
832
|
+
* @returns {EnsureRepoBranchResult} Outcome of the operation.
|
|
833
|
+
*/
|
|
437
834
|
function ensureRepoBranch(repoRoot, branch) {
|
|
438
835
|
const current = currentBranchName(repoRoot);
|
|
439
836
|
if (current === branch) {
|
|
@@ -461,6 +858,14 @@ function ensureRepoBranch(repoRoot, branch) {
|
|
|
461
858
|
return { ok: true, changed: true };
|
|
462
859
|
}
|
|
463
860
|
|
|
861
|
+
/**
|
|
862
|
+
* Fetch `origin/<baseBranch>` and verify the remote ref now exists.
|
|
863
|
+
*
|
|
864
|
+
* @param {string} repoRoot Repo to operate on.
|
|
865
|
+
* @param {string} baseBranch Base branch name (without `origin/` prefix).
|
|
866
|
+
* @returns {void}
|
|
867
|
+
* @throws {Error} When the fetch fails or the remote base ref is missing.
|
|
868
|
+
*/
|
|
464
869
|
function ensureOriginBaseRef(repoRoot, baseBranch) {
|
|
465
870
|
const fetch = gitRun(repoRoot, ['fetch', 'origin', baseBranch, '--quiet'], { allowFailure: true });
|
|
466
871
|
if (fetch.status !== 0) {
|
|
@@ -476,6 +881,15 @@ function ensureOriginBaseRef(repoRoot, baseBranch) {
|
|
|
476
881
|
}
|
|
477
882
|
}
|
|
478
883
|
|
|
884
|
+
/**
|
|
885
|
+
* Compute the ahead/behind commit counts between two refs.
|
|
886
|
+
*
|
|
887
|
+
* @param {string} repoRoot Repo to inspect.
|
|
888
|
+
* @param {string} branchRef Branch ref (left side of `...`).
|
|
889
|
+
* @param {string} baseRef Base ref (right side of `...`).
|
|
890
|
+
* @returns {AheadBehindCounts} Commits ahead of and behind the base.
|
|
891
|
+
* @throws {Error} When `git rev-list` cannot compute the comparison.
|
|
892
|
+
*/
|
|
479
893
|
function aheadBehind(repoRoot, branchRef, baseRef) {
|
|
480
894
|
const result = gitRun(repoRoot, ['rev-list', '--left-right', '--count', `${branchRef}...${baseRef}`], {
|
|
481
895
|
allowFailure: true,
|
|
@@ -489,6 +903,12 @@ function aheadBehind(repoRoot, branchRef, baseRef) {
|
|
|
489
903
|
return { ahead: Number.isFinite(ahead) ? ahead : 0, behind: Number.isFinite(behind) ? behind : 0 };
|
|
490
904
|
}
|
|
491
905
|
|
|
906
|
+
/**
|
|
907
|
+
* Inspect `git status --porcelain` for just the lock-registry file.
|
|
908
|
+
*
|
|
909
|
+
* @param {string} repoRoot Repo to inspect.
|
|
910
|
+
* @returns {LockRegistryStatus} Whether the lock file is dirty and/or untracked.
|
|
911
|
+
*/
|
|
492
912
|
function lockRegistryStatus(repoRoot) {
|
|
493
913
|
const result = gitRun(repoRoot, ['status', '--porcelain', '--', LOCK_FILE_RELATIVE], { allowFailure: true });
|
|
494
914
|
if (result.status !== 0) {
|
|
@@ -502,6 +922,13 @@ function lockRegistryStatus(repoRoot) {
|
|
|
502
922
|
return { dirty: true, untracked };
|
|
503
923
|
}
|
|
504
924
|
|
|
925
|
+
/**
|
|
926
|
+
* Enumerate worktrees whose branch lives under `refs/heads/agent/`.
|
|
927
|
+
*
|
|
928
|
+
* @param {string} repoRoot Repo to inspect.
|
|
929
|
+
* @returns {AgentWorktreeEntry[]} One entry per agent worktree.
|
|
930
|
+
* @throws {Error} When `git worktree list` cannot run.
|
|
931
|
+
*/
|
|
505
932
|
function listAgentWorktrees(repoRoot) {
|
|
506
933
|
const result = gitRun(repoRoot, ['worktree', 'list', '--porcelain'], { allowFailure: true });
|
|
507
934
|
if (result.status !== 0) {
|
|
@@ -543,12 +970,28 @@ function listAgentWorktrees(repoRoot) {
|
|
|
543
970
|
return entries;
|
|
544
971
|
}
|
|
545
972
|
|
|
973
|
+
/**
|
|
974
|
+
* Like {@link listLocalAgentBranches} but restricted to entries starting
|
|
975
|
+
* with `agent/`. Used as the candidate list for batch-finish flows.
|
|
976
|
+
*
|
|
977
|
+
* @param {string} repoRoot Repo to inspect.
|
|
978
|
+
* @returns {string[]} Agent branch names.
|
|
979
|
+
*/
|
|
546
980
|
function listLocalAgentBranchesForFinish(repoRoot) {
|
|
547
981
|
return uniquePreserveOrder(
|
|
548
982
|
listLocalAgentBranches(repoRoot).filter((line) => line.startsWith('agent/')),
|
|
549
983
|
);
|
|
550
984
|
}
|
|
551
985
|
|
|
986
|
+
/**
|
|
987
|
+
* Interpret a `git diff --quiet`-style command: 0 = no changes, 1 = changes,
|
|
988
|
+
* anything else is a real error.
|
|
989
|
+
*
|
|
990
|
+
* @param {string} worktreePath Worktree to invoke git in.
|
|
991
|
+
* @param {ReadonlyArray<string>} args Arguments after `-C <worktreePath>`.
|
|
992
|
+
* @returns {boolean} True when the diff reports changes.
|
|
993
|
+
* @throws {Error} When git exits with a status other than 0 or 1.
|
|
994
|
+
*/
|
|
552
995
|
function gitQuietChangeResult(worktreePath, args) {
|
|
553
996
|
const result = run('git', ['-C', worktreePath, ...args], { stdio: 'pipe' });
|
|
554
997
|
if (result.status === 0) {
|
|
@@ -564,6 +1007,14 @@ function gitQuietChangeResult(worktreePath, args) {
|
|
|
564
1007
|
);
|
|
565
1008
|
}
|
|
566
1009
|
|
|
1010
|
+
/**
|
|
1011
|
+
* Detect any pending local work in `worktreePath` (unstaged, staged, or
|
|
1012
|
+
* untracked), ignoring noise from the agent file-locks state file.
|
|
1013
|
+
*
|
|
1014
|
+
* @param {string} worktreePath Worktree to inspect.
|
|
1015
|
+
* @returns {boolean} True when local changes are present.
|
|
1016
|
+
* @throws {Error} When git commands invoked during the probe fail.
|
|
1017
|
+
*/
|
|
567
1018
|
function worktreeHasLocalChanges(worktreePath) {
|
|
568
1019
|
const hasUnstaged = gitQuietChangeResult(worktreePath, [
|
|
569
1020
|
'diff',
|
|
@@ -597,6 +1048,15 @@ function worktreeHasLocalChanges(worktreePath) {
|
|
|
597
1048
|
return String(untracked.stdout || '').trim().length > 0;
|
|
598
1049
|
}
|
|
599
1050
|
|
|
1051
|
+
/**
|
|
1052
|
+
* Run `git -C <worktreePath> <args>` and return stdout split into trimmed
|
|
1053
|
+
* non-empty lines.
|
|
1054
|
+
*
|
|
1055
|
+
* @param {string} worktreePath Worktree to invoke git in.
|
|
1056
|
+
* @param {ReadonlyArray<string>} args Arguments after `-C <worktreePath>`.
|
|
1057
|
+
* @returns {string[]} Trimmed stdout lines (empties removed).
|
|
1058
|
+
* @throws {Error} When git exits non-zero.
|
|
1059
|
+
*/
|
|
600
1060
|
function gitOutputLines(worktreePath, args) {
|
|
601
1061
|
const result = run('git', ['-C', worktreePath, ...args], { stdio: 'pipe' });
|
|
602
1062
|
if (result.status !== 0) {
|
|
@@ -612,6 +1072,13 @@ function gitOutputLines(worktreePath, args) {
|
|
|
612
1072
|
.filter(Boolean);
|
|
613
1073
|
}
|
|
614
1074
|
|
|
1075
|
+
/**
|
|
1076
|
+
* Test whether `refs/heads/<branch>` exists locally.
|
|
1077
|
+
*
|
|
1078
|
+
* @param {string} repoRoot Repo to inspect.
|
|
1079
|
+
* @param {string} branch Branch name (without `refs/heads/` prefix).
|
|
1080
|
+
* @returns {boolean} True when the local branch exists.
|
|
1081
|
+
*/
|
|
615
1082
|
function branchExists(repoRoot, branch) {
|
|
616
1083
|
const result = gitRun(repoRoot, ['show-ref', '--verify', '--quiet', `refs/heads/${branch}`], {
|
|
617
1084
|
allowFailure: true,
|
|
@@ -619,19 +1086,46 @@ function branchExists(repoRoot, branch) {
|
|
|
619
1086
|
return result.status === 0;
|
|
620
1087
|
}
|
|
621
1088
|
|
|
622
|
-
|
|
1089
|
+
/**
|
|
1090
|
+
* Resolve the base branch for the finish flow: CLI override wins; otherwise
|
|
1091
|
+
* the per-branch `branch.<source>.guardexBase` recorded at branch-start;
|
|
1092
|
+
* otherwise the repo-wide configured base; otherwise the repo's detected
|
|
1093
|
+
* default branch.
|
|
1094
|
+
*
|
|
1095
|
+
* @param {string} repoRoot Repo to inspect.
|
|
1096
|
+
* @param {string} sourceBranch Source agent branch (used for per-branch base).
|
|
1097
|
+
* @param {string} [explicitBase] CLI override, if any.
|
|
1098
|
+
* @returns {string} Resolved base branch name.
|
|
1099
|
+
*/
|
|
1100
|
+
function resolveFinishBaseBranch(repoRoot, sourceBranch, explicitBase) {
|
|
623
1101
|
if (explicitBase) {
|
|
624
1102
|
return explicitBase;
|
|
625
1103
|
}
|
|
626
1104
|
|
|
1105
|
+
if (sourceBranch) {
|
|
1106
|
+
const perBranch = readGitConfig(repoRoot, `branch.${sourceBranch}.guardexBase`);
|
|
1107
|
+
if (perBranch) {
|
|
1108
|
+
return perBranch;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
627
1112
|
const configured = readGitConfig(repoRoot, GIT_BASE_BRANCH_KEY);
|
|
628
1113
|
if (configured) {
|
|
629
1114
|
return configured;
|
|
630
1115
|
}
|
|
631
1116
|
|
|
632
|
-
return
|
|
1117
|
+
return detectDefaultBaseBranch(repoRoot);
|
|
633
1118
|
}
|
|
634
1119
|
|
|
1120
|
+
/**
|
|
1121
|
+
* Test whether `branch` is an ancestor of `baseBranch` (i.e., already merged).
|
|
1122
|
+
*
|
|
1123
|
+
* @param {string} repoRoot Repo to inspect.
|
|
1124
|
+
* @param {string} branch Branch to check.
|
|
1125
|
+
* @param {string} baseBranch Base branch to compare against.
|
|
1126
|
+
* @returns {boolean} True when `branch` is an ancestor of `baseBranch`.
|
|
1127
|
+
* @throws {Error} When git returns an unexpected status (anything other than 0 or 1).
|
|
1128
|
+
*/
|
|
635
1129
|
function branchMergedIntoBase(repoRoot, branch, baseBranch) {
|
|
636
1130
|
if (!branchExists(repoRoot, baseBranch)) {
|
|
637
1131
|
return false;
|
|
@@ -648,6 +1142,17 @@ function branchMergedIntoBase(repoRoot, branch, baseBranch) {
|
|
|
648
1142
|
throw new Error(`Unable to determine merge status for ${branch} -> ${baseBranch}`);
|
|
649
1143
|
}
|
|
650
1144
|
|
|
1145
|
+
/**
|
|
1146
|
+
* Run a sync against `baseRef` using the requested strategy. On failure,
|
|
1147
|
+
* surface git output plus a hint for resolving an in-progress rebase/merge.
|
|
1148
|
+
*
|
|
1149
|
+
* @param {string} repoRoot Repo to operate on.
|
|
1150
|
+
* @param {'rebase'|'merge'} strategy Sync strategy.
|
|
1151
|
+
* @param {string} baseRef Ref to rebase onto / merge from.
|
|
1152
|
+
* @param {boolean} ffOnly Pass `--ff-only` to merge; rejected for rebase.
|
|
1153
|
+
* @returns {void}
|
|
1154
|
+
* @throws {Error} When the rebase/merge fails or when `ffOnly` is combined with `rebase`.
|
|
1155
|
+
*/
|
|
651
1156
|
function syncOperation(repoRoot, strategy, baseRef, ffOnly) {
|
|
652
1157
|
if (strategy === 'rebase') {
|
|
653
1158
|
if (ffOnly) {
|
|
@@ -697,8 +1202,10 @@ module.exports = {
|
|
|
697
1202
|
hasSignificantWorkingTreeChanges,
|
|
698
1203
|
readProtectedBranches,
|
|
699
1204
|
ensureSetupProtectedBranches,
|
|
1205
|
+
ensureSubmoduleAutoSync,
|
|
700
1206
|
writeProtectedBranches,
|
|
701
1207
|
readGitConfig,
|
|
1208
|
+
detectDefaultBaseBranch,
|
|
702
1209
|
resolveBaseBranch,
|
|
703
1210
|
resolveSyncStrategy,
|
|
704
1211
|
currentBranchName,
|