oh-my-space 0.7.0 → 0.8.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 +9 -4
- package/dist/build-info.json +1 -1
- package/dist/oms.js +224 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Command name: `oms`
|
|
|
10
10
|
|
|
11
11
|
Git submodules give you three things for free: a **reproducible pin** (the parent records each source's exact commit), **visibility** (`git status` shows when a pointer moved), and **history** (the pointer travels with your commits). What they make awkward is day-to-day branch work. `oms` keeps the native benefits and removes the friction:
|
|
12
12
|
|
|
13
|
-
- **Create a new branch locally, with no remote precondition.** `oms
|
|
13
|
+
- **Create a new branch locally, with no remote precondition.** `oms switch <alias> <branch>` makes the branch right away even if it does not exist on the remote yet; `oms checkout <alias> <branch>` fetches origin and checks out an existing remote branch as a tracking branch. The remote branch is created lazily on your first `oms push`.
|
|
14
14
|
- **Always on a branch, never detached.** `oms sync` attaches the baseline branch at the pinned commit instead of leaving a detached HEAD.
|
|
15
15
|
- **Pointer moves stay visible.** `oms pull` / `oms push` stage the updated gitlink in the parent so you can see and commit it; `oms status` surfaces drift.
|
|
16
16
|
|
|
@@ -67,16 +67,19 @@ oms sync --list # list registered source repos
|
|
|
67
67
|
oms sync <alias>... # add/initialize submodules on their baseline branch
|
|
68
68
|
oms sync --all # sync every registered source repo
|
|
69
69
|
oms status # branch / pointer / dirty / ahead-behind per submodule
|
|
70
|
-
oms
|
|
70
|
+
oms switch <alias> <branch> # switch to a local branch, or create a brand-new one
|
|
71
|
+
oms checkout <alias> <branch> # fetch origin, then check out a remote branch (tracking)
|
|
71
72
|
oms push <alias>... # push the branch (lazy remote) + stage the pointer
|
|
72
73
|
oms push <alias> --remote upstream # target a specific remote (repeatable; defaults to origin)
|
|
73
74
|
oms unsync <alias>... # deinitialize and remove a submodule
|
|
74
75
|
```
|
|
75
76
|
|
|
77
|
+
For `oms switch` and `oms checkout`, omitting the alias and/or branch prompts you to pick one interactively (synced submodules, and local or `origin/*` branches respectively).
|
|
78
|
+
|
|
76
79
|
A typical new-branch flow:
|
|
77
80
|
|
|
78
81
|
```bash
|
|
79
|
-
oms
|
|
82
|
+
oms switch api feature/login # local branch, no remote needed
|
|
80
83
|
# ... edit, commit inside oms/api ...
|
|
81
84
|
oms push api # creates origin/feature/login and stages the pointer
|
|
82
85
|
git add oms/api && git commit # record the pointer in your project history
|
|
@@ -104,7 +107,8 @@ oms/
|
|
|
104
107
|
| `oms doctor` | project root or child path | Checks `oms.yaml`, git availability, that the workspace is a git repo, and each alias's submodule state. | Returns exit 2 if any warning is raised. |
|
|
105
108
|
| `oms sync <alias>` / `--all` | workspace root | Registers missing repos with `git submodule add`, initializes registered-but-uninitialized ones, fetches, and attaches the baseline branch. | Reproduces the recorded pointer on a fresh clone. |
|
|
106
109
|
| `oms status [alias...]` / `--all` | anywhere under root | Prints branch, pointer state (`ok`/`moved`/`uninit`), dirtiness, and ahead/behind for each submodule. | `moved` means the working commit differs from the recorded pointer — stage/commit it. |
|
|
107
|
-
| `oms
|
|
110
|
+
| `oms switch [alias] [branch]` | workspace root | `git switch` to a LOCAL branch, creating it locally if it does not exist yet (no remote required). | `--from <ref>` sets the start point for a new branch. Omit alias/branch to pick interactively (or create a new branch). |
|
|
111
|
+
| `oms checkout [alias] [branch]` | workspace root | `git fetch origin --prune`, then check out a REMOTE branch (`origin/*`) as a local tracking branch (or switch to an existing local counterpart). | Omit alias/branch to pick interactively. To create a brand-new local branch, use `oms switch`. |
|
|
108
112
|
| `oms fetch ...` | workspace root | `git fetch <remote> --prune` in each submodule. | `--remote <name>` (repeatable) picks the remote(s); omit to choose interactively, defaults to `origin`. |
|
|
109
113
|
| `oms pull ...` | workspace root | `git pull --ff-only <remote>` on each submodule's current branch, then stages the moved pointer. | Requires the submodule to be on a branch. `--remote <name>` selects a single remote (defaults to `origin`). |
|
|
110
114
|
| `oms push <alias>...` | workspace root | `git push <remote> <branch>` (creating the remote branch on first push), then stages the moved pointer. | `--remote <name>` (repeatable) picks the remote(s), defaults to `origin`; upstream is set only for `origin`. `--commit` also commits the pointer update in the parent. |
|
|
@@ -138,6 +142,7 @@ JSON schema: [`oms.schema.json`](./oms.schema.json) (also reachable at `https://
|
|
|
138
142
|
|
|
139
143
|
Detailed migration steps are organized per version under [`docs/migrations/`](./docs/migrations/).
|
|
140
144
|
|
|
145
|
+
- [0.7.x → 0.8.0](./docs/migrations/0.7.x-to-0.8.0.md) — splits `oms checkout` into `oms switch` (local branches) and `oms checkout` (remote branches)
|
|
141
146
|
- [0.5.x → 0.6.0](./docs/migrations/0.5.x-to-0.6.0.md) — switches the data model from bare clone + worktrees back to git submodules
|
|
142
147
|
- [0.3.x → 0.4.0](./docs/migrations/0.3.x-to-0.4.0.md) — renames `sources.yaml`/`sources/` to `oms.yaml`/`oms/`
|
|
143
148
|
- [0.2.x → 0.3.0](./docs/migrations/0.2.x-to-0.3.0.md) — (historical) switched submodules to bare clone + worktrees
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"commit":"
|
|
1
|
+
{"commit":"1151afc09f63460e2973421140929733dc020eda"}
|
package/dist/oms.js
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync, readFileSync, readdirSync, rmSync, rmdirSync, writeFileSync
|
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { Command } from "commander";
|
|
7
|
-
import { cancel, isCancel, log, multiselect, select } from "@clack/prompts";
|
|
7
|
+
import { cancel, isCancel, log, multiselect, select, text } from "@clack/prompts";
|
|
8
8
|
import { parse as parseYaml } from "yaml";
|
|
9
9
|
const ALIAS_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
10
10
|
const REMOTE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
@@ -138,6 +138,88 @@ async function selectInteractive(repos, actionLabel) {
|
|
|
138
138
|
.map((alias) => repos.find((r) => r.alias === alias))
|
|
139
139
|
.filter((r) => r !== undefined);
|
|
140
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Resolve a single alias for a per-repo branch command (switch/checkout). An explicit alias is
|
|
143
|
+
* validated and must be a synced submodule; when omitted, the user picks one interactively from the
|
|
144
|
+
* synced submodules. Returns null (with a clear message) on an unknown/unsynced alias, an empty set,
|
|
145
|
+
* a non-interactive shell, or cancellation.
|
|
146
|
+
*/
|
|
147
|
+
async function resolveInitializedAlias(repos, repoRoot, alias, actionLabel) {
|
|
148
|
+
if (alias) {
|
|
149
|
+
const repo = repos.find((r) => r.alias === alias);
|
|
150
|
+
if (!repo) {
|
|
151
|
+
log.error(`Unknown alias "${alias}". Use "oms sync --list" to see registered aliases.`);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
if (!submoduleInitialized(repoRoot, alias)) {
|
|
155
|
+
log.error(`${alias}: not synced. Run "oms sync ${alias}" first.`);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
return repo;
|
|
159
|
+
}
|
|
160
|
+
const initialized = repos.filter((r) => submoduleInitialized(repoRoot, r.alias));
|
|
161
|
+
if (initialized.length === 0) {
|
|
162
|
+
log.error(`No synced submodules to ${actionLabel}. Run "oms sync" first.`);
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
if (!process.stdin.isTTY) {
|
|
166
|
+
log.error(`No alias given and stdin is not a TTY. Pass an alias: "oms ${actionLabel} <alias>".`);
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
const choice = await select({
|
|
170
|
+
message: `Select a source repo to ${actionLabel}`,
|
|
171
|
+
options: initialized.map((r) => ({
|
|
172
|
+
value: r.alias,
|
|
173
|
+
label: r.alias,
|
|
174
|
+
hint: r.branch ? `branch: ${r.branch}` : undefined,
|
|
175
|
+
})),
|
|
176
|
+
});
|
|
177
|
+
if (isCancel(choice)) {
|
|
178
|
+
cancel("Cancelled.");
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
return initialized.find((r) => r.alias === choice) ?? null;
|
|
182
|
+
}
|
|
183
|
+
/** Sentinel chosen in pickBranch to create a new branch instead of selecting an existing one. */
|
|
184
|
+
const CREATE_NEW_BRANCH = "\0create-new-branch";
|
|
185
|
+
/**
|
|
186
|
+
* Prompt for a branch from the given list. When allowCreate is set, a "create new branch" option
|
|
187
|
+
* collects a name via a text prompt. Returns null (with a clear message) on a non-interactive shell,
|
|
188
|
+
* an empty list with no create option, an empty name, or cancellation.
|
|
189
|
+
*/
|
|
190
|
+
async function pickBranch(branches, message, allowCreate) {
|
|
191
|
+
if (!process.stdin.isTTY) {
|
|
192
|
+
log.error(`No branch given and stdin is not a TTY. Pass a branch name explicitly.`);
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
if (branches.length === 0 && !allowCreate) {
|
|
196
|
+
log.error(`No branches available to select.`);
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const options = [
|
|
200
|
+
...(allowCreate ? [{ value: CREATE_NEW_BRANCH, label: "+ create new branch" }] : []),
|
|
201
|
+
...branches.map((b) => ({ value: b, label: b })),
|
|
202
|
+
];
|
|
203
|
+
const choice = await select({ message, options });
|
|
204
|
+
if (isCancel(choice)) {
|
|
205
|
+
cancel("Cancelled.");
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
if (choice === CREATE_NEW_BRANCH) {
|
|
209
|
+
const name = await text({ message: "New branch name", placeholder: "feature/login" });
|
|
210
|
+
if (isCancel(name)) {
|
|
211
|
+
cancel("Cancelled.");
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
const trimmed = name.trim();
|
|
215
|
+
if (!trimmed) {
|
|
216
|
+
log.error("Branch name is empty.");
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
return trimmed;
|
|
220
|
+
}
|
|
221
|
+
return choice;
|
|
222
|
+
}
|
|
141
223
|
/**
|
|
142
224
|
* Decide which remote(s) a fetch/pull/push targets for one repo. Honors an explicit --remote list,
|
|
143
225
|
* otherwise prompts interactively on a TTY (origin preselected) and falls back to origin off-TTY.
|
|
@@ -238,10 +320,10 @@ function submodulePath(alias) {
|
|
|
238
320
|
function isGitRepo(repoRoot) {
|
|
239
321
|
return runGit(repoRoot, ["rev-parse", "--is-inside-work-tree"]).stdout.trim() === "true";
|
|
240
322
|
}
|
|
241
|
-
/**
|
|
242
|
-
function
|
|
323
|
+
/** Every submodule path registered in .gitmodules (empty when the file is absent or registers none). */
|
|
324
|
+
function registeredSubmodulePaths(repoRoot) {
|
|
243
325
|
if (!existsSync(join(repoRoot, ".gitmodules")))
|
|
244
|
-
return
|
|
326
|
+
return [];
|
|
245
327
|
const result = runGit(repoRoot, [
|
|
246
328
|
"config",
|
|
247
329
|
"--file",
|
|
@@ -250,10 +332,19 @@ function isRegisteredSubmodule(repoRoot, sourcePath) {
|
|
|
250
332
|
"^submodule\\..*\\.path$",
|
|
251
333
|
]);
|
|
252
334
|
if (!result.success)
|
|
253
|
-
return
|
|
335
|
+
return [];
|
|
254
336
|
return result.stdout
|
|
255
337
|
.split("\n")
|
|
256
|
-
.
|
|
338
|
+
.map((line) => line.trim().split(/\s+/)[1])
|
|
339
|
+
.filter((p) => Boolean(p));
|
|
340
|
+
}
|
|
341
|
+
/** True when .gitmodules registers a submodule at the given path (sources/<alias> or oms/<alias>). */
|
|
342
|
+
function isRegisteredSubmodule(repoRoot, sourcePath) {
|
|
343
|
+
return registeredSubmodulePaths(repoRoot).includes(sourcePath);
|
|
344
|
+
}
|
|
345
|
+
/** True when .gitmodules still registers at least one submodule. */
|
|
346
|
+
function hasRegisteredSubmodules(repoRoot) {
|
|
347
|
+
return registeredSubmodulePaths(repoRoot).length > 0;
|
|
257
348
|
}
|
|
258
349
|
/** A submodule is initialized when its working tree has a .git gitlink file/dir. */
|
|
259
350
|
function submoduleInitialized(repoRoot, alias) {
|
|
@@ -277,6 +368,25 @@ function localBranchExists(dir, branch) {
|
|
|
277
368
|
function remoteBranchExists(dir, branch) {
|
|
278
369
|
return runGit(dir, ["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${branch}`]).success;
|
|
279
370
|
}
|
|
371
|
+
/** Local branch short names (refs/heads), e.g. ["main", "dev"]. Empty on failure or none. */
|
|
372
|
+
function listLocalBranches(dir) {
|
|
373
|
+
const r = runGit(dir, ["for-each-ref", "--format=%(refname:short)", "refs/heads"]);
|
|
374
|
+
if (!r.success)
|
|
375
|
+
return [];
|
|
376
|
+
return r.stdout.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
377
|
+
}
|
|
378
|
+
/** Remote branch short names under origin, with the "origin/" prefix stripped and origin/HEAD excluded. */
|
|
379
|
+
function listRemoteBranches(dir) {
|
|
380
|
+
const r = runGit(dir, ["for-each-ref", "--format=%(refname:short)", "refs/remotes/origin"]);
|
|
381
|
+
if (!r.success)
|
|
382
|
+
return [];
|
|
383
|
+
return r.stdout
|
|
384
|
+
.split("\n")
|
|
385
|
+
.map((s) => s.trim())
|
|
386
|
+
.filter((s) => s.length > 0 && s !== "origin/HEAD")
|
|
387
|
+
.map((s) => (s.startsWith("origin/") ? s.slice("origin/".length) : s))
|
|
388
|
+
.filter((s) => s !== "HEAD");
|
|
389
|
+
}
|
|
280
390
|
function isDirty(dir) {
|
|
281
391
|
const r = runGit(dir, ["status", "--porcelain"]);
|
|
282
392
|
return r.success && r.stdout.trim().length > 0;
|
|
@@ -512,22 +622,34 @@ function unsyncRepo(repo, repoRoot, force) {
|
|
|
512
622
|
if (!registered && !exists)
|
|
513
623
|
return "nothing-to-remove";
|
|
514
624
|
if (!force && submoduleInitialized(repoRoot, alias) && isDirty(aliasDir(repoRoot, alias))) {
|
|
515
|
-
log.error(`${alias}: ${path} has uncommitted changes. Commit, stash, or pass --force.`);
|
|
625
|
+
log.error(`${alias}: ${path} has uncommitted or untracked changes. Commit, stash, remove them, or pass --force.`);
|
|
516
626
|
return "failed";
|
|
517
627
|
}
|
|
518
628
|
runGit(repoRoot, ["submodule", "deinit", ...(force ? ["-f"] : []), "--", path]);
|
|
519
629
|
const rm = runGit(repoRoot, ["rm", "-f", "--", path], true);
|
|
520
630
|
if (!rm.success) {
|
|
521
|
-
//
|
|
631
|
+
// git rm couldn't stage the removal (e.g. the submodule was never initialized).
|
|
522
632
|
runGit(repoRoot, ["rm", "-f", "--cached", "--", path]);
|
|
523
|
-
runGit(repoRoot, ["config", "--file", ".gitmodules", "--remove-section", `submodule.${path}`]);
|
|
524
|
-
if (existsSync(join(repoRoot, ".gitmodules")))
|
|
525
|
-
runGit(repoRoot, ["add", ".gitmodules"]);
|
|
526
633
|
}
|
|
634
|
+
// Always strip the registration explicitly: git rm's implicit .gitmodules edit is unreliable
|
|
635
|
+
// across git versions/states, and when it silently no-ops the section is orphaned for good.
|
|
636
|
+
// A missing section just makes these exit non-zero — harmless, output stays captured.
|
|
637
|
+
runGit(repoRoot, ["config", "--file", ".gitmodules", "--remove-section", `submodule.${path}`]);
|
|
638
|
+
if (existsSync(join(repoRoot, ".gitmodules")))
|
|
639
|
+
runGit(repoRoot, ["add", ".gitmodules"]);
|
|
640
|
+
// Drop the matching .git/config section too, in case deinit was skipped or failed.
|
|
641
|
+
runGit(repoRoot, ["config", "--remove-section", `submodule.${path}`]);
|
|
527
642
|
try {
|
|
528
643
|
rmSync(join(repoRoot, ".git", "modules", DATA_DIRNAME, alias), { recursive: true, force: true });
|
|
529
644
|
}
|
|
530
645
|
catch { }
|
|
646
|
+
// Drop the now-empty .git/modules/oms/ container so no stale gitdir scaffolding lingers.
|
|
647
|
+
try {
|
|
648
|
+
const modulesParent = join(repoRoot, ".git", "modules", DATA_DIRNAME);
|
|
649
|
+
if (existsSync(modulesParent) && readdirSync(modulesParent).length === 0)
|
|
650
|
+
rmdirSync(modulesParent);
|
|
651
|
+
}
|
|
652
|
+
catch { }
|
|
531
653
|
if (existsSync(aliasDir(repoRoot, alias))) {
|
|
532
654
|
rmSync(aliasDir(repoRoot, alias), { recursive: true, force: true });
|
|
533
655
|
}
|
|
@@ -538,9 +660,9 @@ function unsyncRepo(repo, repoRoot, force) {
|
|
|
538
660
|
rmdirSync(parent);
|
|
539
661
|
}
|
|
540
662
|
catch { }
|
|
541
|
-
// Drop
|
|
663
|
+
// Drop .gitmodules once it no longer registers any submodule (judged by content, not byte-emptiness).
|
|
542
664
|
const gitmodules = join(repoRoot, ".gitmodules");
|
|
543
|
-
if (existsSync(gitmodules) &&
|
|
665
|
+
if (existsSync(gitmodules) && !hasRegisteredSubmodules(repoRoot)) {
|
|
544
666
|
if (!runGit(repoRoot, ["rm", "-f", "--", ".gitmodules"]).success) {
|
|
545
667
|
rmSync(gitmodules, { force: true });
|
|
546
668
|
}
|
|
@@ -570,7 +692,7 @@ function pullRepo(repo, repoRoot, remote) {
|
|
|
570
692
|
}
|
|
571
693
|
const branch = currentBranch(aliasDir(repoRoot, repo.alias));
|
|
572
694
|
if (!branch) {
|
|
573
|
-
log.error(`${repo.alias}: detached HEAD. Run "oms
|
|
695
|
+
log.error(`${repo.alias}: detached HEAD. Run "oms switch ${repo.alias} <branch>" before pulling.`);
|
|
574
696
|
return "failed";
|
|
575
697
|
}
|
|
576
698
|
log.step(`${repo.alias}/${branch}: git pull --ff-only ${remote} ${branch}`);
|
|
@@ -590,7 +712,7 @@ function pushRepo(repo, repoRoot, remotes, commit) {
|
|
|
590
712
|
}
|
|
591
713
|
const branch = currentBranch(aliasDir(repoRoot, repo.alias));
|
|
592
714
|
if (!branch) {
|
|
593
|
-
log.error(`${repo.alias}: detached HEAD. Run "oms
|
|
715
|
+
log.error(`${repo.alias}: detached HEAD. Run "oms switch ${repo.alias} <branch>" before pushing.`);
|
|
594
716
|
return "failed";
|
|
595
717
|
}
|
|
596
718
|
for (const remote of remotes) {
|
|
@@ -764,49 +886,94 @@ async function runUnsync(aliases, options) {
|
|
|
764
886
|
});
|
|
765
887
|
if (results.length > 1 || options.all)
|
|
766
888
|
printSummary(results);
|
|
889
|
+
// Name the failed aliases so a buried failure among several isn't read as "all unsynced".
|
|
890
|
+
const failed = picked.filter((_, i) => results[i] === "failed").map((r) => r.alias);
|
|
891
|
+
if (failed.length > 0) {
|
|
892
|
+
log.error(`Not unsynced: ${failed.join(", ")}. The submodule had uncommitted or untracked changes — commit/stash/remove them, or re-run with --force.`);
|
|
893
|
+
}
|
|
767
894
|
return exitFromResults(results);
|
|
768
895
|
}
|
|
769
|
-
|
|
896
|
+
/**
|
|
897
|
+
* LOCAL branch management: switch the submodule to an existing local branch or create a new one.
|
|
898
|
+
* No remote is consulted — creating a brand-new branch needs no remote precondition and sets no
|
|
899
|
+
* upstream (that is checkout's job). Omitting alias and/or branch prompts for them interactively.
|
|
900
|
+
*/
|
|
901
|
+
async function runSwitch(alias, branch, options) {
|
|
770
902
|
const loaded = loadForSubmodules();
|
|
771
903
|
if (!loaded)
|
|
772
904
|
return 1;
|
|
773
905
|
const { repos, repoRoot } = loaded;
|
|
774
|
-
const repo = repos
|
|
775
|
-
if (!repo)
|
|
776
|
-
log.error(`Unknown alias "${alias}". Use "oms sync --list" to see registered aliases.`);
|
|
906
|
+
const repo = await resolveInitializedAlias(repos, repoRoot, alias, "switch");
|
|
907
|
+
if (!repo)
|
|
777
908
|
return 1;
|
|
909
|
+
const dir = aliasDir(repoRoot, repo.alias);
|
|
910
|
+
let target = branch;
|
|
911
|
+
if (!target) {
|
|
912
|
+
const picked = await pickBranch(listLocalBranches(dir), `${repo.alias}: select a local branch`, true);
|
|
913
|
+
if (!picked)
|
|
914
|
+
return 1;
|
|
915
|
+
target = picked;
|
|
916
|
+
}
|
|
917
|
+
if (localBranchExists(dir, target)) {
|
|
918
|
+
log.step(`${repo.alias}: git switch ${target}`);
|
|
919
|
+
const r = runSub(repoRoot, repo.alias, ["switch", target], true);
|
|
920
|
+
if (!r.success)
|
|
921
|
+
return 2;
|
|
922
|
+
log.success(`${repo.alias}: on ${target}`);
|
|
923
|
+
return 0;
|
|
778
924
|
}
|
|
779
|
-
|
|
780
|
-
|
|
925
|
+
// Brand-new local branch: no remote precondition, no upstream tracking (use "oms checkout" for that).
|
|
926
|
+
const args = ["switch", "-c", target, ...(options.from ? [options.from] : [])];
|
|
927
|
+
log.step(`${repo.alias}: git ${args.join(" ")}`);
|
|
928
|
+
const r = runSub(repoRoot, repo.alias, args, true);
|
|
929
|
+
if (!r.success)
|
|
930
|
+
return 2;
|
|
931
|
+
log.success(`${repo.alias}: created new local branch ${target}. Push it with "oms push ${repo.alias}".`);
|
|
932
|
+
return 0;
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* REMOTE branch exploration: fetch origin, then check out a remote branch as a local tracking
|
|
936
|
+
* branch (or switch to an existing local counterpart). Omitting alias and/or branch prompts for
|
|
937
|
+
* them interactively. Creating brand-new local branches is "oms switch"'s job.
|
|
938
|
+
*/
|
|
939
|
+
async function runCheckout(alias, branch) {
|
|
940
|
+
const loaded = loadForSubmodules();
|
|
941
|
+
if (!loaded)
|
|
781
942
|
return 1;
|
|
782
|
-
}
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
943
|
+
const { repos, repoRoot } = loaded;
|
|
944
|
+
const repo = await resolveInitializedAlias(repos, repoRoot, alias, "checkout");
|
|
945
|
+
if (!repo)
|
|
946
|
+
return 1;
|
|
947
|
+
const dir = aliasDir(repoRoot, repo.alias);
|
|
948
|
+
log.step(`${repo.alias}: git fetch origin --prune`);
|
|
949
|
+
const fetch = runSub(repoRoot, repo.alias, ["fetch", "origin", "--prune"], true);
|
|
950
|
+
if (!fetch.success)
|
|
951
|
+
return 2;
|
|
952
|
+
let target = branch;
|
|
953
|
+
if (!target) {
|
|
954
|
+
const picked = await pickBranch(listRemoteBranches(dir), `${repo.alias}: select a remote branch (origin/*)`, false);
|
|
955
|
+
if (!picked)
|
|
956
|
+
return 1;
|
|
957
|
+
target = picked;
|
|
958
|
+
}
|
|
959
|
+
if (localBranchExists(dir, target)) {
|
|
960
|
+
log.step(`${repo.alias}: git switch ${target}`);
|
|
961
|
+
const r = runSub(repoRoot, repo.alias, ["switch", target], true);
|
|
789
962
|
if (!r.success)
|
|
790
963
|
return 2;
|
|
791
|
-
log.success(`${alias}: on ${
|
|
964
|
+
log.success(`${repo.alias}: on ${target}`);
|
|
792
965
|
return 0;
|
|
793
966
|
}
|
|
794
|
-
if (remoteBranchExists(dir,
|
|
795
|
-
log.step(`${alias}: git switch -c ${
|
|
796
|
-
const r = runSub(repoRoot, alias, ["switch", "-c",
|
|
967
|
+
if (remoteBranchExists(dir, target)) {
|
|
968
|
+
log.step(`${repo.alias}: git switch -c ${target} origin/${target}`);
|
|
969
|
+
const r = runSub(repoRoot, repo.alias, ["switch", "-c", target, `origin/${target}`], true);
|
|
797
970
|
if (!r.success)
|
|
798
971
|
return 2;
|
|
799
|
-
log.success(`${alias}: on ${
|
|
972
|
+
log.success(`${repo.alias}: on ${target} (tracking origin/${target})`);
|
|
800
973
|
return 0;
|
|
801
974
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
log.step(`${alias}: git ${args.join(" ")}`);
|
|
805
|
-
const r = runSub(repoRoot, alias, args, true);
|
|
806
|
-
if (!r.success)
|
|
807
|
-
return 2;
|
|
808
|
-
log.success(`${alias}: created new local branch ${branch}. Push it with "oms push ${alias}".`);
|
|
809
|
-
return 0;
|
|
975
|
+
log.error(`${repo.alias}: "${target}" not found on origin. To create a new local branch, run "oms switch ${repo.alias} ${target}".`);
|
|
976
|
+
return 1;
|
|
810
977
|
}
|
|
811
978
|
/** Parse the leading status char from `git submodule status` (' ' ok, '+' moved, '-' uninit, 'U' conflict). */
|
|
812
979
|
function pinState(repoRoot, alias) {
|
|
@@ -952,7 +1119,7 @@ async function runDoctor() {
|
|
|
952
1119
|
const dir = aliasDir(repoRoot, repo.alias);
|
|
953
1120
|
const branch = currentBranch(dir);
|
|
954
1121
|
if (!branch) {
|
|
955
|
-
log.warn(`${repo.alias}: detached HEAD. Run "oms
|
|
1122
|
+
log.warn(`${repo.alias}: detached HEAD. Run "oms switch ${repo.alias} <branch>" to get on a branch.`);
|
|
956
1123
|
warnings++;
|
|
957
1124
|
}
|
|
958
1125
|
else {
|
|
@@ -998,6 +1165,7 @@ const commandNames = new Set([
|
|
|
998
1165
|
"doctor",
|
|
999
1166
|
"sync",
|
|
1000
1167
|
"status",
|
|
1168
|
+
"switch",
|
|
1001
1169
|
"checkout",
|
|
1002
1170
|
"fetch",
|
|
1003
1171
|
"pull",
|
|
@@ -1046,14 +1214,23 @@ program
|
|
|
1046
1214
|
await exitWith(runStatus(aliases, options));
|
|
1047
1215
|
});
|
|
1048
1216
|
program
|
|
1049
|
-
.command("
|
|
1050
|
-
.description("Switch a submodule to
|
|
1051
|
-
.argument("
|
|
1052
|
-
.argument("
|
|
1217
|
+
.command("switch")
|
|
1218
|
+
.description("Switch a submodule to a LOCAL branch, creating it locally if it does not exist yet (no remote required).")
|
|
1219
|
+
.argument("[alias]", "registered source alias (omit to pick interactively)")
|
|
1220
|
+
.argument("[branch]", "local branch name (omit to pick from local branches or create one)")
|
|
1053
1221
|
.option("--from <ref>", "start point for a new branch (default: current HEAD)")
|
|
1054
1222
|
.addHelpText("after", exitHelp)
|
|
1055
1223
|
.action(async (alias, branch, options) => {
|
|
1056
|
-
await exitWith(
|
|
1224
|
+
await exitWith(runSwitch(alias, branch, options));
|
|
1225
|
+
});
|
|
1226
|
+
program
|
|
1227
|
+
.command("checkout")
|
|
1228
|
+
.description("Fetch origin, then check out a REMOTE branch (origin/*) as a local tracking branch.")
|
|
1229
|
+
.argument("[alias]", "registered source alias (omit to pick interactively)")
|
|
1230
|
+
.argument("[branch]", "remote branch name (omit to pick from origin/* branches)")
|
|
1231
|
+
.addHelpText("after", exitHelp)
|
|
1232
|
+
.action(async (alias, branch) => {
|
|
1233
|
+
await exitWith(runCheckout(alias, branch));
|
|
1057
1234
|
});
|
|
1058
1235
|
const collectRemote = (value, acc) => [...acc, value];
|
|
1059
1236
|
program
|