unirepo-cli 0.5.0 → 0.5.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unirepo-cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "CLI tool for creating and managing git-subtree monorepos — run your agents across repos",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,9 +1,9 @@
1
- import { git, detectDefaultBranch, extractRepoName, setConfiguredSubtreeBranch } from '../git.js';
1
+ import { git, detectDefaultBranch, extractRepoName, getMonorepoRoot, setConfiguredSubtreeBranch } from '../git.js';
2
2
  import { validateGitSubtree, validateUrls, validateInsideMonorepo, validateNameAvailable, validateReachable } from '../validate.js';
3
3
  import * as ui from '../ui.js';
4
4
 
5
5
  export async function runAdd({ url, prefix, branch, fullHistory }) {
6
- const cwd = process.cwd();
6
+ const cwd = getMonorepoRoot(process.cwd());
7
7
 
8
8
  // ── Preflight ────────────────────────────────────────────────────────────
9
9
  ui.header('Adding repository');
@@ -2,6 +2,7 @@ import {
2
2
  git,
3
3
  getConfiguredSubtreePushBranch,
4
4
  getCurrentBranch,
5
+ getMonorepoRoot,
5
6
  getSubtreePrefixes,
6
7
  getTrackedSubtreeBranch,
7
8
  resolveSubtreePushBranch,
@@ -10,7 +11,7 @@ import { validateInsideMonorepo } from '../validate.js';
10
11
  import * as ui from '../ui.js';
11
12
 
12
13
  export async function runBranch({ name }) {
13
- const cwd = process.cwd();
14
+ const cwd = getMonorepoRoot(process.cwd());
14
15
 
15
16
  validateInsideMonorepo(cwd);
16
17
 
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  getConfiguredSubtreePushBranch,
3
3
  getCurrentBranch,
4
+ getMonorepoRoot,
4
5
  getSubtreePrefixes,
5
6
  getChangedSubtrees,
6
7
  getTrackedSubtreeBranch,
@@ -62,7 +63,7 @@ export function planPrTargets({
62
63
  }
63
64
 
64
65
  export async function runPr({ subtrees: requestedSubtrees, title, body, base, head, draft, dryRun }) {
65
- const cwd = process.cwd();
66
+ const cwd = getMonorepoRoot(process.cwd());
66
67
 
67
68
  ui.header('Creating pull requests');
68
69
  ui.blank();
@@ -1,4 +1,4 @@
1
- import { git, getSubtreePrefixes, getTrackedSubtreeBranch, hasRemoteBranch, setConfiguredSubtreeBranch } from '../git.js';
1
+ import { git, getMonorepoRoot, getSubtreePrefixes, getTrackedSubtreeBranch, hasRemoteBranch, setConfiguredSubtreeBranch } from '../git.js';
2
2
  import { validateGitSubtree, validateInsideMonorepo } from '../validate.js';
3
3
  import * as ui from '../ui.js';
4
4
 
@@ -49,7 +49,7 @@ export function resolvePullUpstreamBranch({ requestedBranch, trackedBranch }) {
49
49
  }
50
50
 
51
51
  export async function runPull({ subtrees: requestedSubtrees, branch, fullHistory }) {
52
- const cwd = process.cwd();
52
+ const cwd = getMonorepoRoot(process.cwd());
53
53
 
54
54
  ui.header('Pulling subtrees');
55
55
  ui.blank();
@@ -2,16 +2,18 @@ import {
2
2
  git,
3
3
  getConfiguredSubtreePushBranch,
4
4
  getCurrentBranch,
5
+ getMonorepoRoot,
5
6
  getSubtreePrefixes,
6
7
  getChangedSubtrees,
7
8
  hasUncommittedChanges,
8
9
  resolveSubtreePushBranch,
10
+ setLastPushedRef,
9
11
  } from '../git.js';
10
12
  import { validateGitSubtree, validateInsideMonorepo } from '../validate.js';
11
13
  import * as ui from '../ui.js';
12
14
 
13
15
  export async function runPush({ subtrees: requestedSubtrees, branch, dryRun }) {
14
- const cwd = process.cwd();
16
+ const cwd = getMonorepoRoot(process.cwd());
15
17
 
16
18
  // ── Preflight ────────────────────────────────────────────────────────────
17
19
  ui.header('Pushing subtrees');
@@ -92,6 +94,8 @@ export async function runPush({ subtrees: requestedSubtrees, branch, dryRun }) {
92
94
  ui.pushSlow();
93
95
  try {
94
96
  git(`subtree push --prefix="${target.name}" "${target.name}" "${target.pushBranch}"`, { cwd });
97
+ const headRef = git('rev-parse HEAD', { cwd, silent: true });
98
+ setLastPushedRef(cwd, target.name, headRef);
95
99
  ui.success(`${target.name} pushed`);
96
100
  succeeded++;
97
101
  } catch (err) {
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  getConfiguredSubtreePushBranch,
3
3
  getCurrentBranch,
4
+ getMonorepoRoot,
4
5
  getSubtreePrefixes,
5
6
  getChangedSubtrees,
6
7
  getTrackedSubtreeBranch,
@@ -31,7 +32,7 @@ export function buildStatusSubtrees({
31
32
  }
32
33
 
33
34
  export async function runStatus({ json }) {
34
- const cwd = process.cwd();
35
+ const cwd = getMonorepoRoot(process.cwd());
35
36
 
36
37
  validateInsideMonorepo(cwd);
37
38
 
package/src/git.js CHANGED
@@ -14,6 +14,10 @@ function getSubtreePushBranchConfigKey(prefixName) {
14
14
  return `unirepo.subtree.${prefixName}.pushBranch`;
15
15
  }
16
16
 
17
+ function getSubtreeLastPushedRefKey(prefixName) {
18
+ return `unirepo.subtree.${prefixName}.lastPushedRef`;
19
+ }
20
+
17
21
  /**
18
22
  * Execute a git command and return trimmed stdout.
19
23
  * @param {string} args - git arguments
@@ -199,15 +203,15 @@ export function getChangedSubtrees(cwd) {
199
203
  continue;
200
204
  }
201
205
 
202
- // Check 2: committed changes since the last subtree add/pull merge
206
+ // Check 2: committed changes since the last push (or last subtree merge as fallback)
203
207
  try {
204
- const mergeCommit = findLastSubtreeMerge(cwd, prefix.name);
205
- if (mergeCommit) {
206
- // Are there any commits after the merge that touch this prefix?
207
- const commits = execSync(
208
- `git log --oneline "${mergeCommit}..HEAD" -- "${prefix.name}"`,
209
- { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }
210
- ).trim();
208
+ const lastPushedRef = getLastPushedRef(cwd, prefix.name);
209
+ const anchor = lastPushedRef || findLastSubtreeMerge(cwd, prefix.name);
210
+ if (anchor) {
211
+ const commits = git(
212
+ `log --oneline ${quoteShellArg(anchor + '..HEAD')} -- ${quoteShellArg(prefix.name)}`,
213
+ { cwd, silent: true, allowFailure: true }
214
+ );
211
215
  if (commits) {
212
216
  changed.push(prefix);
213
217
  }
@@ -312,6 +316,43 @@ export function setConfiguredSubtreePushBranch(cwd, prefixName, branch) {
312
316
  );
313
317
  }
314
318
 
319
+ /**
320
+ * Get the SHA of the last HEAD that was successfully pushed for a subtree.
321
+ */
322
+ export function getLastPushedRef(cwd, prefixName) {
323
+ return git(`config --get ${quoteShellArg(getSubtreeLastPushedRefKey(prefixName))}`, {
324
+ cwd,
325
+ silent: true,
326
+ allowFailure: true,
327
+ }) || null;
328
+ }
329
+
330
+ /**
331
+ * Persist the HEAD SHA that was last successfully pushed for a subtree.
332
+ */
333
+ export function setLastPushedRef(cwd, prefixName, ref) {
334
+ git(
335
+ `config ${quoteShellArg(getSubtreeLastPushedRefKey(prefixName))} ${quoteShellArg(ref)}`,
336
+ { cwd, silent: true }
337
+ );
338
+ }
339
+
340
+ /**
341
+ * Resolve the root of the git work tree (the monorepo root), even when called
342
+ * from inside a subtree subdirectory.
343
+ */
344
+ export function getMonorepoRoot(cwd) {
345
+ try {
346
+ return execSync('git rev-parse --show-toplevel', {
347
+ cwd,
348
+ encoding: 'utf-8',
349
+ stdio: ['pipe', 'pipe', 'pipe'],
350
+ }).trim();
351
+ } catch {
352
+ return cwd;
353
+ }
354
+ }
355
+
315
356
  /**
316
357
  * Resolve the effective push/head branch for a subtree.
317
358
  */