unirepo-cli 0.4.1 → 0.5.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 CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  ### One workspace to refactor your whole stack in one go
8
8
 
9
- Work across multiple repos like they’re one. Unirepo turns multiple GitHub repositories into a single unified workspace. Edit backend, frontend, and shared code in one tree, commit from one place, and push changes back to each repo using the same branch.
9
+ Work across multiple repos like they’re one. Unirepo turns multiple GitHub repositories into a single unified workspace. Edit backend, frontend, and shared code in one tree, commit from one place, and push changes back to each repo using the same branch by default.
10
10
 
11
11
  ### Built for AI coding agents
12
12
 
@@ -140,7 +140,7 @@ AI coding agents work best when they can see the full change at once.
140
140
  | `add <repo>` | Add another repo to the workspace |
141
141
  | `pull [subtree...]` | Pull upstream changes into tracked subtrees |
142
142
  | `status` | Show subtrees, branches, and what changed |
143
- | `branch [name]` | Create or show the current push branch |
143
+ | `branch [name]` | Create or show the current workspace branch |
144
144
  | `push [subtree...]` | Push changed subtrees upstream |
145
145
  | `pr [subtree...]` | Open pull requests for changed subtree repos |
146
146
 
@@ -191,7 +191,7 @@ Pulls upstream changes. Without arguments, pulls all tracked subtrees. When you
191
191
  unirepo status [--json]
192
192
  ```
193
193
 
194
- Shows tracked subtrees, their upstream branches, the current push branch, and which subtrees have changes.
194
+ Shows tracked subtrees, their upstream branches, the current workspace branch, each subtree's effective push branch, and which subtrees have changes.
195
195
 
196
196
  ### branch
197
197
 
@@ -199,7 +199,25 @@ Shows tracked subtrees, their upstream branches, the current push branch, and wh
199
199
  unirepo branch [name]
200
200
  ```
201
201
 
202
- With a name: creates and switches to a new branch. Without: shows the current branch and push targets.
202
+ With a name: creates and switches to a new branch. That branch becomes the default push target for subtrees that do not have their own configured override. Without: shows the current branch and effective push targets.
203
+
204
+ ### Per-subtree push branches
205
+
206
+ By default, `unirepo branch <name>` sets the workspace branch and subtrees push and open PRs from that same branch.
207
+
208
+ If one subtree repo needs a different branch target, configure it in local git config:
209
+
210
+ ```bash
211
+ git config unirepo.subtree.api.pushBranch feature-api
212
+ git config unirepo.subtree.web.pushBranch feature/web-auth
213
+ unirepo status
214
+ ```
215
+
216
+ Remove an override and go back to the workspace branch default:
217
+
218
+ ```bash
219
+ git config --unset unirepo.subtree.api.pushBranch
220
+ ```
203
221
 
204
222
  ### push
205
223
 
@@ -207,11 +225,11 @@ With a name: creates and switches to a new branch. Without: shows the current br
207
225
  unirepo push [subtree...] [--dry-run]
208
226
  ```
209
227
 
210
- Pushes changed subtrees upstream. Without arguments, auto-detects which subtrees have changes.
228
+ Pushes changed subtrees upstream. Without arguments, auto-detects which subtrees have changes. Each subtree pushes to its configured push branch or the current workspace branch by default.
211
229
 
212
230
  | Flag | Effect |
213
231
  | --- | --- |
214
- | `--branch <name>` | Override the upstream branch name |
232
+ | `--branch <name>` | Override the push branch name for all selected subtrees |
215
233
  | `--dry-run` | Show what would run without executing |
216
234
 
217
235
  ### pr
@@ -220,7 +238,7 @@ Pushes changed subtrees upstream. Without arguments, auto-detects which subtrees
220
238
  unirepo pr [subtree...] --title <title> [--body <text>]
221
239
  ```
222
240
 
223
- Opens one GitHub pull request per changed subtree repo. Without subtree arguments, `unirepo` auto-detects changed subtrees. Use explicit subtree names when you want PRs for only part of the workspace.
241
+ Opens one GitHub pull request per changed subtree repo. Without subtree arguments, `unirepo` auto-detects changed subtrees. Use explicit subtree names when you want PRs for only part of the workspace. By default, each PR head matches that subtree's effective push branch.
224
242
 
