oh-my-space 0.6.0 → 0.7.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 +17 -10
- package/dist/build-info.json +1 -0
- package/dist/oms.js +175 -45
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -49,10 +49,13 @@ Run `oms init` to scaffold a starter `oms.yaml`, or create one by hand in your p
|
|
|
49
49
|
# yaml-language-server: $schema=https://raw.githubusercontent.com/divlook/oh-my-space/main/oms.schema.json
|
|
50
50
|
repos:
|
|
51
51
|
- alias: api
|
|
52
|
-
|
|
52
|
+
remotes:
|
|
53
|
+
origin: git@github.com:example/api.git
|
|
53
54
|
branch: main
|
|
54
55
|
- alias: web
|
|
55
|
-
|
|
56
|
+
remotes:
|
|
57
|
+
origin: git@github.com:example/web.git
|
|
58
|
+
upstream: git@github.com:upstream/web.git
|
|
56
59
|
```
|
|
57
60
|
|
|
58
61
|
Sync and work:
|
|
@@ -66,6 +69,7 @@ oms sync --all # sync every registered source repo
|
|
|
66
69
|
oms status # branch / pointer / dirty / ahead-behind per submodule
|
|
67
70
|
oms checkout <alias> <branch> # switch (or create a brand-new local branch)
|
|
68
71
|
oms push <alias>... # push the branch (lazy remote) + stage the pointer
|
|
72
|
+
oms push <alias> --remote upstream # target a specific remote (repeatable; defaults to origin)
|
|
69
73
|
oms unsync <alias>... # deinitialize and remove a submodule
|
|
70
74
|
```
|
|
71
75
|
|
|
@@ -83,7 +87,7 @@ git add oms/api && git commit # record the pointer in your project history
|
|
|
83
87
|
For an alias `api` checked out on `main`:
|
|
84
88
|
|
|
85
89
|
```
|
|
86
|
-
.gitmodules # registers oms/api -> url, branch
|
|
90
|
+
.gitmodules # registers oms/api -> origin url, branch
|
|
87
91
|
oms/
|
|
88
92
|
└── api/ # git submodule (a normal working tree, on a branch)
|
|
89
93
|
```
|
|
@@ -92,7 +96,7 @@ oms/
|
|
|
92
96
|
|
|
93
97
|
## Managing source repositories
|
|
94
98
|
|
|
95
|
-
`oms.yaml` declares each source repo with `alias`, `
|
|
99
|
+
`oms.yaml` declares each source repo with `alias`, a `remotes` mapping (which must include `origin`), and optional `branch` (the baseline).
|
|
96
100
|
|
|
97
101
|
| Command | Runs in | Does | Notes |
|
|
98
102
|
| --- | --- | --- | --- |
|
|
@@ -101,9 +105,9 @@ oms/
|
|
|
101
105
|
| `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. |
|
|
102
106
|
| `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. |
|
|
103
107
|
| `oms checkout <alias> <branch>` | workspace root | `git switch` to the branch; creates it locally if it exists nowhere yet (no remote required). | `--from <ref>` sets the start point for a new branch. |
|
|
104
|
-
| `oms fetch ...` | workspace root | `git fetch
|
|
105
|
-
| `oms pull ...` | workspace root | `git pull --ff-only
|
|
106
|
-
| `oms push <alias>...` | workspace root | `git push
|
|
108
|
+
| `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
|
+
| `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
|
+
| `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. |
|
|
107
111
|
| `oms unsync <alias>` / `--all` | workspace root | `git submodule deinit` + `git rm` for the alias; drops an empty `.gitmodules`. | Keeps the `oms.yaml` entry. Use `--force` to discard uncommitted changes. |
|
|
108
112
|
|
|
109
113
|
## `oms.yaml` format
|
|
@@ -112,17 +116,20 @@ oms/
|
|
|
112
116
|
# yaml-language-server: $schema=https://raw.githubusercontent.com/divlook/oh-my-space/main/oms.schema.json
|
|
113
117
|
repos:
|
|
114
118
|
- alias: service-a
|
|
115
|
-
|
|
119
|
+
remotes:
|
|
120
|
+
origin: git@github.com:example/service-a.git
|
|
116
121
|
branch: main
|
|
117
122
|
- alias: docs
|
|
118
|
-
|
|
123
|
+
remotes:
|
|
124
|
+
origin: https://github.com/example/docs.git
|
|
125
|
+
upstream: https://github.com/upstream/docs.git
|
|
119
126
|
```
|
|
120
127
|
|
|
121
128
|
Rules:
|
|
122
129
|
|
|
123
130
|
- `repos` must be a non-empty array.
|
|
124
131
|
- `alias` must be unique and match `/^[a-z0-9][a-z0-9-]*$/`.
|
|
125
|
-
- `
|
|
132
|
+
- `remotes` is required and must include an `origin` entry; each value is a clonable git URL. `origin` becomes the submodule's primary remote, and additional remotes are configured on `oms sync`.
|
|
126
133
|
- `branch` is optional; when omitted the remote's default branch is used as the baseline.
|
|
127
134
|
|
|
128
135
|
JSON schema: [`oms.schema.json`](./oms.schema.json) (also reachable at `https://raw.githubusercontent.com/divlook/oh-my-space/main/oms.schema.json` for YAML LSPs).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"commit":"23dd80c9eef2a1ba62b193a9497f564dbe116739"}
|
package/dist/oms.js
CHANGED
|
@@ -4,11 +4,12 @@ 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 } from "@clack/prompts";
|
|
7
|
+
import { cancel, isCancel, log, multiselect, select } from "@clack/prompts";
|
|
8
8
|
import { parse as parseYaml } from "yaml";
|
|
9
9
|
const ALIAS_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
10
|
+
const REMOTE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
10
11
|
const ALLOWED_TOP_KEYS = new Set(["repos"]);
|
|
11
|
-
const ALLOWED_ITEM_KEYS = new Set(["alias", "
|
|
12
|
+
const ALLOWED_ITEM_KEYS = new Set(["alias", "remotes", "branch"]);
|
|
12
13
|
const MANIFEST_FILENAME = "oms.yaml";
|
|
13
14
|
const DATA_DIRNAME = "oms";
|
|
14
15
|
const GITIGNORE_ENTRY = `${DATA_DIRNAME}/`;
|
|
@@ -17,6 +18,9 @@ const LEGACY_MANIFEST = "sources.yaml";
|
|
|
17
18
|
const LEGACY_DATA_DIRNAME = "sources";
|
|
18
19
|
const RENAME_MIGRATION_DOC = "docs/migrations/0.3.x-to-0.4.0.md";
|
|
19
20
|
const WORKTREE_MIGRATION_DOC = "docs/migrations/0.5.x-to-0.6.0.md";
|
|
21
|
+
const REMOTES_MIGRATION_DOC = "docs/migrations/0.6.x-to-0.7.0.md";
|
|
22
|
+
/** GitHub blob base for the clickable doc permalinks shown in CLI messages. */
|
|
23
|
+
const DOCS_REPO_BLOB_BASE = "https://github.com/divlook/oh-my-space/blob";
|
|
20
24
|
const MIN_GIT_MAJOR = 2;
|
|
21
25
|
const MIN_GIT_MINOR = 40;
|
|
22
26
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -47,6 +51,10 @@ function validateSources(data) {
|
|
|
47
51
|
throw new Error(`${MANIFEST_FILENAME}: ${where} must be a mapping`);
|
|
48
52
|
}
|
|
49
53
|
const r = item;
|
|
54
|
+
// Friendlier than "unknown key url": the 0.7.0 manifest replaced url with a remotes mapping.
|
|
55
|
+
if ("url" in r && !("remotes" in r)) {
|
|
56
|
+
throw new Error(`${MANIFEST_FILENAME}: ${where}.url is no longer supported; use a "remotes" mapping with an "origin" entry. See ${docUrl(REMOTES_MIGRATION_DOC)}`);
|
|
57
|
+
}
|
|
50
58
|
for (const key of Object.keys(r)) {
|
|
51
59
|
if (!ALLOWED_ITEM_KEYS.has(key)) {
|
|
52
60
|
throw new Error(`${MANIFEST_FILENAME}: ${where} has unknown key "${key}"`);
|
|
@@ -62,8 +70,25 @@ function validateSources(data) {
|
|
|
62
70
|
throw new Error(`${MANIFEST_FILENAME}: duplicate alias "${r.alias}"`);
|
|
63
71
|
}
|
|
64
72
|
seen.add(r.alias);
|
|
65
|
-
if (typeof r.
|
|
66
|
-
throw new Error(`${MANIFEST_FILENAME}: ${where} missing required "
|
|
73
|
+
if (!r.remotes || typeof r.remotes !== "object" || Array.isArray(r.remotes)) {
|
|
74
|
+
throw new Error(`${MANIFEST_FILENAME}: ${where} missing required "remotes" mapping`);
|
|
75
|
+
}
|
|
76
|
+
const remoteEntries = Object.entries(r.remotes);
|
|
77
|
+
if (remoteEntries.length === 0) {
|
|
78
|
+
throw new Error(`${MANIFEST_FILENAME}: ${where}.remotes must have at least one remote`);
|
|
79
|
+
}
|
|
80
|
+
const remotes = {};
|
|
81
|
+
for (const [name, url] of remoteEntries) {
|
|
82
|
+
if (!REMOTE_NAME_PATTERN.test(name)) {
|
|
83
|
+
throw new Error(`${MANIFEST_FILENAME}: ${where}.remotes name "${name}" must match ${REMOTE_NAME_PATTERN}`);
|
|
84
|
+
}
|
|
85
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
86
|
+
throw new Error(`${MANIFEST_FILENAME}: ${where}.remotes.${name} must be a non-empty string URL`);
|
|
87
|
+
}
|
|
88
|
+
remotes[name] = url;
|
|
89
|
+
}
|
|
90
|
+
if (!remotes.origin) {
|
|
91
|
+
throw new Error(`${MANIFEST_FILENAME}: ${where}.remotes must include an "origin" entry`);
|
|
67
92
|
}
|
|
68
93
|
let branch;
|
|
69
94
|
if (r.branch !== undefined) {
|
|
@@ -72,19 +97,27 @@ function validateSources(data) {
|
|
|
72
97
|
}
|
|
73
98
|
branch = r.branch;
|
|
74
99
|
}
|
|
75
|
-
validated.push({ alias: r.alias,
|
|
100
|
+
validated.push({ alias: r.alias, remotes, branch });
|
|
76
101
|
});
|
|
77
102
|
return validated;
|
|
78
103
|
}
|
|
79
104
|
function pad(s, width) {
|
|
80
105
|
return s + " ".repeat(Math.max(0, width - s.length));
|
|
81
106
|
}
|
|
107
|
+
/** Names of a repo's non-origin remotes, in declared order (origin is shown via its URL column). */
|
|
108
|
+
function extraRemoteNames(repo) {
|
|
109
|
+
return Object.keys(repo.remotes).filter((name) => name !== "origin");
|
|
110
|
+
}
|
|
82
111
|
function printList(repos) {
|
|
112
|
+
const extras = (r) => {
|
|
113
|
+
const names = extraRemoteNames(r);
|
|
114
|
+
return names.length > 0 ? ` (+${names.join(",")})` : "";
|
|
115
|
+
};
|
|
83
116
|
const aliasW = Math.max("ALIAS".length, ...repos.map((r) => r.alias.length));
|
|
84
|
-
const urlW = Math.max("
|
|
85
|
-
console.log(dim(`${pad("ALIAS", aliasW)} ${pad("
|
|
117
|
+
const urlW = Math.max("ORIGIN".length, ...repos.map((r) => (r.remotes.origin + extras(r)).length));
|
|
118
|
+
console.log(dim(`${pad("ALIAS", aliasW)} ${pad("ORIGIN", urlW)} BRANCH`));
|
|
86
119
|
for (const r of repos) {
|
|
87
|
-
console.log(`${pad(r.alias, aliasW)} ${pad(r.
|
|
120
|
+
console.log(`${pad(r.alias, aliasW)} ${pad(r.remotes.origin + extras(r), urlW)} ${r.branch ?? ""}`);
|
|
88
121
|
}
|
|
89
122
|
}
|
|
90
123
|
async function selectInteractive(repos, actionLabel) {
|
|
@@ -105,6 +138,56 @@ async function selectInteractive(repos, actionLabel) {
|
|
|
105
138
|
.map((alias) => repos.find((r) => r.alias === alias))
|
|
106
139
|
.filter((r) => r !== undefined);
|
|
107
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Decide which remote(s) a fetch/pull/push targets for one repo. Honors an explicit --remote list,
|
|
143
|
+
* otherwise prompts interactively on a TTY (origin preselected) and falls back to origin off-TTY.
|
|
144
|
+
* pull is restricted to a single remote since --ff-only can advance to at most one. Returns the
|
|
145
|
+
* resolved remote names, or null when the request is invalid or the prompt was cancelled.
|
|
146
|
+
*/
|
|
147
|
+
async function resolveRemotes(repo, requested, command) {
|
|
148
|
+
const declared = Object.keys(repo.remotes);
|
|
149
|
+
if (requested && requested.length > 0) {
|
|
150
|
+
const unique = uniqueAliases(requested);
|
|
151
|
+
const unknown = unique.filter((name) => !declared.includes(name));
|
|
152
|
+
if (unknown.length > 0) {
|
|
153
|
+
log.error(`${repo.alias}: unknown remote(s): ${unknown.join(", ")}. Declared: ${declared.join(", ")}.`);
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
if (command === "pull" && unique.length > 1) {
|
|
157
|
+
log.error(`${repo.alias}: pull targets a single remote (git pull --ff-only can advance only one).`);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return unique;
|
|
161
|
+
}
|
|
162
|
+
// No explicit remote: a lone origin needs no prompt, and a non-interactive shell defaults to origin.
|
|
163
|
+
if (declared.length === 1)
|
|
164
|
+
return declared;
|
|
165
|
+
if (!process.stdin.isTTY)
|
|
166
|
+
return ["origin"];
|
|
167
|
+
if (command === "pull") {
|
|
168
|
+
const choice = await select({
|
|
169
|
+
message: `${repo.alias}: select a remote to ${command}`,
|
|
170
|
+
options: declared.map((name) => ({ value: name, label: name, hint: repo.remotes[name] })),
|
|
171
|
+
initialValue: "origin",
|
|
172
|
+
});
|
|
173
|
+
if (isCancel(choice)) {
|
|
174
|
+
cancel("Cancelled.");
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
return [choice];
|
|
178
|
+
}
|
|
179
|
+
const choice = await multiselect({
|
|
180
|
+
message: `${repo.alias}: select remote(s) to ${command} (space to toggle, enter to confirm)`,
|
|
181
|
+
options: declared.map((name) => ({ value: name, label: name, hint: repo.remotes[name] })),
|
|
182
|
+
initialValues: ["origin"],
|
|
183
|
+
required: true,
|
|
184
|
+
});
|
|
185
|
+
if (isCancel(choice)) {
|
|
186
|
+
cancel("Cancelled.");
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
return choice;
|
|
190
|
+
}
|
|
108
191
|
function runGit(cwd, args, inheritOutput = false) {
|
|
109
192
|
const result = spawnSync("git", args, {
|
|
110
193
|
cwd,
|
|
@@ -232,6 +315,21 @@ function attachBranch(repoRoot, alias, branch) {
|
|
|
232
315
|
runSub(repoRoot, alias, ["branch", "--set-upstream-to", `origin/${branch}`, branch]);
|
|
233
316
|
}
|
|
234
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Reconcile the submodule's git remotes with the declared `remotes` map: add missing remotes and
|
|
320
|
+
* update URLs that drifted. Non-destructive — remotes no longer in oms.yaml are left untouched.
|
|
321
|
+
*/
|
|
322
|
+
function ensureRemotes(repoRoot, alias, remotes) {
|
|
323
|
+
for (const [name, url] of Object.entries(remotes)) {
|
|
324
|
+
const existing = runSub(repoRoot, alias, ["remote", "get-url", name]);
|
|
325
|
+
if (!existing.success) {
|
|
326
|
+
runSub(repoRoot, alias, ["remote", "add", name, url]);
|
|
327
|
+
}
|
|
328
|
+
else if (existing.stdout.trim() !== url) {
|
|
329
|
+
runSub(repoRoot, alias, ["remote", "set-url", name, url]);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
235
333
|
/** Stage the submodule gitlink in the parent so a moved pointer shows up and is ready to commit. */
|
|
236
334
|
function stagePointer(repoRoot, alias) {
|
|
237
335
|
runGit(repoRoot, ["add", "--", submodulePath(alias)]);
|
|
@@ -245,7 +343,7 @@ function emitLegacyRenameMessage(dir, found) {
|
|
|
245
343
|
.join(" and ");
|
|
246
344
|
log.error(`detected legacy ${artifacts} at ${dir}.\n` +
|
|
247
345
|
` oh-my-space 0.4.0 renamed the manifest to ${MANIFEST_FILENAME} and the data directory to ${DATA_DIRNAME}/.\n` +
|
|
248
|
-
` See ${RENAME_MIGRATION_DOC} for the manual steps. Aborting to avoid destructive change.`);
|
|
346
|
+
` See ${docUrl(RENAME_MIGRATION_DOC)} for the manual steps. Aborting to avoid destructive change.`);
|
|
249
347
|
}
|
|
250
348
|
/** Block when the active oms.yaml workspace still has 0.3.x rename artifacts at its root. */
|
|
251
349
|
function abortOnLegacyRenameAt(repoRoot) {
|
|
@@ -283,7 +381,7 @@ function abortOnLegacyWorktree(repoRoot, repos) {
|
|
|
283
381
|
if (!stale)
|
|
284
382
|
return false;
|
|
285
383
|
log.error(`detected a legacy bare clone at ${DATA_DIRNAME}/${stale.alias}/.bare. oh-my-space 0.6.0 manages sources as git submodules.\n` +
|
|
286
|
-
` See ${WORKTREE_MIGRATION_DOC} for the manual steps. Aborting to avoid destructive change.`);
|
|
384
|
+
` See ${docUrl(WORKTREE_MIGRATION_DOC)} for the manual steps. Aborting to avoid destructive change.`);
|
|
287
385
|
return true;
|
|
288
386
|
}
|
|
289
387
|
/** Submodules live inside the parent's history, so oms/ must not be gitignored. Strip a managed entry. */
|
|
@@ -347,11 +445,11 @@ function syncRepo(repo, repoRoot) {
|
|
|
347
445
|
"ls-remote",
|
|
348
446
|
"--exit-code",
|
|
349
447
|
"--heads",
|
|
350
|
-
repo.
|
|
448
|
+
repo.remotes.origin,
|
|
351
449
|
repo.branch,
|
|
352
450
|
]);
|
|
353
451
|
if (lsRemote.exitCode === 2) {
|
|
354
|
-
log.error(`${alias}: branch "${repo.branch}" not found on ${repo.
|
|
452
|
+
log.error(`${alias}: branch "${repo.branch}" not found on ${repo.remotes.origin}. Push the branch upstream or fix the alias, then retry.`);
|
|
355
453
|
return "failed";
|
|
356
454
|
}
|
|
357
455
|
if (!lsRemote.success && lsRemote.exitCode !== 2) {
|
|
@@ -364,14 +462,15 @@ function syncRepo(repo, repoRoot) {
|
|
|
364
462
|
if (!existsSync(gitmodules) && runGit(repoRoot, ["cat-file", "-e", "HEAD:.gitmodules"]).success) {
|
|
365
463
|
writeFileSync(gitmodules, "");
|
|
366
464
|
}
|
|
367
|
-
log.step(`${alias}: git submodule add${repo.branch ? ` -b ${repo.branch}` : ""} ${repo.
|
|
368
|
-
const args = ["submodule", "add", ...(repo.branch ? ["-b", repo.branch] : []), "--", repo.
|
|
465
|
+
log.step(`${alias}: git submodule add${repo.branch ? ` -b ${repo.branch}` : ""} ${repo.remotes.origin} ${path}`);
|
|
466
|
+
const args = ["submodule", "add", ...(repo.branch ? ["-b", repo.branch] : []), "--", repo.remotes.origin, path];
|
|
369
467
|
const add = runGit(repoRoot, args, true);
|
|
370
468
|
if (!add.success) {
|
|
371
469
|
log.error(`${alias}: git submodule add failed (exit ${add.exitCode})`);
|
|
372
470
|
cleanupFailedAdd(repoRoot, alias);
|
|
373
471
|
return "failed";
|
|
374
472
|
}
|
|
473
|
+
ensureRemotes(repoRoot, alias, repo.remotes);
|
|
375
474
|
const branch = repo.branch ?? currentBranch(aliasDir(repoRoot, alias));
|
|
376
475
|
if (branch)
|
|
377
476
|
attachBranch(repoRoot, alias, branch);
|
|
@@ -385,12 +484,14 @@ function syncRepo(repo, repoRoot) {
|
|
|
385
484
|
log.error(`${alias}: git submodule update --init failed (exit ${upd.exitCode})`);
|
|
386
485
|
return "failed";
|
|
387
486
|
}
|
|
487
|
+
ensureRemotes(repoRoot, alias, repo.remotes);
|
|
388
488
|
const branch = gitmodulesBranch(repoRoot, alias) ?? repo.branch;
|
|
389
489
|
if (branch)
|
|
390
490
|
attachBranch(repoRoot, alias, branch);
|
|
391
491
|
log.success(`${alias}: initialized${branch ? ` (branch=${branch})` : ""}`);
|
|
392
492
|
return "added";
|
|
393
493
|
}
|
|
494
|
+
ensureRemotes(repoRoot, alias, repo.remotes);
|
|
394
495
|
log.step(`${alias}: git fetch origin --prune`);
|
|
395
496
|
const fetch = runSub(repoRoot, alias, ["fetch", "origin", "--prune"], true);
|
|
396
497
|
if (!fetch.success) {
|
|
@@ -446,21 +547,23 @@ function unsyncRepo(repo, repoRoot, force) {
|
|
|
446
547
|
}
|
|
447
548
|
return "removed";
|
|
448
549
|
}
|
|
449
|
-
function fetchRepo(repo, repoRoot) {
|
|
550
|
+
function fetchRepo(repo, repoRoot, remotes) {
|
|
450
551
|
if (!submoduleInitialized(repoRoot, repo.alias)) {
|
|
451
552
|
log.error(`${repo.alias}: not synced. Run "oms sync ${repo.alias}" first.`);
|
|
452
553
|
return "failed";
|
|
453
554
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
555
|
+
for (const remote of remotes) {
|
|
556
|
+
log.step(`${repo.alias}: git fetch ${remote} --prune`);
|
|
557
|
+
const r = runSub(repoRoot, repo.alias, ["fetch", remote, "--prune"], true);
|
|
558
|
+
if (!r.success) {
|
|
559
|
+
log.error(`${repo.alias}: fetch ${remote} failed (exit ${r.exitCode})`);
|
|
560
|
+
return "failed";
|
|
561
|
+
}
|
|
459
562
|
}
|
|
460
|
-
log.
|
|
461
|
-
return "
|
|
563
|
+
log.success(`${repo.alias}: fetched (${remotes.join(", ")})`);
|
|
564
|
+
return "fetched";
|
|
462
565
|
}
|
|
463
|
-
function pullRepo(repo, repoRoot) {
|
|
566
|
+
function pullRepo(repo, repoRoot, remote) {
|
|
464
567
|
if (!submoduleInitialized(repoRoot, repo.alias)) {
|
|
465
568
|
log.error(`${repo.alias}: not synced. Run "oms sync ${repo.alias}" first.`);
|
|
466
569
|
return "failed";
|
|
@@ -470,17 +573,17 @@ function pullRepo(repo, repoRoot) {
|
|
|
470
573
|
log.error(`${repo.alias}: detached HEAD. Run "oms checkout ${repo.alias} <branch>" before pulling.`);
|
|
471
574
|
return "failed";
|
|
472
575
|
}
|
|
473
|
-
log.step(`${repo.alias}/${branch}: git pull --ff-only`);
|
|
474
|
-
const r = runSub(repoRoot, repo.alias, ["pull", "--ff-only"], true);
|
|
576
|
+
log.step(`${repo.alias}/${branch}: git pull --ff-only ${remote} ${branch}`);
|
|
577
|
+
const r = runSub(repoRoot, repo.alias, ["pull", "--ff-only", remote, branch], true);
|
|
475
578
|
if (r.success) {
|
|
476
579
|
stagePointer(repoRoot, repo.alias);
|
|
477
|
-
log.success(`${repo.alias}/${branch}: pulled`);
|
|
580
|
+
log.success(`${repo.alias}/${branch}: pulled from ${remote}`);
|
|
478
581
|
return "pulled";
|
|
479
582
|
}
|
|
480
|
-
log.error(`${repo.alias}/${branch}: pull failed (exit ${r.exitCode})`);
|
|
583
|
+
log.error(`${repo.alias}/${branch}: pull from ${remote} failed (exit ${r.exitCode})`);
|
|
481
584
|
return "failed";
|
|
482
585
|
}
|
|
483
|
-
function pushRepo(repo, repoRoot, commit) {
|
|
586
|
+
function pushRepo(repo, repoRoot, remotes, commit) {
|
|
484
587
|
if (!submoduleInitialized(repoRoot, repo.alias)) {
|
|
485
588
|
log.error(`${repo.alias}: not synced. Run "oms sync ${repo.alias}" first.`);
|
|
486
589
|
return "failed";
|
|
@@ -490,18 +593,22 @@ function pushRepo(repo, repoRoot, commit) {
|
|
|
490
593
|
log.error(`${repo.alias}: detached HEAD. Run "oms checkout ${repo.alias} <branch>" before pushing.`);
|
|
491
594
|
return "failed";
|
|
492
595
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
log.
|
|
497
|
-
|
|
596
|
+
for (const remote of remotes) {
|
|
597
|
+
// Only origin sets upstream — repointing @{u} to a fork would skew "oms status" ahead/behind.
|
|
598
|
+
const args = remote === "origin" ? ["push", "-u", "origin", branch] : ["push", remote, branch];
|
|
599
|
+
log.step(`${repo.alias}/${branch}: git ${args.join(" ")}`);
|
|
600
|
+
const r = runSub(repoRoot, repo.alias, args, true);
|
|
601
|
+
if (!r.success) {
|
|
602
|
+
log.error(`${repo.alias}/${branch}: push to ${remote} failed (exit ${r.exitCode})`);
|
|
603
|
+
return "failed";
|
|
604
|
+
}
|
|
498
605
|
}
|
|
499
606
|
stagePointer(repoRoot, repo.alias);
|
|
500
607
|
if (commit) {
|
|
501
608
|
const sha = shortSha(aliasDir(repoRoot, repo.alias));
|
|
502
609
|
runGit(repoRoot, ["commit", "-m", `oms: bump ${repo.alias} to ${sha}`, "--", submodulePath(repo.alias)]);
|
|
503
610
|
}
|
|
504
|
-
log.success(`${repo.alias}/${branch}: pushed${commit ? " and recorded pointer" : ""}`);
|
|
611
|
+
log.success(`${repo.alias}/${branch}: pushed to ${remotes.join(", ")}${commit ? " and recorded pointer" : ""}`);
|
|
505
612
|
return "pushed";
|
|
506
613
|
}
|
|
507
614
|
function printSummary(results) {
|
|
@@ -616,13 +723,20 @@ async function runManage(command, aliases, options) {
|
|
|
616
723
|
const picked = await selectRepos(repos, aliases, options, command);
|
|
617
724
|
if (!picked || picked.length === 0)
|
|
618
725
|
return 1;
|
|
619
|
-
const results =
|
|
726
|
+
const results = [];
|
|
727
|
+
for (const repo of picked) {
|
|
728
|
+
const remotes = await resolveRemotes(repo, options.remote, command);
|
|
729
|
+
if (!remotes || remotes.length === 0) {
|
|
730
|
+
results.push("failed");
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
620
733
|
if (command === "fetch")
|
|
621
|
-
|
|
622
|
-
if (command === "pull")
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
734
|
+
results.push(fetchRepo(repo, repoRoot, remotes));
|
|
735
|
+
else if (command === "pull")
|
|
736
|
+
results.push(pullRepo(repo, repoRoot, remotes[0]));
|
|
737
|
+
else
|
|
738
|
+
results.push(pushRepo(repo, repoRoot, remotes, options.commit ?? false));
|
|
739
|
+
}
|
|
626
740
|
if (results.length > 1 || options.all)
|
|
627
741
|
printSummary(results);
|
|
628
742
|
return exitFromResults(results);
|
|
@@ -763,7 +877,9 @@ async function runStatus(aliases, options) {
|
|
|
763
877
|
const INIT_TEMPLATE = `# yaml-language-server: $schema=https://raw.githubusercontent.com/divlook/oh-my-space/main/oms.schema.json
|
|
764
878
|
repos:
|
|
765
879
|
- alias: example
|
|
766
|
-
|
|
880
|
+
remotes:
|
|
881
|
+
origin: git@github.com:example/repo.git
|
|
882
|
+
# upstream: git@github.com:upstream/repo.git
|
|
767
883
|
branch: main
|
|
768
884
|
`;
|
|
769
885
|
/** 현재 디렉터리에 기본 oms.yaml 템플릿을 생성합니다. */
|
|
@@ -780,7 +896,7 @@ async function runInit(options) {
|
|
|
780
896
|
if (!isGitRepo(process.cwd())) {
|
|
781
897
|
log.info(`oms manages sources as git submodules; run "git init" here if this is not a git repo yet.`);
|
|
782
898
|
}
|
|
783
|
-
log.info(`edit alias/
|
|
899
|
+
log.info(`edit alias/remotes/branch, then run "oms sync".`);
|
|
784
900
|
return 0;
|
|
785
901
|
}
|
|
786
902
|
async function runDoctor() {
|
|
@@ -857,6 +973,16 @@ function readPackageVersion() {
|
|
|
857
973
|
const pkg = readJson(join(packageRoot, "package.json"));
|
|
858
974
|
return pkg?.version ?? "0.0.0";
|
|
859
975
|
}
|
|
976
|
+
/** The commit baked in at build time, or null when unavailable (dev/no-git build). */
|
|
977
|
+
function readBuildCommit() {
|
|
978
|
+
const info = readJson(join(dirname(fileURLToPath(import.meta.url)), "build-info.json"));
|
|
979
|
+
return info?.commit ?? null;
|
|
980
|
+
}
|
|
981
|
+
/** Clickable GitHub permalink for a repo doc, pinned to the build commit; falls back to the version tag. */
|
|
982
|
+
function docUrl(relPath) {
|
|
983
|
+
const ref = readBuildCommit() ?? `v${readPackageVersion()}`;
|
|
984
|
+
return `${DOCS_REPO_BLOB_BASE}/${ref}/${relPath}`;
|
|
985
|
+
}
|
|
860
986
|
async function exitWith(action) {
|
|
861
987
|
try {
|
|
862
988
|
process.exit(await action);
|
|
@@ -929,29 +1055,33 @@ program
|
|
|
929
1055
|
.action(async (alias, branch, options) => {
|
|
930
1056
|
await exitWith(runCheckout(alias, branch, options));
|
|
931
1057
|
});
|
|
1058
|
+
const collectRemote = (value, acc) => [...acc, value];
|
|
932
1059
|
program
|
|
933
1060
|
.command("fetch")
|
|
934
|
-
.description("Run git fetch
|
|
1061
|
+
.description("Run git fetch <remote> --prune in each submodule (defaults to origin).")
|
|
935
1062
|
.argument("[aliases...]", "repo aliases to fetch (omit for interactive multi-select)")
|
|
936
1063
|
.option("--all", "fetch every registered source repo")
|
|
1064
|
+
.option("--remote <name>", "remote to fetch (repeatable; omit to choose interactively)", collectRemote, [])
|
|
937
1065
|
.addHelpText("after", exitHelp)
|
|
938
1066
|
.action(async (aliases, options) => {
|
|
939
1067
|
await exitWith(runManage("fetch", aliases, options));
|
|
940
1068
|
});
|
|
941
1069
|
program
|
|
942
1070
|
.command("pull")
|
|
943
|
-
.description("Run git pull --ff-only on each submodule's current branch and stage the moved pointer.")
|
|
1071
|
+
.description("Run git pull --ff-only <remote> on each submodule's current branch and stage the moved pointer (defaults to origin).")
|
|
944
1072
|
.argument("[aliases...]", "repo aliases to pull (omit for interactive multi-select)")
|
|
945
1073
|
.option("--all", "pull every registered source repo")
|
|
1074
|
+
.option("--remote <name>", "remote to pull from (single; omit to choose interactively)", collectRemote, [])
|
|
946
1075
|
.addHelpText("after", exitHelp)
|
|
947
1076
|
.action(async (aliases, options) => {
|
|
948
1077
|
await exitWith(runManage("pull", aliases, options));
|
|
949
1078
|
});
|
|
950
1079
|
program
|
|
951
1080
|
.command("push")
|
|
952
|
-
.description("Run git push
|
|
1081
|
+
.description("Run git push <remote> <branch> on each listed submodule (creating the remote branch on first push) and stage the moved pointer (defaults to origin).")
|
|
953
1082
|
.argument("<aliases...>", "repo aliases to push")
|
|
954
1083
|
.option("--commit", "also commit the pointer update in the parent repo")
|
|
1084
|
+
.option("--remote <name>", "remote to push to (repeatable; omit to choose interactively)", collectRemote, [])
|
|
955
1085
|
.addHelpText("after", exitHelp)
|
|
956
1086
|
.action(async (aliases, options) => {
|
|
957
1087
|
await exitWith(runManage("push", aliases, options));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oh-my-space",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "CLI for managing external source repositories declared in oms.yaml.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/divlook/oh-my-space",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"node": ">=20.19.0"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
|
-
"build": "tsc -p tsconfig.build.json && node -e \"require('node:fs').chmodSync('dist/oms.js', 0o755)\"",
|
|
38
|
+
"build": "tsc -p tsconfig.build.json && node -e \"require('node:fs').chmodSync('dist/oms.js', 0o755)\" && node scripts/gen-build-info.mjs",
|
|
39
39
|
"test": "npm run build && node --test tests/*.test.js",
|
|
40
40
|
"prepack": "npm test",
|
|
41
41
|
"changeset": "changeset",
|