@kova1/pullr 0.6.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 +45 -14
- package/bin/pullr +156 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,13 +13,13 @@ everything current means `cd`-ing into each one.
|
|
|
13
13
|
```console
|
|
14
14
|
$ pullr ~/work
|
|
15
15
|
+ api (main, 3 new commits)
|
|
16
|
-
|
|
17
|
-
~ docs (main, 2 commits held back
|
|
16
|
+
+ web (main, 1 new commit, local changes reapplied)
|
|
17
|
+
~ docs (main, 2 commits held back because local changes conflict with them)
|
|
18
18
|
- scratch (main has no upstream)
|
|
19
19
|
x infra (main)
|
|
20
20
|
diverged from upstream (behind 2, ahead 1): cannot fast-forward
|
|
21
21
|
|
|
22
|
-
Repos: 5 updated:
|
|
22
|
+
Repos: 5 updated: 2 up to date: 0 dirty: 1 skipped: 1 failed: 1
|
|
23
23
|
Dirty: docs
|
|
24
24
|
Failed: infra
|
|
25
25
|
```
|
|
@@ -55,14 +55,21 @@ first. It will never:
|
|
|
55
55
|
failed and left exactly as it was. Rebasing it is opt-in with `--rebase`,
|
|
56
56
|
and a rebase that hits a conflict is aborted, leaving the repository as it
|
|
57
57
|
was.
|
|
58
|
-
- **
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
- **Leave your changes half-merged, or lose them.** When local edits or
|
|
59
|
+
untracked files are in the way of an update, pullr stashes them, updates,
|
|
60
|
+
and re-applies them on top. If they conflict with the incoming commits, it
|
|
61
|
+
puts the repository back exactly as it was - same commit, same files, same
|
|
62
|
+
staged and unstaged changes, no stash left behind - and reports it as
|
|
63
|
+
dirty. Edits that don't touch incoming files are never stashed at all.
|
|
64
|
+
`--no-autostash` skips the stash and just reports the repository as dirty.
|
|
62
65
|
- **Touch a repository on a detached `HEAD`**, or a branch with no upstream.
|
|
63
66
|
Both are skipped.
|
|
64
|
-
- **Enter a nested repository.** Repositories inside another repository
|
|
65
|
-
|
|
67
|
+
- **Enter a nested repository.** Repositories inside another repository are
|
|
68
|
+
left to their parent.
|
|
69
|
+
- **Move a submodule off its branch.** With `--submodules`, a submodule is
|
|
70
|
+
only ever fast-forwarded on the branch it is on. (If you set git's
|
|
71
|
+
`submodule.recurse`, pullr does what `git pull` does with it: checks
|
|
72
|
+
submodules out at the recorded commit.)
|
|
66
73
|
- **Follow symlinked directories.**
|
|
67
74
|
|
|
68
75
|
## Usage
|
|
@@ -78,6 +85,8 @@ pullr [options] [DIR]
|
|
|
78
85
|
| `--max-depth N` | `2` | How many directory levels below `DIR` to search. `0` = only `DIR` itself, `1` = `DIR` and its direct children, ... |
|
|
79
86
|
| `-n`, `--dry-run` | | Show what a run would do without pulling, see below |
|
|
80
87
|
| `-r`, `--rebase` | off | Rebase local commits onto the upstream instead of refusing a diverged branch (`git pull --rebase`). A rebase that hits a conflict is aborted and the repository is left as it was. |
|
|
88
|
+
| `--no-autostash` | | Don't stash local changes that are in the way of an update; report the repository as dirty instead. Autostash is on by default, see "What it will never do". |
|
|
89
|
+
| `--submodules` | off | Also fast-forward each submodule that is checked out on a branch, see below |
|
|
81
90
|
| `--no-color` | | Disable colored output. Also disabled when the [`NO_COLOR`](https://no-color.org) environment variable is set, or when output is not a terminal. |
|
|
82
91
|
| `-V`, `--version` | | Print the version |
|
|
83
92
|
| `-h`, `--help` | | Show help |
|
|
@@ -85,7 +94,7 @@ pullr [options] [DIR]
|
|
|
85
94
|
### Output
|
|
86
95
|
|
|
87
96
|
Each repository gets one line: `+` updated, `=` already up to date, `~`
|
|
88
|
-
dirty (local changes
|
|
97
|
+
dirty (local changes conflict with the update, which is held back), `-`
|
|
89
98
|
skipped, `x` failed (with the reason indented below it). Exits `1` if any
|
|
90
99
|
pull failed, `2` on invalid arguments, `0` otherwise; a dirty repository is
|
|
91
100
|
not a failure.
|
|
@@ -108,12 +117,34 @@ Failed: infra
|
|
|
108
117
|
```
|
|
109
118
|
|
|
110
119
|
It runs `git fetch` in each repository first, so the answer reflects the
|
|
111
|
-
remote as it is now, then prints the same lines a real run would
|
|
112
|
-
|
|
113
|
-
|
|
120
|
+
remote as it is now, then prints the same lines a real run would. An update
|
|
121
|
+
that would need to stash local changes shows as `+ ... , local changes to
|
|
122
|
+
autostash`, and with `--rebase` a diverged repository shows as
|
|
123
|
+
`+ ... , 2 to rebase` instead of `x`; whether re-applying or rebasing would
|
|
124
|
+
conflict is only known by running it. Fetching
|
|
114
125
|
only updates remote-tracking refs: the working tree and current branch are
|
|
115
126
|
never modified. Exit codes match a real run.
|
|
116
127
|
|
|
128
|
+
### Submodules
|
|
129
|
+
|
|
130
|
+
By default pullr updates a repository the way `git pull` does: submodules stay
|
|
131
|
+
where they are, unless you set git's `submodule.recurse`, in which case they
|
|
132
|
+
are checked out at the commits the parent now records, as `git pull` would.
|
|
133
|
+
|
|
134
|
+
`--submodules` is for working inside submodules. Each submodule that is
|
|
135
|
+
checked out on a branch is treated as a repository of its own - fetched,
|
|
136
|
+
fast-forwarded with the same rules, and listed under its parent:
|
|
137
|
+
|
|
138
|
+
```console
|
|
139
|
+
$ pullr --submodules ~/work
|
|
140
|
+
+ app (main, 2 new commits)
|
|
141
|
+
+ app/lib/core (main, 5 new commits)
|
|
142
|
+
- app/vendor/sdk (detached HEAD)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Submodules on a detached HEAD (git's default after `git submodule update`) are
|
|
146
|
+
skipped, and uninitialized ones are left out.
|
|
147
|
+
|
|
117
148
|
## How it compares
|
|
118
149
|
|
|
119
150
|
| | pullr | [gita](https://github.com/nosarthur/gita) | [gitup](https://github.com/earwig/git-repo-updater) | [myrepos](https://myrepos.branchable.com) (`mr`) | [mani](https://github.com/alajmo/mani) |
|
|
@@ -123,7 +154,7 @@ never modified. Exit codes match a real run.
|
|
|
123
154
|
| Pulls in parallel | 8 at a time by default | all at once, no limit | no | opt-in | opt-in |
|
|
124
155
|
| Update | fetch + fast‑forward | `git pull` | fetch + fast‑forward | `git pull` | no built-in pull |
|
|
125
156
|
| Merge commits | never | depends on your git config | never | depends on your git config | depends on your command |
|
|
126
|
-
| Local changes in the way |
|
|
157
|
+
| Local changes in the way | stashed and reapplied; rolled back on conflict | git's error | skipped | git's error | - |
|
|
127
158
|
| Dry run | yes | no | no | no | prints commands |
|
|
128
159
|
|
|
129
160
|
**Why "8 at a time" and not "all at once".** Many self-hosted git servers
|
package/bin/pullr
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Recursively find git repos under a directory and fast-forward pull each one.
|
|
3
|
-
# Usage: pullr [--jobs N] [--max-depth N] [--dry-run] [--rebase] [--no-color] [DIR]
|
|
3
|
+
# Usage: pullr [--jobs N] [--max-depth N] [--dry-run] [--rebase] [--no-autostash] [--submodules] [--no-color] [DIR]
|
|
4
4
|
set -euo pipefail
|
|
5
5
|
|
|
6
|
-
VERSION="0.
|
|
6
|
+
VERSION="0.8.0" # set by the release workflow
|
|
7
7
|
|
|
8
8
|
usage() {
|
|
9
9
|
cat <<'EOF'
|
|
@@ -24,6 +24,14 @@ Options:
|
|
|
24
24
|
refusing a branch that has diverged (git pull --rebase).
|
|
25
25
|
A rebase that hits a conflict is aborted, leaving the
|
|
26
26
|
repository as it was.
|
|
27
|
+
--no-autostash Don't stash local changes that are in the way of an
|
|
28
|
+
update. By default they are stashed, the update is
|
|
29
|
+
made and they are re-applied; if re-applying conflicts,
|
|
30
|
+
the repository is put back exactly as it was.
|
|
31
|
+
--autostash The default; accepted for scripts that want to be explicit.
|
|
32
|
+
--submodules Also fast-forward each submodule that is checked out on a
|
|
33
|
+
branch, with the same rules as any other repository.
|
|
34
|
+
Submodules on a detached HEAD are skipped.
|
|
27
35
|
--no-color Disable colored output. Also disabled when NO_COLOR is
|
|
28
36
|
set (https://no-color.org) or output is not a terminal.
|
|
29
37
|
-V, --version Print the version.
|
|
@@ -34,6 +42,8 @@ EOF
|
|
|
34
42
|
mode=pull
|
|
35
43
|
jobs=8
|
|
36
44
|
rebase=0
|
|
45
|
+
autostash=1
|
|
46
|
+
submodules=0
|
|
37
47
|
color=1
|
|
38
48
|
max_depth=2
|
|
39
49
|
root=""
|
|
@@ -46,6 +56,9 @@ while (($#)); do
|
|
|
46
56
|
--max-depth=*) max_depth="${1#*=}"; shift ;;
|
|
47
57
|
-n|--dry-run) mode=dryrun; shift ;;
|
|
48
58
|
-r|--rebase) rebase=1; shift ;;
|
|
59
|
+
--autostash) autostash=1; shift ;;
|
|
60
|
+
--no-autostash) autostash=0; shift ;;
|
|
61
|
+
--submodules) submodules=1; shift ;;
|
|
49
62
|
--no-color) color=0; shift ;;
|
|
50
63
|
-h|--help) usage; exit 0 ;;
|
|
51
64
|
-V|--version) echo "pullr $VERSION"; exit 0 ;;
|
|
@@ -120,8 +133,8 @@ report_diverged() {
|
|
|
120
133
|
|
|
121
134
|
report_dirty() {
|
|
122
135
|
printf 'dirty\t%s\n' "$1"
|
|
123
|
-
printf '%s~%s %s %s(%s, %s held back
|
|
124
|
-
"$yellow" "$reset" "$1" "$dim" "$2" "$(commits "$3")" "$reset"
|
|
136
|
+
printf '%s~%s %s %s(%s, %s held back %s)%s\n' \
|
|
137
|
+
"$yellow" "$reset" "$1" "$dim" "$2" "$(commits "$3")" "${4:-by local changes}" "$reset"
|
|
125
138
|
}
|
|
126
139
|
|
|
127
140
|
report_current() {
|
|
@@ -133,6 +146,7 @@ report_updated() {
|
|
|
133
146
|
local note
|
|
134
147
|
note="$3 new commit$([[ "$3" == 1 ]] || printf s)"
|
|
135
148
|
[[ "$4" == 0 ]] || note+=", $4 $5"
|
|
149
|
+
[[ -z "${6:-}" ]] || note+=", $6"
|
|
136
150
|
echo updated
|
|
137
151
|
printf '%s+%s %s %s(%s, %s)%s\n' "$green" "$reset" "$1" "$dim" "$2" "$note" "$reset"
|
|
138
152
|
}
|
|
@@ -142,9 +156,24 @@ commits() {
|
|
|
142
156
|
printf '%s commit%s' "$1" "$([[ "$1" == 1 ]] || printf s)"
|
|
143
157
|
}
|
|
144
158
|
|
|
145
|
-
# Uncommitted changes to tracked files, staged or not.
|
|
159
|
+
# Uncommitted changes to tracked files, staged or not. A submodule checked out
|
|
160
|
+
# at a different commit than the one recorded is not one: git neither refuses
|
|
161
|
+
# to update over it nor touches it.
|
|
146
162
|
has_tracked_changes() {
|
|
147
|
-
! git -C "$1" diff --quiet HEAD --
|
|
163
|
+
! git -C "$1" diff --quiet --ignore-submodules HEAD --
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
has_local_changes() {
|
|
167
|
+
[[ -n "$(git -C "$1" status --porcelain --ignore-submodules=all)" ]]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Mirror `git pull` for people who set submodule.recurse: after the update,
|
|
171
|
+
# check submodules out at the commits the parent now records. With
|
|
172
|
+
# --submodules each one is fast-forwarded on its own branch instead.
|
|
173
|
+
sync_submodules() {
|
|
174
|
+
[[ "$submodules" == 0 ]] || return 0
|
|
175
|
+
[[ "$(git -C "$1" config --type=bool --get submodule.recurse 2>/dev/null)" == true ]] || return 0
|
|
176
|
+
git -C "$1" submodule update --recursive 2>&1
|
|
148
177
|
}
|
|
149
178
|
|
|
150
179
|
pull_repo() {
|
|
@@ -157,24 +186,28 @@ pull_repo() {
|
|
|
157
186
|
elif [[ "$ahead" == 0 ]]; then
|
|
158
187
|
# git refuses, touching nothing, when local changes are in the way
|
|
159
188
|
if out="$(git -C "$repo" merge --ff-only --quiet '@{upstream}' 2>&1)"; then
|
|
160
|
-
|
|
161
|
-
elif
|
|
189
|
+
report_synced "$repo" "$rel" "$branch" "$behind" 0 ""
|
|
190
|
+
elif has_local_changes "$repo" && [[ "$autostash" == 1 ]]; then
|
|
191
|
+
autostash_update "$repo" "$rel" "$branch" "$behind" 0 ff
|
|
192
|
+
elif has_local_changes "$repo"; then
|
|
162
193
|
report_dirty "$rel" "$branch" "$behind"
|
|
163
194
|
else
|
|
164
195
|
report_failed "$rel" "$branch" "$out"
|
|
165
196
|
fi
|
|
166
197
|
elif [[ "$rebase" == 0 ]]; then
|
|
167
198
|
report_diverged "$rel" "$branch" "$behind" "$ahead"
|
|
199
|
+
elif has_tracked_changes "$repo" && [[ "$autostash" == 1 ]]; then
|
|
200
|
+
autostash_update "$repo" "$rel" "$branch" "$behind" "$ahead" rebase
|
|
168
201
|
elif has_tracked_changes "$repo"; then
|
|
169
202
|
report_dirty "$rel" "$branch" "$behind"
|
|
170
203
|
elif out="$(git -C "$repo" rebase --quiet '@{upstream}' 2>&1)"; then
|
|
171
|
-
|
|
204
|
+
report_synced "$repo" "$rel" "$branch" "$behind" "$ahead" rebased
|
|
172
205
|
else
|
|
173
206
|
# A rebase that stopped on a conflict must not leave the repository
|
|
174
207
|
# mid-rebase; --abort succeeds only when one is in progress.
|
|
175
208
|
if git -C "$repo" rebase --abort >/dev/null 2>&1; then
|
|
176
209
|
report_failed "$rel" "$branch" "$out"
|
|
177
|
-
elif
|
|
210
|
+
elif has_local_changes "$repo"; then
|
|
178
211
|
report_dirty "$rel" "$branch" "$behind"
|
|
179
212
|
else
|
|
180
213
|
report_failed "$rel" "$branch" "$out"
|
|
@@ -182,12 +215,98 @@ pull_repo() {
|
|
|
182
215
|
fi
|
|
183
216
|
}
|
|
184
217
|
|
|
218
|
+
# Local changes are in the way of the update ($6: ff or rebase). Stash them,
|
|
219
|
+
# update, and re-apply them. If re-applying is anything but clean, put the
|
|
220
|
+
# repository back exactly as it was and report it as dirty. The stash is only
|
|
221
|
+
# dropped once its changes are back in the working tree.
|
|
222
|
+
autostash_update() {
|
|
223
|
+
local repo="$1" rel="$2" branch="$3" behind="$4" ahead="$5" how="$6"
|
|
224
|
+
local before stash out
|
|
225
|
+
before="$(git -C "$repo" rev-parse HEAD)"
|
|
226
|
+
if ! out="$(git -C "$repo" stash push --include-untracked --quiet -m "pullr autostash" 2>&1)" ||
|
|
227
|
+
! stash="$(git -C "$repo" rev-parse --verify --quiet refs/stash)"; then
|
|
228
|
+
report_failed "$rel" "$branch" "could not stash local changes:"$'\n'"$out"
|
|
229
|
+
return
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
if [[ "$how" == ff ]]; then
|
|
233
|
+
out="$(git -C "$repo" merge --ff-only --quiet '@{upstream}' 2>&1)"
|
|
234
|
+
else
|
|
235
|
+
out="$(git -C "$repo" rebase --quiet '@{upstream}' 2>&1)"
|
|
236
|
+
fi || {
|
|
237
|
+
# Not caused by local changes - they were stashed. Undo, then restore
|
|
238
|
+
# them onto the commit they came from, where they apply cleanly.
|
|
239
|
+
git -C "$repo" rebase --abort >/dev/null 2>&1 || true
|
|
240
|
+
if restore_stash "$repo" "$stash"; then
|
|
241
|
+
report_failed "$rel" "$branch" "$out"
|
|
242
|
+
else
|
|
243
|
+
report_failed "$rel" "$branch" "$out"$'\n'"$(stash_kept "$repo")"
|
|
244
|
+
fi
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if git -C "$repo" stash apply --index --quiet "$stash" >/dev/null 2>&1; then
|
|
249
|
+
drop_stash "$repo" "$stash"
|
|
250
|
+
local verb=""
|
|
251
|
+
[[ "$how" == ff ]] || verb=rebased
|
|
252
|
+
report_synced "$repo" "$rel" "$branch" "$behind" "$([[ "$how" == ff ]] && echo 0 || echo "$ahead")" "$verb" "local changes reapplied"
|
|
253
|
+
return
|
|
254
|
+
fi
|
|
255
|
+
|
|
256
|
+
# Conflict: go back to where we started.
|
|
257
|
+
git -C "$repo" reset --hard --quiet "$before"
|
|
258
|
+
clear_restored_untracked "$repo" "$stash"
|
|
259
|
+
if restore_stash "$repo" "$stash"; then
|
|
260
|
+
report_dirty "$rel" "$branch" "$behind" "because local changes conflict with them"
|
|
261
|
+
else
|
|
262
|
+
report_failed "$rel" "$branch" "could not re-apply local changes after an update conflict."$'\n'"$(stash_kept "$repo")"
|
|
263
|
+
fi
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
# Re-apply a stash onto the commit it was made from, then drop it.
|
|
267
|
+
restore_stash() {
|
|
268
|
+
git -C "$1" stash apply --index --quiet "$2" >/dev/null 2>&1 || return 1
|
|
269
|
+
drop_stash "$1" "$2"
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
# `git stash drop` takes stash@{n}, not a commit: find the entry that is ours.
|
|
273
|
+
drop_stash() {
|
|
274
|
+
local ref
|
|
275
|
+
ref="$(git -C "$1" stash list --format='%gd %H' | awk -v c="$2" '$2 == c { print $1; exit }')"
|
|
276
|
+
[[ -z "$ref" ]] || git -C "$1" stash drop --quiet "$ref" >/dev/null 2>&1 || true
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
# A failed `stash apply` may already have written the stash's untracked files
|
|
280
|
+
# back. They were moved into the stash, so anything at those paths now came
|
|
281
|
+
# from that apply; remove them so the stash can be applied again.
|
|
282
|
+
clear_restored_untracked() {
|
|
283
|
+
local path
|
|
284
|
+
git -C "$1" rev-parse --verify --quiet "$2^3" >/dev/null || return 0
|
|
285
|
+
while IFS= read -r path; do
|
|
286
|
+
[[ -z "$path" ]] || rm -f -- "$1/$path"
|
|
287
|
+
done < <(git -C "$1" ls-tree -r --name-only "$2^3")
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
stash_kept() {
|
|
291
|
+
printf 'Your local changes are safe in the stash (git -C %q stash list).' "$1"
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
# The repository was updated; report it, unless syncing submodules failed.
|
|
295
|
+
report_synced() {
|
|
296
|
+
local out
|
|
297
|
+
if out="$(sync_submodules "$1")"; then
|
|
298
|
+
report_updated "$2" "$3" "$4" "$5" "$6" "${7:-}"
|
|
299
|
+
else
|
|
300
|
+
report_failed "$2" "$3" "updated, but updating submodules failed:"$'\n'"$out"
|
|
301
|
+
fi
|
|
302
|
+
}
|
|
303
|
+
|
|
185
304
|
# Would local changes stop a fast-forward? Git refuses when a file it has to
|
|
186
305
|
# update is modified locally or exists untracked, so compare the two lists.
|
|
187
306
|
changes_block_update() {
|
|
188
307
|
local repo="$1" incoming local_paths
|
|
189
308
|
incoming="$(git -C "$repo" diff --name-only HEAD '@{upstream}')"
|
|
190
|
-
local_paths="$(git -C "$repo" status --porcelain --untracked-files=all | cut -c4-)"
|
|
309
|
+
local_paths="$(git -C "$repo" status --porcelain --untracked-files=all --ignore-submodules=all | cut -c4-)"
|
|
191
310
|
[[ -n "$incoming" && -n "$local_paths" ]] || return 1
|
|
192
311
|
printf '%s\n' "$local_paths" | grep -qFxf <(printf '%s\n' "$incoming")
|
|
193
312
|
}
|
|
@@ -200,13 +319,17 @@ dryrun_repo() {
|
|
|
200
319
|
if [[ "$behind" == 0 ]]; then
|
|
201
320
|
report_current "$rel" "$branch"
|
|
202
321
|
elif [[ "$ahead" == 0 ]]; then
|
|
203
|
-
if changes_block_update "$repo"; then
|
|
322
|
+
if changes_block_update "$repo" && [[ "$autostash" == 1 ]]; then
|
|
323
|
+
report_updated "$rel" "$branch" "$behind" 0 "" "local changes to autostash"
|
|
324
|
+
elif changes_block_update "$repo"; then
|
|
204
325
|
report_dirty "$rel" "$branch" "$behind"
|
|
205
326
|
else
|
|
206
327
|
report_updated "$rel" "$branch" "$behind" 0 ""
|
|
207
328
|
fi
|
|
208
329
|
elif [[ "$rebase" == 0 ]]; then
|
|
209
330
|
report_diverged "$rel" "$branch" "$behind" "$ahead"
|
|
331
|
+
elif has_tracked_changes "$repo" && [[ "$autostash" == 1 ]]; then
|
|
332
|
+
report_updated "$rel" "$branch" "$behind" "$ahead" "to rebase" "local changes to autostash"
|
|
210
333
|
elif has_tracked_changes "$repo"; then
|
|
211
334
|
report_dirty "$rel" "$branch" "$behind"
|
|
212
335
|
else
|
|
@@ -225,11 +348,12 @@ worker() {
|
|
|
225
348
|
|
|
226
349
|
# --- discovery ---------------------------------------------------------------
|
|
227
350
|
|
|
228
|
-
repos=()
|
|
351
|
+
repos=() nested=()
|
|
229
352
|
walk() {
|
|
230
353
|
local dir="$1" depth="$2"
|
|
231
354
|
if [[ -e "$dir/.git" ]]; then
|
|
232
|
-
repos+=("$dir")
|
|
355
|
+
repos+=("$dir") nested+=(0)
|
|
356
|
+
[[ "$submodules" == 0 ]] || add_submodules "$dir"
|
|
233
357
|
return
|
|
234
358
|
fi
|
|
235
359
|
((depth < max_depth)) || return 0
|
|
@@ -239,6 +363,17 @@ walk() {
|
|
|
239
363
|
walk "${child%/}" $((depth + 1))
|
|
240
364
|
done
|
|
241
365
|
}
|
|
366
|
+
# Checked-out submodules, recursively, in git's order; uninitialized ones
|
|
367
|
+
# have no checkout to update and are left out.
|
|
368
|
+
add_submodules() {
|
|
369
|
+
local sub
|
|
370
|
+
# shellcheck disable=SC2016 # $displaypath is expanded by git submodule foreach
|
|
371
|
+
while IFS= read -r sub; do
|
|
372
|
+
[[ -n "$sub" ]] || continue
|
|
373
|
+
repos+=("$1/$sub") nested+=(1)
|
|
374
|
+
done < <(git -C "$1" submodule foreach --quiet --recursive 'printf "%s\n" "$displaypath"' 2>/dev/null)
|
|
375
|
+
}
|
|
376
|
+
|
|
242
377
|
walk "$root" 0
|
|
243
378
|
|
|
244
379
|
count=${#repos[@]}
|
|
@@ -256,6 +391,8 @@ failed_repos=() dirty_repos=()
|
|
|
256
391
|
|
|
257
392
|
consume() {
|
|
258
393
|
local text="$1" head="${1%%$'\n'*}" body="${1#*$'\n'}"
|
|
394
|
+
# a submodule's lines sit under its parent's
|
|
395
|
+
[[ "${2:-0}" == 0 ]] || body=" ${body//$'\n'/$'\n' }"
|
|
259
396
|
local category="${head%%$'\t'*}" label="${head#*$'\t'}"
|
|
260
397
|
case "$category" in
|
|
261
398
|
updated) updated=$((updated + 1)) ;;
|
|
@@ -272,8 +409,8 @@ if ((jobs > 1 && count > 1)); then
|
|
|
272
409
|
scratch="$(mktemp -d)"
|
|
273
410
|
trap 'rm -rf "$scratch"' EXIT
|
|
274
411
|
export GIT_TERMINAL_PROMPT=0
|
|
275
|
-
export mode rebase root green yellow red dim reset
|
|
276
|
-
export -f worker pull_repo dryrun_repo repo_label indented prepare report_failed report_diverged report_dirty report_current report_updated commits has_tracked_changes changes_block_update
|
|
412
|
+
export mode rebase autostash submodules root green yellow red dim reset
|
|
413
|
+
export -f worker pull_repo dryrun_repo repo_label indented prepare report_failed report_diverged report_dirty report_current report_updated report_synced autostash_update restore_stash drop_stash clear_restored_untracked stash_kept commits has_tracked_changes has_local_changes sync_submodules changes_block_update
|
|
277
414
|
# shellcheck disable=SC2016 # the single-quoted body runs inside the child shell
|
|
278
415
|
for ((i = 0; i < count; i++)); do printf '%s\0%s\0' "$i" "${repos[i]}"; done |
|
|
279
416
|
xargs -0 -n 2 -P "$jobs" "$BASH" -c 'worker "$@"' _ "$scratch" &
|
|
@@ -287,12 +424,12 @@ if ((jobs > 1 && count > 1)); then
|
|
|
287
424
|
fi
|
|
288
425
|
sleep 0.1
|
|
289
426
|
done
|
|
290
|
-
consume "$(cat "$scratch/$i.done")"
|
|
427
|
+
consume "$(cat "$scratch/$i.done")" "${nested[i]}"
|
|
291
428
|
done
|
|
292
429
|
wait "$pool" || true
|
|
293
430
|
else
|
|
294
|
-
for
|
|
295
|
-
consume "$("${mode}_repo" "$
|
|
431
|
+
for ((i = 0; i < count; i++)); do
|
|
432
|
+
consume "$("${mode}_repo" "${repos[i]}")" "${nested[i]}"
|
|
296
433
|
done
|
|
297
434
|
fi
|
|
298
435
|
|