mintree 0.1.9 → 0.1.10
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/dist/lib/git.d.ts +18 -0
- package/dist/lib/git.js +32 -0
- package/dist/lib/worktreeCreate.js +32 -11
- package/package.json +1 -1
package/dist/lib/git.d.ts
CHANGED
|
@@ -43,6 +43,24 @@ export declare function ensureGitignoreEntries(repoRoot: string, entries: string
|
|
|
43
43
|
export declare function getDefaultBranch(repoRoot: string): string | null;
|
|
44
44
|
export type BranchExistence = "local" | "remote" | null;
|
|
45
45
|
export declare function branchExists(repoRoot: string, branch: string): BranchExistence;
|
|
46
|
+
/**
|
|
47
|
+
* True when `origin/<branch>` resolves locally. Unlike `branchExists`, this
|
|
48
|
+
* reports the remote-tracking ref even when a local branch of the same name
|
|
49
|
+
* also exists — callers that want to fork from the freshest remote tip need
|
|
50
|
+
* to know the remote ref is there, not just "some ref named X".
|
|
51
|
+
*/
|
|
52
|
+
export declare function remoteBranchExists(repoRoot: string, branch: string): boolean;
|
|
53
|
+
export type FetchResult = {
|
|
54
|
+
ok: boolean;
|
|
55
|
+
reason?: string;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Best-effort `git fetch origin` so worktrees get created off fresh refs
|
|
59
|
+
* instead of a stale local checkout. Never throws: when there's no `origin`
|
|
60
|
+
* remote or the network is down, returns `{ ok: false, reason }` and callers
|
|
61
|
+
* fall back to whatever refs are already local.
|
|
62
|
+
*/
|
|
63
|
+
export declare function fetchRemote(repoRoot: string): FetchResult;
|
|
46
64
|
/**
|
|
47
65
|
* Returns the absolute path where `branch` is checked out as a worktree, or
|
|
48
66
|
* null when the branch is not checked out anywhere. Parses the porcelain
|
package/dist/lib/git.js
CHANGED
|
@@ -158,6 +158,38 @@ export function branchExists(repoRoot, branch) {
|
|
|
158
158
|
return "remote";
|
|
159
159
|
return null;
|
|
160
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* True when `origin/<branch>` resolves locally. Unlike `branchExists`, this
|
|
163
|
+
* reports the remote-tracking ref even when a local branch of the same name
|
|
164
|
+
* also exists — callers that want to fork from the freshest remote tip need
|
|
165
|
+
* to know the remote ref is there, not just "some ref named X".
|
|
166
|
+
*/
|
|
167
|
+
export function remoteBranchExists(repoRoot, branch) {
|
|
168
|
+
return trySh(`git rev-parse --verify --quiet "refs/remotes/origin/${branch}"`, repoRoot) !== null;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Best-effort `git fetch origin` so worktrees get created off fresh refs
|
|
172
|
+
* instead of a stale local checkout. Never throws: when there's no `origin`
|
|
173
|
+
* remote or the network is down, returns `{ ok: false, reason }` and callers
|
|
174
|
+
* fall back to whatever refs are already local.
|
|
175
|
+
*/
|
|
176
|
+
export function fetchRemote(repoRoot) {
|
|
177
|
+
if (!trySh("git remote get-url origin", repoRoot)) {
|
|
178
|
+
return { ok: false, reason: "no origin remote" };
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
execSync("git fetch origin", { cwd: repoRoot, stdio: ["ignore", "pipe", "pipe"] });
|
|
182
|
+
return { ok: true };
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
const stderr = err && typeof err === "object" && "stderr" in err
|
|
186
|
+
? String(err.stderr).trim()
|
|
187
|
+
: err instanceof Error
|
|
188
|
+
? err.message
|
|
189
|
+
: String(err);
|
|
190
|
+
return { ok: false, reason: stderr || "git fetch failed" };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
161
193
|
/**
|
|
162
194
|
* Returns the absolute path where `branch` is checked out as a worktree, or
|
|
163
195
|
* null when the branch is not checked out anywhere. Parses the porcelain
|
|
@@ -3,7 +3,7 @@ import * as os from "os";
|
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import { execSync } from "child_process";
|
|
5
5
|
import { parseBranch, isParseError } from "./branch.js";
|
|
6
|
-
import { findMainRepoRoot, getMintreeDir, getWorktreesDir, getInitScriptPath, getDefaultBranch, getCurrentBranch, branchExists, worktreeForBranch, addWorktree, pathExists, isExecutable, } from "./git.js";
|
|
6
|
+
import { findMainRepoRoot, getMintreeDir, getWorktreesDir, getInitScriptPath, getDefaultBranch, getCurrentBranch, branchExists, remoteBranchExists, fetchRemote, worktreeForBranch, addWorktree, pathExists, isExecutable, } from "./git.js";
|
|
7
7
|
import { upsertIssue } from "./metadata.js";
|
|
8
8
|
function tryRunInitScript(scriptPath, worktreePath, repoRoot) {
|
|
9
9
|
if (!pathExists(scriptPath))
|
|
@@ -80,6 +80,19 @@ export function runCreate(branchArg, opts) {
|
|
|
80
80
|
hint: "Remove it first or pick a different branch description.",
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
|
+
const steps = [];
|
|
84
|
+
steps.push({
|
|
85
|
+
kind: "ok",
|
|
86
|
+
label: "parsed branch",
|
|
87
|
+
detail: `type=${parsed.type}, issue=${parsed.issueId}, desc=${parsed.desc}`,
|
|
88
|
+
});
|
|
89
|
+
// Fetch before resolving refs so the worktree forks from fresh code, not a
|
|
90
|
+
// stale local checkout. Best-effort: offline / no-remote just warns and we
|
|
91
|
+
// fall back to whatever is already local.
|
|
92
|
+
const fetch = fetchRemote(root);
|
|
93
|
+
steps.push(fetch.ok
|
|
94
|
+
? { kind: "ok", label: "fetched origin", detail: "refs up to date" }
|
|
95
|
+
: { kind: "warn", label: "skipped git fetch", detail: fetch.reason });
|
|
83
96
|
const existence = branchExists(root, parsed.branch);
|
|
84
97
|
let base;
|
|
85
98
|
if (existence === null) {
|
|
@@ -99,14 +112,15 @@ export function runCreate(branchArg, opts) {
|
|
|
99
112
|
};
|
|
100
113
|
}
|
|
101
114
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
115
|
+
// For a brand-new branch, fork from the freshly fetched `origin/<base>`
|
|
116
|
+
// tip when origin has it — that's the whole point of the fetch above.
|
|
117
|
+
// Without a successful fetch (or origin ref) we fork from the local base.
|
|
118
|
+
let baseRef = base;
|
|
119
|
+
if (existence === null && base && fetch.ok && remoteBranchExists(root, base)) {
|
|
120
|
+
baseRef = `origin/${base}`;
|
|
121
|
+
}
|
|
108
122
|
try {
|
|
109
|
-
addWorktree({ repoRoot: root, branch: parsed.branch, worktreePath, base });
|
|
123
|
+
addWorktree({ repoRoot: root, branch: parsed.branch, worktreePath, base: baseRef });
|
|
110
124
|
}
|
|
111
125
|
catch (err) {
|
|
112
126
|
const stderr = err && typeof err === "object" && "stderr" in err
|
|
@@ -134,7 +148,7 @@ export function runCreate(branchArg, opts) {
|
|
|
134
148
|
steps.push({
|
|
135
149
|
kind: "ok",
|
|
136
150
|
label: "created new branch",
|
|
137
|
-
detail: `${parsed.branch} (from ${
|
|
151
|
+
detail: `${parsed.branch} (from ${baseRef})`,
|
|
138
152
|
});
|
|
139
153
|
}
|
|
140
154
|
steps.push({ kind: "ok", label: "worktree created", detail: worktreePath });
|
|
@@ -243,8 +257,15 @@ export function runCreateDetached(opts) {
|
|
|
243
257
|
label: "detached worktree",
|
|
244
258
|
detail: `issue=${opts.issueId}, base=${currentBranch}`,
|
|
245
259
|
});
|
|
260
|
+
// Fetch so the detached worktree forks from the fresh remote tip of the
|
|
261
|
+
// current branch instead of a stale local checkout. Best-effort.
|
|
262
|
+
const fetch = fetchRemote(root);
|
|
263
|
+
steps.push(fetch.ok
|
|
264
|
+
? { kind: "ok", label: "fetched origin", detail: "refs up to date" }
|
|
265
|
+
: { kind: "warn", label: "skipped git fetch", detail: fetch.reason });
|
|
266
|
+
const baseRef = fetch.ok && remoteBranchExists(root, currentBranch) ? `origin/${currentBranch}` : currentBranch;
|
|
246
267
|
try {
|
|
247
|
-
execSync(`git worktree add --detach '${worktreePath.replace(/'/g, `'\\''`)}' '${
|
|
268
|
+
execSync(`git worktree add --detach '${worktreePath.replace(/'/g, `'\\''`)}' '${baseRef.replace(/'/g, `'\\''`)}'`, { cwd: root, stdio: ["ignore", "pipe", "pipe"] });
|
|
248
269
|
}
|
|
249
270
|
catch (err) {
|
|
250
271
|
const stderr = err && typeof err === "object" && "stderr" in err
|
|
@@ -257,7 +278,7 @@ export function runCreateDetached(opts) {
|
|
|
257
278
|
steps.push({
|
|
258
279
|
kind: "ok",
|
|
259
280
|
label: "checked out detached HEAD",
|
|
260
|
-
detail: `at tip of ${
|
|
281
|
+
detail: `at tip of ${baseRef}`,
|
|
261
282
|
});
|
|
262
283
|
steps.push({ kind: "ok", label: "worktree created", detail: worktreePath });
|
|
263
284
|
upsertIssue(root, opts.issueId, { base_branch: currentBranch });
|
package/package.json
CHANGED