unirepo-cli 0.4.1 → 0.5.1
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 +26 -8
- package/package.json +3 -2
- package/src/commands/branch.js +26 -4
- package/src/commands/pr.js +19 -5
- package/src/commands/push.js +37 -12
- package/src/commands/status.js +39 -13
- package/src/git.js +33 -0
- package/src/templates.js +8 -8
- package/src/ui.js +11 -8
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
|
|
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
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.5.1",
|
|
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
|
}
|
package/src/commands/branch.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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();
|
package/src/commands/pr.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/src/commands/push.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
53
|
-
|
|
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 =
|
|
59
|
-
(
|
|
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
|
|
70
|
-
ui.pushStart(
|
|
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="${
|
|
74
|
-
ui.success(`${
|
|
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 ${
|
|
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
|
-
|
|
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
|
}
|
package/src/commands/status.js
CHANGED
|
@@ -1,29 +1,55 @@
|
|
|
1
|
-
import {
|
|
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
|
|
38
|
+
const currentBranch = getCurrentBranch(cwd);
|
|
11
39
|
const prefixes = getSubtreePrefixes(cwd);
|
|
12
40
|
const changed = getChangedSubtrees(cwd);
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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({
|
|
50
|
+
console.log(JSON.stringify({ workspaceBranch: currentBranch, subtrees }, null, 2));
|
|
25
51
|
return;
|
|
26
52
|
}
|
|
27
53
|
|
|
28
|
-
ui.subtreeTable(subtrees,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|