225
243
 
226
244
  | Flag | Effect |
@@ -228,7 +246,7 @@ Opens one GitHub pull request per changed subtree repo. Without subtree argument
228
246
  | `--title <title>` | Shared PR title for all selected repos |
229
247
  | `--body <text>` | Shared PR description |
230
248
  | `--base <name>` | Override the base branch for all selected repos |
231
- | `--head <name>` | Override the head branch name (default: current branch) |
249
+ | `--head <name>` | Override the head branch name for all selected subtrees |
232
250
  | `--draft` | Create draft pull requests |
233
251
  | `--dry-run` | Show the `gh pr create` commands without executing |
234
252
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unirepo-cli",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "CLI tool for creating and managing git-subtree monorepos — run your agents across repos",
5
5
  "type": "module",
6
6
  "bin": {
@@ -38,6 +38,7 @@
38
38
  "dependencies": {
39
39
  "@inquirer/core": "^10.0.0",
40
40
  "@inquirer/prompts": "^7.0.0",
41
- "chalk": "^5.3.0"
41
+ "chalk": "^5.3.0",
42
+ "unirepo-cli": "^0.4.1"
42
43
  }
43
44
  }
@@ -1,4 +1,11 @@
1
- import { git, getCurrentBranch, getSubtreePrefixes, getTrackedSubtreeBranch } from '../git.js';
1
+ import {
2
+ git,
3
+ getConfiguredSubtreePushBranch,
4
+ getCurrentBranch,
5
+ getSubtreePrefixes,
6
+ getTrackedSubtreeBranch,
7
+ resolveSubtreePushBranch,
8
+ } from '../git.js';
2
9
  import { validateInsideMonorepo } from '../validate.js';
3
10
  import * as ui from '../ui.js';
4
11
 
@@ -18,10 +25,16 @@ export async function runBranch({ name }) {
18
25
  ui.blank();
19
26
  for (const s of subtrees) {
20
27
  const upstream = getTrackedSubtreeBranch(cwd, s.name) || 'unknown';
21
- ui.info(` ${s.name} upstream: ${upstream} push target: ${currentBranch}`);
28
+ const configuredPushBranch = getConfiguredSubtreePushBranch(cwd, s.name);
29
+ const pushBranch = resolveSubtreePushBranch({
30
+ configuredBranch: configuredPushBranch,
31
+ currentBranch,
32
+ });
33
+ const suffix = configuredPushBranch ? ' (configured)' : '';
34
+ ui.info(` ${s.name} upstream: ${upstream} push target: ${pushBranch}${suffix}`);
22
35
  }
23
36
  ui.blank();
24
- ui.info('Use "unirepo branch <name>" to switch all subtrees to a new branch.');
37
+ ui.info('Use "unirepo branch <name>" to switch the workspace branch. Subtrees without a configured override use that branch by default.');
25
38
  ui.blank();
26
39
  return;
27
40
  }
