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 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
- const steps = [];
103
- steps.push({
104
- kind: "ok",
105
- label: "parsed branch",
106
- detail: `type=${parsed.type}, issue=${parsed.issueId}, desc=${parsed.desc}`,
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 ${base})`,
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, `'\\''`)}' '${currentBranch.replace(/'/g, `'\\''`)}'`, { cwd: root, stdio: ["ignore", "pipe", "pipe"] });
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 ${currentBranch}`,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mintree",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Issue-driven git worktrees + Claude Code sessions for repos with an opinionated SDD+TDD flow.",
5
5
  "license": "MIT",
6
6
  "author": "Martin Mineo <mmineo@canarytechnologies.com>",