github-update-submodule 1.0.0 → 1.2.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 +121 -49
- package/bin/github-update-submodule.js +378 -61
- package/package.json +1 -1
- package/submodule.config.example.json +10 -0
package/README.md
CHANGED
|
@@ -6,21 +6,23 @@
|
|
|
6
6
|
|
|
7
7
|
## The Problem
|
|
8
8
|
|
|
9
|
-
Git submodules
|
|
9
|
+
Git submodules store a **commit pointer** (a hash) in the parent repo. When a submodule gets new commits, the parent's pointer goes stale — GitHub still shows the old commit until someone manually updates and pushes it. With deeply nested submodules this becomes a nightmare to manage by hand.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
|
-
GitHub (parent repo) ──pins──▶ old commit
|
|
12
|
+
GitHub (parent repo) ──pins──▶ old commit ❌
|
|
13
13
|
Your local submodule latest commit ✅
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
## The Solution
|
|
17
17
|
|
|
18
|
-
One command
|
|
18
|
+
One command from any repo with submodules:
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
21
|
github-update-submodule
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
Everything is handled automatically — pull, commit, push — all the way down the tree and back up again.
|
|
25
|
+
|
|
24
26
|
---
|
|
25
27
|
|
|
26
28
|
## Installation
|
|
@@ -33,79 +35,127 @@ npm install -g github-update-submodule
|
|
|
33
35
|
|
|
34
36
|
## Usage
|
|
35
37
|
|
|
36
|
-
Navigate to your root repo in the terminal and run:
|
|
37
|
-
|
|
38
38
|
```bash
|
|
39
|
+
# Run from your root repo — pulls + commits + pushes everything
|
|
39
40
|
github-update-submodule
|
|
40
|
-
```
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
# Preview what would change without touching anything
|
|
43
|
+
github-update-submodule --dry-run
|
|
44
|
+
|
|
45
|
+
# Confirm each repo before pushing
|
|
46
|
+
github-update-submodule --interactive
|
|
47
|
+
|
|
48
|
+
# Fetch all submodules at the same time (much faster on large trees)
|
|
49
|
+
github-update-submodule --parallel
|
|
50
|
+
|
|
51
|
+
# Skip specific submodules
|
|
52
|
+
github-update-submodule --ignore frontend --ignore legacy-lib
|
|
53
|
+
|
|
54
|
+
# Local update only, no push
|
|
55
|
+
github-update-submodule --no-push
|
|
56
|
+
```
|
|
43
57
|
|
|
44
|
-
|
|
45
|
-
2. **Commit** — stage and commit the updated submodule pointers in each parent repo
|
|
46
|
-
3. **Push** — push from the innermost repos outward to the root, so GitHub is fully up to date at every level
|
|
58
|
+
---
|
|
47
59
|
|
|
48
|
-
|
|
60
|
+
## Options
|
|
49
61
|
|
|
50
62
|
| Flag | Description |
|
|
51
63
|
|---|---|
|
|
52
|
-
| `--no-push` | Pull locally only,
|
|
53
|
-
| `--
|
|
64
|
+
| `--no-push` | Pull locally only, skip commit and push |
|
|
65
|
+
| `--interactive` | Show a diff and ask yes/no before pushing each repo |
|
|
66
|
+
| `--ignore <name>` | Skip a submodule by name. Repeatable: `--ignore a --ignore b` |
|
|
67
|
+
| `--parallel` | Fetch all submodules concurrently (huge speedup on large trees) |
|
|
68
|
+
| `--dry-run` | Preview all changes — nothing is modified |
|
|
54
69
|
| `--message <m>` | Custom commit message (default: `chore: update submodule refs`) |
|
|
55
70
|
| `--branch <b>` | Default branch if not declared in `.gitmodules` (default: `main`) |
|
|
56
71
|
| `--depth <n>` | Limit recursion depth |
|
|
57
72
|
| `--verbose` | Show full git output for every operation |
|
|
58
73
|
| `--no-color` | Disable colored output |
|
|
74
|
+
| `--no-progress` | Disable the progress bar |
|
|
75
|
+
| `--make-config` | Generate a `submodule.config.json` in the current repo with all defaults, then exit |
|
|
59
76
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
```bash
|
|
63
|
-
# Standard usage — pull + commit + push everything
|
|
64
|
-
github-update-submodule
|
|
65
|
-
|
|
66
|
-
# Preview changes without modifying anything
|
|
67
|
-
github-update-submodule --dry-run
|
|
68
|
-
|
|
69
|
-
# Pull locally only, skip the push
|
|
70
|
-
github-update-submodule --no-push
|
|
77
|
+
---
|
|
71
78
|
|
|
72
|
-
|
|
73
|
-
github-update-submodule --message "ci: bump all submodule refs to latest"
|
|
79
|
+
## Config File
|
|
74
80
|
|
|
75
|
-
|
|
76
|
-
github-update-submodule /path/to/your/repo
|
|
81
|
+
Run `--make-config` once inside your repo to generate a pre-filled `submodule.config.json` with all available keys and their defaults:
|
|
77
82
|
|
|
78
|
-
|
|
79
|
-
github-update-submodule --
|
|
83
|
+
```bash
|
|
84
|
+
github-update-submodule --make-config
|
|
85
|
+
```
|
|
80
86
|
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
This creates `submodule.config.json` in the current directory and prints a description of every key. Edit the values to set your preferred defaults — CLI flags always override the config file.
|
|
88
|
+
|
|
89
|
+
Example generated file:
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"defaultBranch": "main",
|
|
93
|
+
"parallel": false,
|
|
94
|
+
"ignore": [],
|
|
95
|
+
"commitMessage": "chore: update submodule refs",
|
|
96
|
+
"interactive": false,
|
|
97
|
+
"verbose": false,
|
|
98
|
+
"color": true,
|
|
99
|
+
"progress": true
|
|
100
|
+
}
|
|
83
101
|
```
|
|
84
102
|
|
|
103
|
+
All config keys match the CLI flag names (camelCase, without `--`):
|
|
104
|
+
|
|
105
|
+
| Key | Type | Default |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `push` | boolean | `true` |
|
|
108
|
+
| `interactive` | boolean | `false` |
|
|
109
|
+
| `ignore` | string or string[] | `[]` |
|
|
110
|
+
| `parallel` | boolean | `false` |
|
|
111
|
+
| `commitMessage` | string | `"chore: update submodule refs"` |
|
|
112
|
+
| `defaultBranch` | string | `"main"` |
|
|
113
|
+
| `maxDepth` | number | unlimited |
|
|
114
|
+
| `verbose` | boolean | `false` |
|
|
115
|
+
| `color` | boolean | `true` |
|
|
116
|
+
| `progress` | boolean | `true` |
|
|
117
|
+
|
|
85
118
|
---
|
|
86
119
|
|
|
87
120
|
## How It Works
|
|
88
121
|
|
|
89
122
|
### Phase 1 — Pull
|
|
90
123
|
|
|
91
|
-
For each submodule (recursively):
|
|
124
|
+
For each submodule (recursively, depth-first):
|
|
92
125
|
|
|
93
126
|
1. Initialises any submodule that hasn't been cloned yet
|
|
94
|
-
2.
|
|
95
|
-
3. Resolves the correct branch
|
|
127
|
+
2. Fetches from `origin` (in parallel if `--parallel` is set)
|
|
128
|
+
3. Resolves the correct branch: `.gitmodules` declaration → remote HEAD → `--branch` flag
|
|
96
129
|
4. Runs `git checkout -B <branch> origin/<branch>` to hard-move to the remote tip
|
|
97
|
-
5. Stages the updated pointer in the parent
|
|
98
|
-
6.
|
|
130
|
+
5. Stages the updated pointer in the parent with `git add <path>`
|
|
131
|
+
6. Prints a clickable **GitHub compare URL** for every submodule that changed:
|
|
132
|
+
```
|
|
133
|
+
⎘ https://github.com/org/repo/compare/abc12345...def67890
|
|
134
|
+
```
|
|
135
|
+
7. Recurses into the submodule's own submodules
|
|
99
136
|
|
|
100
137
|
### Phase 2 — Commit & Push
|
|
101
138
|
|
|
102
|
-
Walks the
|
|
139
|
+
Walks the tree **innermost → outermost**:
|
|
140
|
+
|
|
141
|
+
1. For each repo with staged changes, optionally shows a `--interactive` diff prompt
|
|
142
|
+
2. Commits with the configured message
|
|
143
|
+
3. Pushes to `origin/<branch>`
|
|
144
|
+
4. Moves up to the parent and repeats
|
|
145
|
+
|
|
146
|
+
The innermost-first order guarantees that by the time GitHub receives a pointer update from a parent, the commit it points to already exists on the remote.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Progress Bar
|
|
151
|
+
|
|
152
|
+
In sequential mode (default) a live progress bar tracks the fetch phase:
|
|
103
153
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
154
|
+
```
|
|
155
|
+
[████████████░░░░░░░░░░░░░░░░] 43% (6/13) frontend
|
|
156
|
+
```
|
|
107
157
|
|
|
108
|
-
|
|
158
|
+
In `--parallel` mode the bar advances as each concurrent fetch completes.
|
|
109
159
|
|
|
110
160
|
---
|
|
111
161
|
|
|
@@ -113,27 +163,33 @@ The innermost-first order ensures that by the time GitHub receives a pointer upd
|
|
|
113
163
|
|
|
114
164
|
```
|
|
115
165
|
╔══════════════════════════════════════════╗
|
|
116
|
-
║ github-update-submodule
|
|
166
|
+
║ github-update-submodule v2.0.0 ║
|
|
117
167
|
╚══════════════════════════════════════════╝
|
|
118
168
|
|
|
119
169
|
› Repository : /projects/my-app
|
|
120
170
|
› Default branch : main
|
|
121
171
|
› Push mode : ON
|
|
172
|
+
› Interactive : OFF
|
|
173
|
+
› Parallel fetch : ON
|
|
174
|
+
› Ignoring : legacy-lib
|
|
122
175
|
|
|
123
176
|
Phase 1 — Pull all submodules to latest remote commit
|
|
124
177
|
|
|
178
|
+
Parallel fetching 12 submodules…
|
|
179
|
+
[████████████████████████████] 100% (12/12)
|
|
180
|
+
|
|
125
181
|
▸ QuantumDocsSyncer (docs/QuantumDocsSyncer)
|
|
126
|
-
› Fetching from origin…
|
|
127
182
|
› Branch: main
|
|
128
183
|
✔ Updated d11a9fce → 4a82bc91
|
|
184
|
+
⎘ https://github.com/org/QuantumDocsSyncer/compare/d11a9fce...4a82bc91
|
|
129
185
|
▸ frontend (frontend)
|
|
130
|
-
› Fetching from origin…
|
|
131
186
|
› Branch: main
|
|
132
187
|
✔ Updated fe03e5be → 9c14d7aa
|
|
188
|
+
⎘ https://github.com/org/frontend/compare/fe03e5be...9c14d7aa
|
|
133
189
|
▸ backend (backend)
|
|
134
|
-
› Fetching from origin…
|
|
135
190
|
› Branch: main
|
|
136
191
|
✔ Already up to date (b6732bc5)
|
|
192
|
+
⊘ legacy-lib (ignored)
|
|
137
193
|
|
|
138
194
|
Phase 2 — Commit & push updated refs (innermost → root)
|
|
139
195
|
|
|
@@ -148,18 +204,34 @@ Summary
|
|
|
148
204
|
· Up to date : 1
|
|
149
205
|
↑ Committed : 2
|
|
150
206
|
↑ Pushed : 2
|
|
207
|
+
⊘ Ignored : 1
|
|
151
208
|
⚠ Skipped : 0
|
|
152
209
|
✘ Failed : 0
|
|
153
|
-
Total :
|
|
210
|
+
Total : 4 (8.31s)
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Interactive Mode
|
|
216
|
+
|
|
217
|
+
With `--interactive`, the tool pauses before pushing each parent repo and shows a staged diff summary:
|
|
218
|
+
|
|
154
219
|
```
|
|
220
|
+
docs/QuantumDocsSyncer | 2 +-
|
|
221
|
+
1 file changed, 1 insertion(+), 1 deletion(-)
|
|
222
|
+
|
|
223
|
+
Push 'my-app' → origin/main? [y/N]
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Type `y` to push or anything else to skip that repo.
|
|
155
227
|
|
|
156
228
|
---
|
|
157
229
|
|
|
158
230
|
## Requirements
|
|
159
231
|
|
|
160
232
|
- **Node.js** >= 14
|
|
161
|
-
- **Git** installed and
|
|
162
|
-
-
|
|
233
|
+
- **Git** installed and in your PATH
|
|
234
|
+
- Remote authentication set up (SSH keys or credential manager) so pushes don't require a password prompt
|
|
163
235
|
|
|
164
236
|
---
|
|
165
237
|
|
|
@@ -1,69 +1,123 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* github-update-submodule
|
|
4
|
+
* github-update-submodule v2.0.0
|
|
5
|
+
*
|
|
5
6
|
* Recursively pulls all Git submodules to their latest remote commit,
|
|
6
|
-
* then commits and pushes the updated refs up every parent repo
|
|
7
|
-
*
|
|
7
|
+
* then commits and pushes the updated refs up every parent repo.
|
|
8
|
+
*
|
|
9
|
+
* New in v2: --interactive, --ignore, --parallel, progress bar,
|
|
10
|
+
* GitHub compare links, .submodulerc / submodule.config.json
|
|
8
11
|
*
|
|
9
12
|
* Usage:
|
|
10
13
|
* github-update-submodule [repo-path] [options]
|
|
11
14
|
*
|
|
12
15
|
* Options:
|
|
13
|
-
* --no-push
|
|
14
|
-
* --
|
|
15
|
-
* --
|
|
16
|
-
* --
|
|
17
|
-
* --
|
|
18
|
-
* --
|
|
19
|
-
* --
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* github-update-submodule --message "ci: bump submodules"
|
|
26
|
-
* github-update-submodule /path/to/repo --branch master
|
|
16
|
+
* --no-push Skip committing and pushing (local update only)
|
|
17
|
+
* --interactive Prompt before pushing each parent repo
|
|
18
|
+
* --ignore <n> Submodule name to skip (repeatable)
|
|
19
|
+
* --parallel Fetch all submodules concurrently
|
|
20
|
+
* --message <m> Commit message (default: "chore: update submodule refs")
|
|
21
|
+
* --dry-run Preview changes without modifying anything
|
|
22
|
+
* --branch <b> Default branch when not in .gitmodules (default: main)
|
|
23
|
+
* --depth <n> Max recursion depth (default: unlimited)
|
|
24
|
+
* --verbose Show full git output
|
|
25
|
+
* --no-color Disable colored output
|
|
26
|
+
* --no-progress Disable the progress bar
|
|
27
|
+
* --make-config Generate a submodule.config.json in the current repo and exit
|
|
27
28
|
*/
|
|
28
29
|
|
|
29
|
-
const { spawnSync } = require("child_process");
|
|
30
|
-
const path
|
|
31
|
-
const fs
|
|
30
|
+
const { spawnSync, spawn } = require("child_process");
|
|
31
|
+
const path = require("path");
|
|
32
|
+
const fs = require("fs");
|
|
33
|
+
const readline = require("readline");
|
|
34
|
+
|
|
35
|
+
// ─── Config file loader ───────────────────────────────────────────────────────
|
|
36
|
+
// Reads .submodulerc or submodule.config.json from cwd.
|
|
37
|
+
// CLI flags always override config values.
|
|
38
|
+
|
|
39
|
+
function loadConfig(repoPath) {
|
|
40
|
+
const candidates = [
|
|
41
|
+
path.join(repoPath, ".submodulerc"),
|
|
42
|
+
path.join(repoPath, "submodule.config.json"),
|
|
43
|
+
];
|
|
44
|
+
for (const f of candidates) {
|
|
45
|
+
if (fs.existsSync(f)) {
|
|
46
|
+
try {
|
|
47
|
+
const raw = fs.readFileSync(f, "utf8").trim();
|
|
48
|
+
const cfg = JSON.parse(raw);
|
|
49
|
+
return cfg;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
console.warn(`⚠ Could not parse config file ${f}: ${e.message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
32
57
|
|
|
33
58
|
// ─── CLI argument parsing ────────────────────────────────────────────────────
|
|
34
59
|
|
|
35
|
-
const
|
|
60
|
+
const cliArgs = process.argv.slice(2);
|
|
36
61
|
|
|
62
|
+
// Defaults (lowest priority)
|
|
37
63
|
const options = {
|
|
38
64
|
repoPath: process.cwd(),
|
|
39
|
-
push: true,
|
|
65
|
+
push: true,
|
|
66
|
+
interactive: false,
|
|
67
|
+
ignore: [], // array of submodule names to skip
|
|
68
|
+
parallel: false,
|
|
40
69
|
commitMessage: "chore: update submodule refs",
|
|
41
70
|
dryRun: false,
|
|
42
71
|
defaultBranch: "main",
|
|
43
72
|
maxDepth: Infinity,
|
|
44
73
|
verbose: false,
|
|
45
74
|
color: true,
|
|
75
|
+
progress: true,
|
|
46
76
|
};
|
|
47
77
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
// Collect positional repo path first so config is loaded from correct dir
|
|
79
|
+
for (let i = 0; i < cliArgs.length; i++) {
|
|
80
|
+
if (!cliArgs[i].startsWith("--")) options.repoPath = path.resolve(cliArgs[i]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Merge config file (overrides defaults, CLI will override config)
|
|
84
|
+
const cfg = loadConfig(options.repoPath);
|
|
85
|
+
if (cfg.push !== undefined) options.push = cfg.push;
|
|
86
|
+
if (cfg.interactive !== undefined) options.interactive = cfg.interactive;
|
|
87
|
+
if (cfg.ignore !== undefined) options.ignore = [].concat(cfg.ignore);
|
|
88
|
+
if (cfg.parallel !== undefined) options.parallel = cfg.parallel;
|
|
89
|
+
if (cfg.commitMessage !== undefined) options.commitMessage = cfg.commitMessage;
|
|
90
|
+
if (cfg.defaultBranch !== undefined) options.defaultBranch = cfg.defaultBranch;
|
|
91
|
+
if (cfg.maxDepth !== undefined) options.maxDepth = cfg.maxDepth;
|
|
92
|
+
if (cfg.verbose !== undefined) options.verbose = cfg.verbose;
|
|
93
|
+
if (cfg.color !== undefined) options.color = cfg.color;
|
|
94
|
+
if (cfg.progress !== undefined) options.progress = cfg.progress;
|
|
95
|
+
|
|
96
|
+
// CLI flags (highest priority)
|
|
97
|
+
for (let i = 0; i < cliArgs.length; i++) {
|
|
98
|
+
const a = cliArgs[i];
|
|
99
|
+
if (a === "--no-push") options.push = false;
|
|
100
|
+
else if (a === "--interactive") options.interactive = true;
|
|
101
|
+
else if (a === "--parallel") options.parallel = true;
|
|
102
|
+
else if (a === "--dry-run") options.dryRun = true;
|
|
103
|
+
else if (a === "--verbose") options.verbose = true;
|
|
104
|
+
else if (a === "--no-color") options.color = false;
|
|
105
|
+
else if (a === "--no-progress") options.progress = false;
|
|
106
|
+
else if (a === "--make-config") options.makeConfig = true;
|
|
107
|
+
else if (a === "--branch") options.defaultBranch = cliArgs[++i];
|
|
108
|
+
else if (a === "--message") options.commitMessage = cliArgs[++i];
|
|
109
|
+
else if (a === "--depth") options.maxDepth = parseInt(cliArgs[++i], 10);
|
|
110
|
+
else if (a === "--ignore") options.ignore.push(cliArgs[++i]);
|
|
58
111
|
}
|
|
59
112
|
|
|
60
113
|
// ─── Colour helpers ──────────────────────────────────────────────────────────
|
|
61
114
|
|
|
62
115
|
const C = options.color
|
|
63
116
|
? { reset:"\x1b[0m", bold:"\x1b[1m", dim:"\x1b[2m", green:"\x1b[32m",
|
|
64
|
-
yellow:"\x1b[33m", cyan:"\x1b[36m", red:"\x1b[31m", magenta:"\x1b[35m",
|
|
117
|
+
yellow:"\x1b[33m", cyan:"\x1b[36m", red:"\x1b[31m", magenta:"\x1b[35m",
|
|
118
|
+
blue:"\x1b[34m", white:"\x1b[37m" }
|
|
65
119
|
: Object.fromEntries(
|
|
66
|
-
["reset","bold","dim","green","yellow","cyan","red","magenta","blue"].map(k
|
|
120
|
+
["reset","bold","dim","green","yellow","cyan","red","magenta","blue","white"].map(k=>[k,""])
|
|
67
121
|
);
|
|
68
122
|
|
|
69
123
|
// ─── Logging ─────────────────────────────────────────────────────────────────
|
|
@@ -76,8 +130,74 @@ const warn = (d, m) => log(d, "⚠", C.yellow, m);
|
|
|
76
130
|
const error = (d, m) => log(d, "✘", C.red, m);
|
|
77
131
|
const header = (d, m) => log(d, "▸", C.bold + C.magenta, m);
|
|
78
132
|
const pushLog = (d, m) => log(d, "↑", C.bold + C.green, m);
|
|
133
|
+
const linkLog = (d, m) => log(d, "⎘", C.bold + C.blue, m);
|
|
79
134
|
const verbose = (d, m) => { if (options.verbose) log(d, " ", C.dim, m); };
|
|
80
135
|
|
|
136
|
+
// ─── Progress bar ─────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
const progress = {
|
|
139
|
+
total: 0,
|
|
140
|
+
current: 0,
|
|
141
|
+
active: false,
|
|
142
|
+
|
|
143
|
+
init(total) {
|
|
144
|
+
if (!options.progress || !process.stdout.isTTY) return;
|
|
145
|
+
this.total = total;
|
|
146
|
+
this.current = 0;
|
|
147
|
+
this.active = true;
|
|
148
|
+
this._render();
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
tick(label = "") {
|
|
152
|
+
if (!this.active) return;
|
|
153
|
+
this.current++;
|
|
154
|
+
this._render(label);
|
|
155
|
+
if (this.current >= this.total) this.done();
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
done() {
|
|
159
|
+
if (!this.active) return;
|
|
160
|
+
this.active = false;
|
|
161
|
+
process.stdout.write("\r\x1b[K"); // clear line
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
_render(label = "") {
|
|
165
|
+
const W = 28;
|
|
166
|
+
const filled = Math.round((this.current / this.total) * W);
|
|
167
|
+
const empty = W - filled;
|
|
168
|
+
const bar = C.green + "█".repeat(filled) + C.dim + "░".repeat(empty) + C.reset;
|
|
169
|
+
const pct = String(Math.round((this.current / this.total) * 100)).padStart(3);
|
|
170
|
+
const counter = `${this.current}/${this.total}`;
|
|
171
|
+
const lbl = label ? ` ${C.dim}${label.slice(0, 24)}${C.reset}` : "";
|
|
172
|
+
process.stdout.write(`\r${C.bold}[${bar}${C.bold}] ${pct}% (${counter})${lbl}\x1b[K`);
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// ─── GitHub compare URL helper ────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
function getRemoteUrl(dir) {
|
|
179
|
+
const r = git(dir, "remote", "get-url", "origin");
|
|
180
|
+
return r.ok ? r.stdout : null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function buildCompareUrl(remoteUrl, oldHash, newHash) {
|
|
184
|
+
if (!remoteUrl) return null;
|
|
185
|
+
|
|
186
|
+
let url = remoteUrl.trim();
|
|
187
|
+
|
|
188
|
+
// SSH → HTTPS: git@github.com:org/repo.git → https://github.com/org/repo
|
|
189
|
+
if (url.startsWith("git@github.com:")) {
|
|
190
|
+
url = url.replace("git@github.com:", "https://github.com/");
|
|
191
|
+
}
|
|
192
|
+
// Strip .git suffix
|
|
193
|
+
url = url.replace(/\.git$/, "");
|
|
194
|
+
|
|
195
|
+
// Only emit links for github.com repos
|
|
196
|
+
if (!url.includes("github.com")) return null;
|
|
197
|
+
|
|
198
|
+
return `${url}/compare/${oldHash.slice(0, 8)}...${newHash.slice(0, 8)}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
81
201
|
// ─── Git helpers ─────────────────────────────────────────────────────────────
|
|
82
202
|
|
|
83
203
|
function git(cwd, ...gitArgs) {
|
|
@@ -93,6 +213,24 @@ function git(cwd, ...gitArgs) {
|
|
|
93
213
|
};
|
|
94
214
|
}
|
|
95
215
|
|
|
216
|
+
// Async version used by parallel fetch
|
|
217
|
+
function gitAsync(cwd, ...gitArgs) {
|
|
218
|
+
return new Promise((resolve) => {
|
|
219
|
+
let stdout = "", stderr = "";
|
|
220
|
+
const proc = spawn("git", gitArgs, {
|
|
221
|
+
cwd,
|
|
222
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" },
|
|
223
|
+
});
|
|
224
|
+
proc.stdout.on("data", d => { stdout += d; });
|
|
225
|
+
proc.stderr.on("data", d => { stderr += d; });
|
|
226
|
+
proc.on("close", status => resolve({
|
|
227
|
+
stdout: stdout.trim(),
|
|
228
|
+
stderr: stderr.trim(),
|
|
229
|
+
ok: status === 0,
|
|
230
|
+
}));
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
96
234
|
function isGitRepo(dir) {
|
|
97
235
|
return fs.existsSync(path.join(dir, ".git"));
|
|
98
236
|
}
|
|
@@ -126,6 +264,19 @@ function parseGitmodules(repoDir) {
|
|
|
126
264
|
return submodules.filter(s => s.path);
|
|
127
265
|
}
|
|
128
266
|
|
|
267
|
+
/** Flatten the full submodule tree for progress bar counting */
|
|
268
|
+
function countAllSubmodules(repoDir, depth = 0) {
|
|
269
|
+
if (depth > options.maxDepth) return 0;
|
|
270
|
+
let n = 0;
|
|
271
|
+
for (const sub of parseGitmodules(repoDir)) {
|
|
272
|
+
if (options.ignore.includes(sub.name)) continue;
|
|
273
|
+
n++;
|
|
274
|
+
const subDir = path.resolve(repoDir, sub.path);
|
|
275
|
+
if (fs.existsSync(subDir)) n += countAllSubmodules(subDir, depth + 1);
|
|
276
|
+
}
|
|
277
|
+
return n;
|
|
278
|
+
}
|
|
279
|
+
|
|
129
280
|
function resolveBranch(dir, declared) {
|
|
130
281
|
if (declared) return declared;
|
|
131
282
|
const r = git(dir, "remote", "show", "origin");
|
|
@@ -140,14 +291,58 @@ function hasStagedChanges(dir) {
|
|
|
140
291
|
return !git(dir, "diff", "--cached", "--quiet").ok;
|
|
141
292
|
}
|
|
142
293
|
|
|
294
|
+
// ─── Interactive prompt ───────────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
function askUser(question) {
|
|
297
|
+
return new Promise((resolve) => {
|
|
298
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
299
|
+
rl.question(question, (answer) => { rl.close(); resolve(answer.trim().toLowerCase()); });
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
143
303
|
// ─── Statistics ───────────────────────────────────────────────────────────────
|
|
144
304
|
|
|
145
305
|
const stats = {
|
|
146
|
-
updated: 0, upToDate: 0, skipped: 0,
|
|
147
|
-
committed: 0, pushed: 0, total: 0,
|
|
306
|
+
updated: 0, upToDate: 0, skipped: 0, ignored: 0,
|
|
307
|
+
failed: 0, committed: 0, pushed: 0, total: 0,
|
|
148
308
|
};
|
|
149
309
|
|
|
150
|
-
// ─── Phase 1 —
|
|
310
|
+
// ─── Phase 1 — parallel fetch pass ───────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Collect every submodule directory in the tree (flat list) so we can
|
|
314
|
+
* fire all fetches concurrently in --parallel mode.
|
|
315
|
+
*/
|
|
316
|
+
function collectSubmoduleDirs(repoDir, depth = 0, out = []) {
|
|
317
|
+
if (depth > options.maxDepth) return out;
|
|
318
|
+
for (const sub of parseGitmodules(repoDir)) {
|
|
319
|
+
if (options.ignore.includes(sub.name)) continue;
|
|
320
|
+
const subDir = path.resolve(repoDir, sub.path);
|
|
321
|
+
if (fs.existsSync(subDir) && isGitRepo(subDir)) {
|
|
322
|
+
out.push(subDir);
|
|
323
|
+
collectSubmoduleDirs(subDir, depth + 1, out);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function parallelFetchAll(repoDir) {
|
|
330
|
+
const dirs = collectSubmoduleDirs(repoDir);
|
|
331
|
+
if (!dirs.length) return;
|
|
332
|
+
|
|
333
|
+
info(0, `Parallel fetching ${C.bold}${dirs.length}${C.reset} submodules…`);
|
|
334
|
+
progress.init(dirs.length);
|
|
335
|
+
|
|
336
|
+
await Promise.all(dirs.map(async (dir) => {
|
|
337
|
+
await gitAsync(dir, "fetch", "--prune", "origin");
|
|
338
|
+
progress.tick(path.basename(dir));
|
|
339
|
+
}));
|
|
340
|
+
|
|
341
|
+
progress.done();
|
|
342
|
+
console.log();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ─── Phase 1 — sequential update pass ────────────────────────────────────────
|
|
151
346
|
|
|
152
347
|
function pullSubmodules(repoDir, depth = 0) {
|
|
153
348
|
if (depth > options.maxDepth) { warn(depth, "Max depth reached."); return false; }
|
|
@@ -161,9 +356,16 @@ function pullSubmodules(repoDir, depth = 0) {
|
|
|
161
356
|
const subDir = path.resolve(repoDir, sub.path);
|
|
162
357
|
stats.total++;
|
|
163
358
|
|
|
359
|
+
// ── Ignore list ───────────────────────────────────────────────────────
|
|
360
|
+
if (options.ignore.includes(sub.name)) {
|
|
361
|
+
log(depth, "⊘", C.dim, `${sub.name} ${C.dim}(ignored)${C.reset}`);
|
|
362
|
+
stats.ignored++;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
164
366
|
header(depth, `${sub.name} ${C.dim}(${sub.path})${C.reset}`);
|
|
165
367
|
|
|
166
|
-
// Init if missing
|
|
368
|
+
// ── Init if missing ───────────────────────────────────────────────────
|
|
167
369
|
if (!fs.existsSync(subDir) || !isGitRepo(subDir)) {
|
|
168
370
|
info(depth + 1, "Not initialised — running git submodule update --init");
|
|
169
371
|
if (!options.dryRun) {
|
|
@@ -182,15 +384,19 @@ function pullSubmodules(repoDir, depth = 0) {
|
|
|
182
384
|
continue;
|
|
183
385
|
}
|
|
184
386
|
|
|
185
|
-
// Fetch
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
387
|
+
// ── Fetch (sequential mode; parallel mode already fetched above) ──────
|
|
388
|
+
if (!options.parallel) {
|
|
389
|
+
info(depth + 1, "Fetching from origin…");
|
|
390
|
+
if (!options.dryRun) {
|
|
391
|
+
const f = git(subDir, "fetch", "--prune", "origin");
|
|
392
|
+
if (!f.ok) warn(depth + 1, `Fetch warning: ${f.stderr}`);
|
|
393
|
+
else verbose(depth + 1, f.stderr || "fetch ok");
|
|
394
|
+
}
|
|
395
|
+
// Update progress bar in sequential mode
|
|
396
|
+
progress.tick(sub.name);
|
|
191
397
|
}
|
|
192
398
|
|
|
193
|
-
// Resolve branch + remote tip
|
|
399
|
+
// ── Resolve branch + remote tip ───────────────────────────────────────
|
|
194
400
|
const branch = resolveBranch(subDir, sub.branch);
|
|
195
401
|
const remoteRef = `origin/${branch}`;
|
|
196
402
|
const remoteTip = git(subDir, "rev-parse", remoteRef).stdout;
|
|
@@ -204,8 +410,9 @@ function pullSubmodules(repoDir, depth = 0) {
|
|
|
204
410
|
}
|
|
205
411
|
|
|
206
412
|
const beforeHash = git(subDir, "rev-parse", "HEAD").stdout;
|
|
413
|
+
const remoteUrl = getRemoteUrl(subDir);
|
|
207
414
|
|
|
208
|
-
// Dry-run
|
|
415
|
+
// ── Dry-run ───────────────────────────────────────────────────────────
|
|
209
416
|
if (options.dryRun) {
|
|
210
417
|
if (beforeHash === remoteTip) {
|
|
211
418
|
success(depth + 1, `Up to date (${remoteTip.slice(0, 8)})`);
|
|
@@ -213,6 +420,8 @@ function pullSubmodules(repoDir, depth = 0) {
|
|
|
213
420
|
} else {
|
|
214
421
|
success(depth + 1,
|
|
215
422
|
`Would update ${C.dim}${beforeHash.slice(0, 8)}${C.reset} → ${C.bold}${C.green}${remoteTip.slice(0, 8)}${C.reset} (dry-run)`);
|
|
423
|
+
const url = buildCompareUrl(remoteUrl, beforeHash, remoteTip);
|
|
424
|
+
if (url) linkLog(depth + 1, url);
|
|
216
425
|
stats.updated++;
|
|
217
426
|
anyChanged = true;
|
|
218
427
|
}
|
|
@@ -220,7 +429,7 @@ function pullSubmodules(repoDir, depth = 0) {
|
|
|
220
429
|
continue;
|
|
221
430
|
}
|
|
222
431
|
|
|
223
|
-
// Checkout
|
|
432
|
+
// ── Checkout + reset to remote tip ────────────────────────────────────
|
|
224
433
|
const co = git(subDir, "checkout", "-B", branch, remoteRef);
|
|
225
434
|
if (!co.ok) {
|
|
226
435
|
const co2 = git(subDir, "checkout", branch);
|
|
@@ -251,11 +460,16 @@ function pullSubmodules(repoDir, depth = 0) {
|
|
|
251
460
|
} else {
|
|
252
461
|
success(depth + 1,
|
|
253
462
|
`Updated ${C.dim}${beforeHash.slice(0, 8)}${C.reset} → ${C.bold}${C.green}${afterHash.slice(0, 8)}${C.reset}`);
|
|
463
|
+
|
|
464
|
+
// ── GitHub compare link ───────────────────────────────────────────
|
|
465
|
+
const url = buildCompareUrl(remoteUrl, beforeHash, afterHash);
|
|
466
|
+
if (url) linkLog(depth + 1, `${C.cyan}${url}${C.reset}`);
|
|
467
|
+
|
|
254
468
|
stats.updated++;
|
|
255
469
|
anyChanged = true;
|
|
256
470
|
}
|
|
257
471
|
|
|
258
|
-
// Recurse
|
|
472
|
+
// ── Recurse ───────────────────────────────────────────────────────────
|
|
259
473
|
if (pullSubmodules(subDir, depth + 1)) {
|
|
260
474
|
git(repoDir, "add", sub.path);
|
|
261
475
|
anyChanged = true;
|
|
@@ -265,14 +479,15 @@ function pullSubmodules(repoDir, depth = 0) {
|
|
|
265
479
|
return anyChanged;
|
|
266
480
|
}
|
|
267
481
|
|
|
268
|
-
// ─── Phase 2 — commit + push
|
|
482
|
+
// ─── Phase 2 — commit + push, innermost first ────────────────────────────────
|
|
269
483
|
|
|
270
|
-
function commitAndPush(repoDir, label, depth = 0) {
|
|
271
|
-
// Children first
|
|
484
|
+
async function commitAndPush(repoDir, label, depth = 0) {
|
|
485
|
+
// Children first
|
|
272
486
|
for (const sub of parseGitmodules(repoDir)) {
|
|
487
|
+
if (options.ignore.includes(sub.name)) continue;
|
|
273
488
|
const subDir = path.resolve(repoDir, sub.path);
|
|
274
489
|
if (fs.existsSync(subDir) && isGitRepo(subDir)) {
|
|
275
|
-
commitAndPush(subDir, sub.name, depth + 1);
|
|
490
|
+
await commitAndPush(subDir, sub.name, depth + 1);
|
|
276
491
|
}
|
|
277
492
|
}
|
|
278
493
|
|
|
@@ -284,6 +499,24 @@ function commitAndPush(repoDir, label, depth = 0) {
|
|
|
284
499
|
const branch = resolveBranch(repoDir, null);
|
|
285
500
|
info(depth, `${C.bold}${label}${C.reset} — committing on ${C.bold}${branch}${C.reset}…`);
|
|
286
501
|
|
|
502
|
+
// ── Interactive prompt ────────────────────────────────────────────────
|
|
503
|
+
if (options.interactive && !options.dryRun) {
|
|
504
|
+
// Show what's staged
|
|
505
|
+
const diff = git(repoDir, "diff", "--cached", "--stat");
|
|
506
|
+
console.log();
|
|
507
|
+
console.log(`${C.dim}${diff.stdout}${C.reset}`);
|
|
508
|
+
console.log();
|
|
509
|
+
const answer = await askUser(
|
|
510
|
+
`${C.bold}${C.yellow} Push '${label}' → origin/${branch}? [y/N] ${C.reset}`
|
|
511
|
+
);
|
|
512
|
+
console.log();
|
|
513
|
+
if (answer !== "y" && answer !== "yes") {
|
|
514
|
+
warn(depth, `Skipped '${label}' (user declined)`);
|
|
515
|
+
stats.skipped++;
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
287
520
|
if (options.dryRun) {
|
|
288
521
|
warn(depth, `Would commit + push '${label}' → origin/${branch} (dry-run)`);
|
|
289
522
|
stats.committed++;
|
|
@@ -315,12 +548,74 @@ function commitAndPush(repoDir, label, depth = 0) {
|
|
|
315
548
|
stats.pushed++;
|
|
316
549
|
}
|
|
317
550
|
|
|
551
|
+
// ─── Config generator ────────────────────────────────────────────────────────
|
|
552
|
+
|
|
553
|
+
async function runMakeConfig() {
|
|
554
|
+
const dest = path.join(options.repoPath, "submodule.config.json");
|
|
555
|
+
const exists = fs.existsSync(dest);
|
|
556
|
+
|
|
557
|
+
const template = {
|
|
558
|
+
defaultBranch: "main",
|
|
559
|
+
parallel: false,
|
|
560
|
+
ignore: [],
|
|
561
|
+
commitMessage: "chore: update submodule refs",
|
|
562
|
+
interactive: false,
|
|
563
|
+
verbose: false,
|
|
564
|
+
color: true,
|
|
565
|
+
progress: true
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
console.log();
|
|
569
|
+
console.log(`${C.bold}${C.blue}╔══════════════════════════════════════════╗${C.reset}`);
|
|
570
|
+
console.log(`${C.bold}${C.blue}║ github-update-submodule v2.0.0 ║${C.reset}`);
|
|
571
|
+
console.log(`${C.bold}${C.blue}╚══════════════════════════════════════════╝${C.reset}`);
|
|
572
|
+
console.log();
|
|
573
|
+
|
|
574
|
+
if (exists) {
|
|
575
|
+
console.log(`${C.bold}${C.yellow}⚠ Config file already exists:${C.reset} ${dest}`);
|
|
576
|
+
console.log();
|
|
577
|
+
const answer = await askUser(` ${C.bold}${C.yellow}Overwrite it with defaults? [y/N] ${C.reset}`);
|
|
578
|
+
console.log();
|
|
579
|
+
if (answer !== "y" && answer !== "yes") {
|
|
580
|
+
console.log(`${C.dim} Cancelled — existing config file left unchanged.${C.reset}`);
|
|
581
|
+
console.log();
|
|
582
|
+
process.exit(0);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
fs.writeFileSync(dest, JSON.stringify(template, null, 2) + "\n", "utf8");
|
|
587
|
+
|
|
588
|
+
const action = exists ? "overwritten" : "created";
|
|
589
|
+
console.log(`${C.green}${C.bold}✔ Config file ${action}:${C.reset} ${dest}`);
|
|
590
|
+
console.log();
|
|
591
|
+
console.log(` ${C.dim}Edit the values to set your preferred defaults.`);
|
|
592
|
+
console.log(` CLI flags always override the config file.${C.reset}`);
|
|
593
|
+
console.log();
|
|
594
|
+
console.log(` ${C.bold}Available keys:${C.reset}`);
|
|
595
|
+
console.log(` ${C.cyan}defaultBranch${C.reset} branch to use when not set in .gitmodules ${C.dim}(default: "main")${C.reset}`);
|
|
596
|
+
console.log(` ${C.cyan}parallel${C.reset} fetch all submodules concurrently ${C.dim}(default: false)${C.reset}`);
|
|
597
|
+
console.log(` ${C.cyan}ignore${C.reset} array of submodule names to skip ${C.dim}(default: [])${C.reset}`);
|
|
598
|
+
console.log(` ${C.cyan}commitMessage${C.reset} commit message for pointer updates ${C.dim}(default: "chore: update submodule refs")${C.reset}`);
|
|
599
|
+
console.log(` ${C.cyan}interactive${C.reset} prompt before pushing each repo ${C.dim}(default: false)${C.reset}`);
|
|
600
|
+
console.log(` ${C.cyan}verbose${C.reset} show full git output ${C.dim}(default: false)${C.reset}`);
|
|
601
|
+
console.log(` ${C.cyan}color${C.reset} colored terminal output ${C.dim}(default: true)${C.reset}`);
|
|
602
|
+
console.log(` ${C.cyan}progress${C.reset} show progress bar ${C.dim}(default: true)${C.reset}`);
|
|
603
|
+
console.log();
|
|
604
|
+
process.exit(0);
|
|
605
|
+
}
|
|
606
|
+
|
|
318
607
|
// ─── Entry point ─────────────────────────────────────────────────────────────
|
|
319
608
|
|
|
320
|
-
function main() {
|
|
609
|
+
async function main() {
|
|
610
|
+
// --make-config: generate a config file and exit immediately
|
|
611
|
+
if (options.makeConfig) {
|
|
612
|
+
await runMakeConfig();
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
321
616
|
console.log();
|
|
322
617
|
console.log(`${C.bold}${C.blue}╔══════════════════════════════════════════╗${C.reset}`);
|
|
323
|
-
console.log(`${C.bold}${C.blue}║ github-update-submodule
|
|
618
|
+
console.log(`${C.bold}${C.blue}║ github-update-submodule v2.0.0 ║${C.reset}`);
|
|
324
619
|
console.log(`${C.bold}${C.blue}╚══════════════════════════════════════════╝${C.reset}`);
|
|
325
620
|
console.log();
|
|
326
621
|
|
|
@@ -329,26 +624,44 @@ function main() {
|
|
|
329
624
|
process.exit(1);
|
|
330
625
|
}
|
|
331
626
|
|
|
627
|
+
// Print active config
|
|
332
628
|
info(0, `Repository : ${C.bold}${options.repoPath}${C.reset}`);
|
|
333
629
|
info(0, `Default branch : ${C.bold}${options.defaultBranch}${C.reset}`);
|
|
334
|
-
info(0, `Push mode : ${options.push
|
|
335
|
-
|
|
336
|
-
|
|
630
|
+
info(0, `Push mode : ${options.push ? C.bold+C.green+"ON" : C.dim+"OFF"}${C.reset}`);
|
|
631
|
+
info(0, `Interactive : ${options.interactive ? C.bold+C.yellow+"ON" : C.dim+"OFF"}${C.reset}`);
|
|
632
|
+
info(0, `Parallel fetch : ${options.parallel ? C.bold+C.cyan+"ON" : C.dim+"OFF"}${C.reset}`);
|
|
633
|
+
if (options.ignore.length)
|
|
634
|
+
info(0, `Ignoring : ${C.bold}${C.yellow}${options.ignore.join(", ")}${C.reset}`);
|
|
635
|
+
if (options.dryRun)
|
|
636
|
+
warn(0, "DRY RUN — no changes will be made");
|
|
637
|
+
if (options.maxDepth !== Infinity)
|
|
638
|
+
info(0, `Max depth : ${options.maxDepth}`);
|
|
337
639
|
console.log();
|
|
338
640
|
|
|
339
641
|
const t0 = Date.now();
|
|
340
642
|
|
|
341
|
-
// Phase 1
|
|
643
|
+
// ── Phase 1 ───────────────────────────────────────────────────────────────
|
|
342
644
|
console.log(`${C.bold}${C.cyan}Phase 1 — Pull all submodules to latest remote commit${C.reset}`);
|
|
343
645
|
console.log();
|
|
646
|
+
|
|
647
|
+
if (options.parallel && !options.dryRun) {
|
|
648
|
+
// Fire all fetches at once, then do the sequential update pass
|
|
649
|
+
await parallelFetchAll(options.repoPath);
|
|
650
|
+
} else if (!options.parallel) {
|
|
651
|
+
// Sequential mode: init progress bar based on tree size
|
|
652
|
+
const total = countAllSubmodules(options.repoPath);
|
|
653
|
+
progress.init(total);
|
|
654
|
+
}
|
|
655
|
+
|
|
344
656
|
pullSubmodules(options.repoPath, 0);
|
|
657
|
+
progress.done(); // ensure bar is cleared if sequential
|
|
345
658
|
|
|
346
|
-
// Phase 2
|
|
659
|
+
// ── Phase 2 ───────────────────────────────────────────────────────────────
|
|
347
660
|
if (options.push) {
|
|
348
661
|
console.log();
|
|
349
662
|
console.log(`${C.bold}${C.cyan}Phase 2 — Commit & push updated refs (innermost → root)${C.reset}`);
|
|
350
663
|
console.log();
|
|
351
|
-
commitAndPush(options.repoPath, path.basename(options.repoPath), 0);
|
|
664
|
+
await commitAndPush(options.repoPath, path.basename(options.repoPath), 0);
|
|
352
665
|
} else {
|
|
353
666
|
console.log();
|
|
354
667
|
warn(0, `Refs staged locally but NOT pushed (--no-push mode).`);
|
|
@@ -365,6 +678,7 @@ function main() {
|
|
|
365
678
|
console.log(` ${C.green}↑ Committed : ${stats.committed}${C.reset}`);
|
|
366
679
|
console.log(` ${C.green}↑ Pushed : ${stats.pushed}${C.reset}`);
|
|
367
680
|
}
|
|
681
|
+
console.log(` ${C.yellow}⊘ Ignored : ${stats.ignored}${C.reset}`);
|
|
368
682
|
console.log(` ${C.yellow}⚠ Skipped : ${stats.skipped}${C.reset}`);
|
|
369
683
|
console.log(` ${C.red}✘ Failed : ${stats.failed}${C.reset}`);
|
|
370
684
|
console.log(` ${C.dim} Total : ${stats.total} (${elapsed}s)${C.reset}`);
|
|
@@ -373,4 +687,7 @@ function main() {
|
|
|
373
687
|
if (stats.failed > 0) process.exit(1);
|
|
374
688
|
}
|
|
375
689
|
|
|
376
|
-
main()
|
|
690
|
+
main().catch(err => {
|
|
691
|
+
console.error(`\n${C.red}Fatal error: ${err.message}${C.reset}`);
|
|
692
|
+
process.exit(1);
|
|
693
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-update-submodule",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Recursively pull all Git submodules to their latest remote commit and push updated refs up every parent repo — so GitHub always points to the latest.",
|
|
5
5
|
"main": "bin/github-update-submodule.js",
|
|
6
6
|
"bin": {
|