@@ -41,7 +54,16 @@ export async function runBranch({ name }) {
41
54
  ui.blank();
42
55
 
43
56
  for (const s of subtrees) {
44
- ui.success(`${s.name} push will target "${name}" upstream`);
57
+ const configuredPushBranch = getConfiguredSubtreePushBranch(cwd, s.name);
58
+ const pushBranch = resolveSubtreePushBranch({
59
+ configuredBranch: configuredPushBranch,
60
+ currentBranch: name,
61
+ });
62
+ if (configuredPushBranch && configuredPushBranch !== name) {
63
+ ui.success(`${s.name} → push will target "${pushBranch}" upstream (configured override)`);
64
+ } else {
65
+ ui.success(`${s.name} → push will target "${pushBranch}" upstream`);
66
+ }
45
67
  }
46
68
 
47
69
  ui.blank();
@@ -1,9 +1,11 @@
1
1
  import {
2
+ getConfiguredSubtreePushBranch,
2
3
  getCurrentBranch,
3
4
  getSubtreePrefixes,
4
5
  getChangedSubtrees,
5
6
  getTrackedSubtreeBranch,
6
7
  hasRemoteBranch,
8
+ resolveSubtreePushBranch,
7
9
  } from '../git.js';
8
10
  import { isGhAvailable, getGitHubRepoSlugFromUrl, createPullRequest } from '../github.js';
9
11
  import { validateInsideMonorepo } from '../validate.js';
@@ -37,8 +39,10 @@ export function planPrTargets({
37
39
  changedPrefixes,
38
40
  requestedSubtrees,
39
41
  requestedBase,
40
- headBranch,
42
+ requestedHead,
43
+ currentBranch,
41
44
  getTrackedBranchFn = () => null,
45
+ getConfiguredPushBranchFn = () => null,
42
46
  getRepoSlugFn = () => null,
43
47
  }) {
44
48
  return selectPrSubtrees(allPrefixes, changedPrefixes, requestedSubtrees).map((prefix) => ({
@@ -49,7 +53,11 @@ export function planPrTargets({
49
53
  requestedBase,
50
54
  trackedBranch: getTrackedBranchFn(prefix.name),
51
55
  }),
52
- head: headBranch,
56
+ head: resolveSubtreePushBranch({
57
+ requestedBranch: requestedHead,
58
+ configuredBranch: getConfiguredPushBranchFn(prefix.name),
59
+ currentBranch,
60
+ }),
53
61
  }));
54
62
  }
55
63
 
@@ -66,7 +74,6 @@ export async function runPr({ subtrees: requestedSubtrees, title, body, base, he
66
74
  }
67
75
 
68
76
  const currentBranch = getCurrentBranch(cwd);
69
- const headBranch = head || currentBranch;
70
77
  const allPrefixes = getSubtreePrefixes(cwd);
71
78
  const changedPrefixes = getChangedSubtrees(cwd);
72
79
  const targets = planPrTargets({
@@ -74,8 +81,10 @@ export async function runPr({ subtrees: requestedSubtrees, title, body, base, he
74
81
  changedPrefixes,
75
82
  requestedSubtrees,
76
83
  requestedBase: base,
77
- headBranch,
84
+ requestedHead: head,
85
+ currentBranch,
78
86
  getTrackedBranchFn: (prefixName) => getTrackedSubtreeBranch(cwd, prefixName),
87
+ getConfiguredPushBranchFn: (prefixName) => getConfiguredSubtreePushBranch(cwd, prefixName),
79
88
  getRepoSlugFn: getGitHubRepoSlugFromUrl,
80
89
  });
81
90
 
@@ -86,7 +95,12 @@ export async function runPr({ subtrees: requestedSubtrees, title, body, base, he
86
95
  return;
87
96
  }
88
97
 
89
- ui.info(`Head branch: ${headBranch}`);
98
+ const uniqueHeadBranches = [...new Set(targets.map((target) => target.head))];
99
+ if (uniqueHeadBranches.length === 1) {
100
+ ui.info(`Head branch: ${uniqueHeadBranches[0]}`);
101
+ } else {
102
+ ui.info('Head branches: per subtree');
103
+ }
90
104
  ui.info(`Subtrees: ${targets.map((target) => target.name).join(', ')}`);
91
105
  ui.blank();
92
106
 
@@ -1,4 +1,12 @@
1
- import { git, getCurrentBranch, getSubtreePrefixes, getChangedSubtrees, hasUncommittedChanges } from '../git.js';
1
+ import {
2
+ git,
3
+ getConfiguredSubtreePushBranch,
4
+ getCurrentBranch,
5
+ getSubtreePrefixes,
6
+ getChangedSubtrees,
7
+ hasUncommittedChanges,
8
+ resolveSubtreePushBranch,
9
+ } from '../git.js';
2
10
  import { validateGitSubtree, validateInsideMonorepo } from '../validate.js';
3
11
  import * as ui from '../ui.js';
4
12
 
@@ -13,7 +21,6 @@ export async function runPush({ subtrees: requestedSubtrees, branch, dryRun }) {
13
21
  validateInsideMonorepo(cwd);
14
22
 
15
23
  const currentBranch = getCurrentBranch(cwd);
16
- const pushBranch = branch || currentBranch;
17
24
 
18
25
  // ── Check for uncommitted work ────────────────────────────────────────────
19
26
  const hasUncommitted = hasUncommittedChanges(cwd);
@@ -49,14 +56,28 @@ export async function runPush({ subtrees: requestedSubtrees, branch, dryRun }) {
49
56
  }
50
57
  }
51
58
 
52
- ui.info(`Branch: ${pushBranch}`);
53
- ui.info(`Subtrees to push: ${toPush.map((p) => p.name).join(', ')}`);
59
+ const targets = toPush.map((prefix) => ({
60
+ ...prefix,
61
+ pushBranch: resolveSubtreePushBranch({
62
+ requestedBranch: branch,
63
+ configuredBranch: getConfiguredSubtreePushBranch(cwd, prefix.name),
64
+ currentBranch,
65
+ }),
66
+ }));
67
+ const uniqueBranches = [...new Set(targets.map((target) => target.pushBranch))];
68
+
69
+ if (uniqueBranches.length === 1) {
70
+ ui.info(`Branch: ${uniqueBranches[0]}`);
71
+ } else {
72
+ ui.info('Branches: per subtree');
73
+ }
74
+ ui.info(`Subtrees to push: ${targets.map((target) => `${target.name}:${target.pushBranch}`).join(', ')}`);
54
75
  ui.blank();
55
76
 
56
77
  // ── Dry run ──────────────────────────────────────────────────────────────
57
78
  if (dryRun) {
58
- const commands = toPush.map(
59
- (p) => `git subtree push --prefix="${p.name}" "${p.name}" "${pushBranch}"`
79
+ const commands = targets.map(
80
+ (target) => `git subtree push --prefix="${target.name}" "${target.name}" "${target.pushBranch}"`
60
81
  );
61
82
  ui.dryRun(commands);
62
83
  return;
@@ -66,15 +87,15 @@ export async function runPush({ subtrees: requestedSubtrees, branch, dryRun }) {
66
87
  let succeeded = 0;
67
88
  let failed = 0;
68
89
 
69
- for (const prefix of toPush) {
70
- ui.pushStart(prefix.name, pushBranch);
90
+ for (const target of targets) {
91
+ ui.pushStart(target.name, target.pushBranch);
71
92
  ui.pushSlow();
72
93
  try {
73
- git(`subtree push --prefix="${prefix.name}" "${prefix.name}" "${pushBranch}"`, { cwd });
74
- ui.success(`${prefix.name} pushed`);
94
+ git(`subtree push --prefix="${target.name}" "${target.name}" "${target.pushBranch}"`, { cwd });
95
+ ui.success(`${target.name} pushed`);
75
96
  succeeded++;
76
97
  } catch (err) {
77
- ui.error(`Failed to push ${prefix.name}: ${err.message}`);
98
+ ui.error(`Failed to push ${target.name}: ${err.message}`);
78
99
  failed++;
79
100
  }
80
101
  }
@@ -82,7 +103,11 @@ export async function runPush({ subtrees: requestedSubtrees, branch, dryRun }) {
82
103
  // ── Summary ──────────────────────────────────────────────────────────────
83
104
  ui.blank();
84
105
  if (failed === 0) {
85
- ui.success(`All ${succeeded} subtree(s) pushed to branch "${pushBranch}"`);
106
+ if (uniqueBranches.length === 1) {
107
+ ui.success(`All ${succeeded} subtree(s) pushed to branch "${uniqueBranches[0]}"`);
108
+ } else {
109
+ ui.success(`All ${succeeded} subtree(s) pushed`);
110
+ }
86
111
  } else {
87
112
  ui.warning(`${succeeded} pushed, ${failed} failed`);
88
113
  }
@@ -1,29 +1,55 @@
1
- import { getCurrentBranch, getSubtreePrefixes, getChangedSubtrees, getTrackedSubtreeBranch } from '../git.js';
1
+ import {
2
+ getConfiguredSubtreePushBranch,
3
+ getCurrentBranch,
4
+ getSubtreePrefixes,
5
+ getChangedSubtrees,
6
+ getTrackedSubtreeBranch,
7
+ resolveSubtreePushBranch,
8
+ } from '../git.js';
2
9
  import { validateInsideMonorepo } from '../validate.js';
3
10
  import * as ui from '../ui.js';
4
11
 
12
+ export function buildStatusSubtrees({
13
+ prefixes,
14
+ changedPrefixes,
15
+ currentBranch,
16
+ getTrackedBranchFn = () => null,
17
+ getConfiguredPushBranchFn = () => null,
18
+ }) {
19
+ const changedNames = new Set(changedPrefixes.map((prefix) => prefix.name));
20
+
21
+ return prefixes.map((prefix) => ({
22
+ name: prefix.name,
23
+ url: prefix.url,
24
+ upstream: getTrackedBranchFn(prefix.name) || 'unknown',
25
+ pushBranch: resolveSubtreePushBranch({
26
+ configuredBranch: getConfiguredPushBranchFn(prefix.name),
27
+ currentBranch,
28
+ }),
29
+ changed: changedNames.has(prefix.name),
30
+ }));
31
+ }
32
+
5
33
  export async function runStatus({ json }) {
6
34
  const cwd = process.cwd();
7
35
 
8
36
  validateInsideMonorepo(cwd);
9
37
 
10
- const branch = getCurrentBranch(cwd);
38
+ const currentBranch = getCurrentBranch(cwd);
11
39
  const prefixes = getSubtreePrefixes(cwd);
12
40
  const changed = getChangedSubtrees(cwd);
13
- const changedNames = new Set(changed.map((c) => c.name));
14
-
15
- const subtrees = prefixes.map((p) => ({
16
- name: p.name,
17
- url: p.url,
18
- upstream: getTrackedSubtreeBranch(cwd, p.name) || 'unknown',
19
- pushBranch: branch,
20
- changed: changedNames.has(p.name),
21
- }));
41
+ const subtrees = buildStatusSubtrees({
42
+ prefixes,
43
+ changedPrefixes: changed,
44
+ currentBranch,
45
+ getTrackedBranchFn: (prefixName) => getTrackedSubtreeBranch(cwd, prefixName),
46
+ getConfiguredPushBranchFn: (prefixName) => getConfiguredSubtreePushBranch(cwd, prefixName),
47
+ });
22
48
 
23
49
  if (json) {
24
- console.log(JSON.stringify({ pushBranch: branch, subtrees }, null, 2));
50
+ console.log(JSON.stringify({ workspaceBranch: currentBranch, subtrees }, null, 2));
25
51
  return;
26
52
  }
27
53
 
28
- ui.subtreeTable(subtrees, branch);
54
+ ui.subtreeTable(subtrees, currentBranch);
29
55
  }
package/src/git.js CHANGED
@@ -10,6 +10,10 @@ function getSubtreeBranchConfigKey(prefixName) {
10
10
  return `unirepo.subtree.${prefixName}.branch`;
11
11
  }
12
12
 
13
+ function getSubtreePushBranchConfigKey(prefixName) {
14
+ return `unirepo.subtree.${prefixName}.pushBranch`;
15
+ }
16
+
13
17
  /**
14
18
  * Execute a git command and return trimmed stdout.
15
19
  * @param {string} args - git arguments
@@ -286,6 +290,35 @@ export function setConfiguredSubtreeBranch(cwd, prefixName, branch) {
286
290
  );
287
291
  }
288
292
 
293
+ /**
294
+ * Get the push/head branch explicitly configured for a subtree in local git config.
295
+ */
296
+ export function getConfiguredSubtreePushBranch(cwd, prefixName) {
297
+ const branch = git(`config --get ${quoteShellArg(getSubtreePushBranchConfigKey(prefixName))}`, {
298
+ cwd,
299
+ silent: true,
300
+ allowFailure: true,
301
+ });
302
+ return branch || null;
303
+ }
304
+
305
+ /**
306
+ * Persist the push/head branch override for a subtree in local git config.
307
+ */
308
+ export function setConfiguredSubtreePushBranch(cwd, prefixName, branch) {
309
+ git(
310
+ `config ${quoteShellArg(getSubtreePushBranchConfigKey(prefixName))} ${quoteShellArg(branch)}`,
311
+ { cwd, silent: true }
312
+ );
313
+ }
314
+
315
+ /**
316
+ * Resolve the effective push/head branch for a subtree.
317
+ */
318
+ export function resolveSubtreePushBranch({ requestedBranch, configuredBranch, currentBranch }) {
319
+ return requestedBranch || configuredBranch || currentBranch;
320
+ }
321
+
289
322
  /**
290
323
  * Get the branch unirepo should treat as the subtree's upstream branch.
291
324
  */
package/src/templates.js CHANGED
@@ -11,12 +11,12 @@ This repository is a git-subtree monorepo.
11
11
  ## Rules
12
12
 
13
13
  - Never use regular \`git push\` to publish the monorepo itself as a deployment target.
14
- - Work in a single monorepo branch and reuse that branch name when pushing subtree branches upstream.
14
+ - Work in a single monorepo branch by default. A subtree may use a different push branch when local git config explicitly overrides it.
15
15
  - Keep changes scoped to the subtree or subtrees you intend to update.
16
16
  - Edit files inside subtree directories, not in unrelated top-level folders.
17
17
  - Prefer separate commits per subtree unless the change is tightly coupled across repos.
18
18
  - Push only subtrees that actually changed.
19
- - Keep subtree directory, remote name, and pushed branch consistent.
19
+ - Keep subtree directory, remote name, and configured push branch consistent.
20
20
  - Do not mix files from different subtrees in a subtree PR.
21
21
 
22
22
  ## CLI
@@ -34,17 +34,17 @@ unirepo pr --title "feat: ..." --body "..."
34
34
  unirepo add <repo-url> --branch <branch>
35
35
  \`\`\`
36
36
 
37
- - \`status\` shows tracked subtrees, upstream branches, the current push branch, and changed files.
38
- - \`branch <name>\` creates the local branch name you should reuse when pushing subtrees upstream.
37
+ - \`status\` shows tracked subtrees, upstream branches, the current workspace branch, each subtree's effective push branch, and changed files.
38
+ - \`branch <name>\` creates the default local branch to reuse when pushing subtrees upstream.
39
39
  - \`pull\` updates one or more tracked subtrees from upstream before or during your work. Use \`--prefix\` when you want a branch-specific pull for just one subtree.
40
40
  - \`push --dry-run\` is the safe first step before a real push.
41
41
  - \`push\` without subtree names auto-detects changed subtrees. \`push <subtree>\` pushes one subtree.
42
- - \`pr\` opens one PR per changed or explicitly selected subtree repo after those branches have been pushed.
42
+ - \`pr\` opens one PR per changed or explicitly selected subtree repo after those branches have been pushed, using each subtree's effective push branch as the default PR head.
43
43
  - \`add\` imports another repository as a subtree. Use \`--branch\` to import from a non-default upstream branch.
44
44
 
45
45
  ## Workflow
46
46
 
47
- 1. Create or reuse one branch name for the work.
47
+ 1. Create or reuse one default branch name for the work.
48
48
  CLI:
49
49
  \`\`\`bash
50
50
  unirepo branch <branch>
@@ -118,12 +118,12 @@ git subtree push --prefix=libfoo https://github.com/example/libfoo.git <branch>
118
118
  \`\`\`
119
119
 
120
120
  - If subtree directory and remote name match, the remote name often works in place of the full URL.
121
- - Reuse the same branch name across the monorepo and each upstream subtree repo.
121
+ - By default, subtrees reuse the monorepo branch name. To override one subtree, set \`git config unirepo.subtree.<subtree>.pushBranch <branch>\`.
122
122
 
123
123
  ## PRs
124
124
 
125
125
  - Open one PR per upstream subtree repo.
126
- - Use the same branch name in each upstream repo.
126
+ - By default, PR heads follow each subtree's effective push branch.
127
127
  - Target the default branch of the upstream repo unless that repo's workflow says otherwise.
128
128
  - Run \`unirepo pr\` only after the head branch exists upstream for that subtree repo.
129
129
  - Ensure each PR contains only changes from its own subtree.
package/src/ui.js CHANGED
@@ -78,7 +78,7 @@ export function subtreeTable(subtrees, currentBranch) {
78
78
  header('Monorepo Status');
79
79
  blank();
80
80
  console.log(` ${ICON.folder} Subtrees: ${chalk.bold(subtrees.length)}`);
81
- console.log(` ${ICON.rocket} Push branch: ${chalk.bold.cyan(currentBranch)}`);
81
+ console.log(` ${ICON.branch} Workspace branch: ${chalk.bold.cyan(currentBranch)}`);
82
82
  blank();
83
83
 
84
84
  if (subtrees.length === 0) {
@@ -90,7 +90,7 @@ export function subtreeTable(subtrees, currentBranch) {
90
90
  // Column widths
91
91
  const nameWidth = Math.max(12, ...subtrees.map((s) => s.name.length)) + 2;
92
92
  const upstreamWidth = Math.max(10, ...subtrees.map((s) => (s.upstream || '').length)) + 2;
93
- const pushWidth = Math.max(12, currentBranch.length) + 2;
93
+ const pushWidth = Math.max(12, ...subtrees.map((s) => (s.pushBranch || currentBranch).length)) + 2;
94
94
  const urlWidth = Math.max(10, ...subtrees.map((s) => s.url.length)) + 2;
95
95
  const headerRow =
96
96
  'Subtree'.padEnd(nameWidth) +
@@ -107,7 +107,7 @@ export function subtreeTable(subtrees, currentBranch) {
107
107
  ? chalk.yellow('● yes')
108
108
  : chalk.dim('○ no');
109
109
  const upstreamStr = chalk.dim((s.upstream || 'unknown').padEnd(upstreamWidth));
110
- const pushStr = chalk.cyan(currentBranch.padEnd(pushWidth));
110
+ const pushStr = chalk.cyan((s.pushBranch || currentBranch).padEnd(pushWidth));
111
111
  console.log(
112
112
  ` ${chalk.bold(s.name.padEnd(nameWidth))}${upstreamStr}${pushStr}${chalk.dim(s.url.padEnd(urlWidth))}${changed}`
113
113
  );
@@ -177,10 +177,10 @@ ${chalk.bold('Commands:')}
177
177
  ${chalk.green('init')} <dir> <repo-url...> Create a new monorepo from repo URLs
178
178
  ${chalk.green('add')} <repo-url> Add a repo to the current monorepo
179
179
  ${chalk.green('pull')} [subtree...] Pull subtree updates from upstream
180
- ${chalk.green('status')} Show tracked subtrees and changes
180
+ ${chalk.green('status')} Show tracked subtrees, branches, and changes
181
181
  ${chalk.green('push')} [subtree...] Push changed subtrees upstream
182
182
  ${chalk.green('pr')} [subtree...] Open PRs for changed subtree repos
183
- ${chalk.green('branch')} [name] Create a branch on all upstream repos
183
+ ${chalk.green('branch')} [name] Create or show the workspace branch
184
184
  ${chalk.green('version')} Show the installed CLI version
185
185
 
186
186
  ${chalk.bold('Global options:')}
@@ -204,14 +204,14 @@ ${chalk.bold('Status options:')}
204
204
  --json Output machine-readable JSON
205
205
 
206
206
  ${chalk.bold('Push options:')}
207
- --branch <name> Branch name for upstream push (default: current)
207
+ --branch <name> Branch name for all selected subtree pushes
208
208
  --dry-run Show commands without executing
209
209
 
210
210
  ${chalk.bold('PR options:')}
211
211
  --title <title> Shared PR title (required)
212
212
  --body <text> Shared PR description
213
213
  --base <name> Override the base branch for all selected repos
214
- --head <name> Override the head branch (default: current branch)
214
+ --head <name> Override the head branch for all selected repos
215
215
  --draft Create draft PRs
216
216
  --dry-run Show gh pr create commands without executing
217
217
 
@@ -232,9 +232,12 @@ ${chalk.bold('Examples:')}
232
232
  ${chalk.dim('# Check status')}
233
233
  npx unirepo status
234
234
 
235
- ${chalk.dim('# Create a branch (used as target when pushing all subtrees)')}
235
+ ${chalk.dim('# Create the default workspace branch for subtree pushes')}
236
236
  npx unirepo branch feature-x
237
237
 
238
+ ${chalk.dim('# Override one subtree push/PR branch via local git config')}
239
+ git config unirepo.subtree.api.pushBranch feature-api
240
+
238
241
  ${chalk.dim('# Push changes upstream')}
239
242
  npx unirepo push --dry-run
240
243