@kova1/pullr 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 +27 -2
- package/bin/pullr +60 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,8 +61,12 @@ first. It will never:
|
|
|
61
61
|
incoming files don't stop the update.
|
|
62
62
|
- **Touch a repository on a detached `HEAD`**, or a branch with no upstream.
|
|
63
63
|
Both are skipped.
|
|
64
|
-
- **Enter a nested repository.** Repositories inside another repository
|
|
65
|
-
|
|
64
|
+
- **Enter a nested repository.** Repositories inside another repository are
|
|
65
|
+
left to their parent.
|
|
66
|
+
- **Move a submodule off its branch.** With `--submodules`, a submodule is
|
|
67
|
+
only ever fast-forwarded on the branch it is on. (If you set git's
|
|
68
|
+
`submodule.recurse`, pullr does what `git pull` does with it: checks
|
|
69
|
+
submodules out at the recorded commit.)
|
|
66
70
|
- **Follow symlinked directories.**
|
|
67
71
|
|
|
68
72
|
## Usage
|
|
@@ -78,6 +82,7 @@ pullr [options] [DIR]
|
|
|
78
82
|
| `--max-depth N` | `2` | How many directory levels below `DIR` to search. `0` = only `DIR` itself, `1` = `DIR` and its direct children, ... |
|
|
79
83
|
| `-n`, `--dry-run` | | Show what a run would do without pulling, see below |
|
|
80
84
|
| `-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. |
|
|
85
|
+
| `--submodules` | off | Also fast-forward each submodule that is checked out on a branch, see below |
|
|
81
86
|
| `--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
87
|
| `-V`, `--version` | | Print the version |
|
|
83
88
|
| `-h`, `--help` | | Show help |
|
|
@@ -114,6 +119,26 @@ remote as it is now, then prints the same lines a real run would (with
|
|
|
114
119
|
only updates remote-tracking refs: the working tree and current branch are
|
|
115
120
|
never modified. Exit codes match a real run.
|
|
116
121
|
|
|
122
|
+
### Submodules
|
|
123
|
+
|
|
124
|
+
By default pullr updates a repository the way `git pull` does: submodules stay
|
|
125
|
+
where they are, unless you set git's `submodule.recurse`, in which case they
|
|
126
|
+
are checked out at the commits the parent now records, as `git pull` would.
|
|
127
|
+
|
|
128
|
+
`--submodules` is for working inside submodules. Each submodule that is
|
|
129
|
+
checked out on a branch is treated as a repository of its own - fetched,
|
|
130
|
+
fast-forwarded with the same rules, and listed under its parent:
|
|
131
|
+
|
|
132
|
+
```console
|
|
133
|
+
$ pullr --submodules ~/work
|
|
134
|
+
+ app (main, 2 new commits)
|
|
135
|
+
+ app/lib/core (main, 5 new commits)
|
|
136
|
+
- app/vendor/sdk (detached HEAD)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Submodules on a detached HEAD (git's default after `git submodule update`) are
|
|
140
|
+
skipped, and uninitialized ones are left out.
|
|
141
|
+
|
|
117
142
|
## How it compares
|
|
118
143
|
|
|
119
144
|
| | 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) |
|
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] [--submodules] [--no-color] [DIR]
|
|
4
4
|
set -euo pipefail
|
|
5
5
|
|
|
6
|
-
VERSION="0.
|
|
6
|
+
VERSION="0.7.0" # set by the release workflow
|
|
7
7
|
|
|
8
8
|
usage() {
|
|
9
9
|
cat <<'EOF'
|
|
@@ -24,6 +24,9 @@ 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
|
+
--submodules Also fast-forward each submodule that is checked out on a
|
|
28
|
+
branch, with the same rules as any other repository.
|
|
29
|
+
Submodules on a detached HEAD are skipped.
|
|
27
30
|
--no-color Disable colored output. Also disabled when NO_COLOR is
|
|
28
31
|
set (https://no-color.org) or output is not a terminal.
|
|
29
32
|
-V, --version Print the version.
|
|
@@ -34,6 +37,7 @@ EOF
|
|
|
34
37
|
mode=pull
|
|
35
38
|
jobs=8
|
|
36
39
|
rebase=0
|
|
40
|
+
submodules=0
|
|
37
41
|
color=1
|
|
38
42
|
max_depth=2
|
|
39
43
|
root=""
|
|
@@ -46,6 +50,7 @@ while (($#)); do
|
|
|
46
50
|
--max-depth=*) max_depth="${1#*=}"; shift ;;
|
|
47
51
|
-n|--dry-run) mode=dryrun; shift ;;
|
|
48
52
|
-r|--rebase) rebase=1; shift ;;
|
|
53
|
+
--submodules) submodules=1; shift ;;
|
|
49
54
|
--no-color) color=0; shift ;;
|
|
50
55
|
-h|--help) usage; exit 0 ;;
|
|
51
56
|
-V|--version) echo "pullr $VERSION"; exit 0 ;;
|
|
@@ -142,9 +147,24 @@ commits() {
|
|
|
142
147
|
printf '%s commit%s' "$1" "$([[ "$1" == 1 ]] || printf s)"
|
|
143
148
|
}
|
|
144
149
|
|
|
145
|
-
# Uncommitted changes to tracked files, staged or not.
|
|
150
|
+
# Uncommitted changes to tracked files, staged or not. A submodule checked out
|
|
151
|
+
# at a different commit than the one recorded is not one: git neither refuses
|
|
152
|
+
# to update over it nor touches it.
|
|
146
153
|
has_tracked_changes() {
|
|
147
|
-
! git -C "$1" diff --quiet HEAD --
|
|
154
|
+
! git -C "$1" diff --quiet --ignore-submodules HEAD --
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
has_local_changes() {
|
|
158
|
+
[[ -n "$(git -C "$1" status --porcelain --ignore-submodules=all)" ]]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# Mirror `git pull` for people who set submodule.recurse: after the update,
|
|
162
|
+
# check submodules out at the commits the parent now records. With
|
|
163
|
+
# --submodules each one is fast-forwarded on its own branch instead.
|
|
164
|
+
sync_submodules() {
|
|
165
|
+
[[ "$submodules" == 0 ]] || return 0
|
|
166
|
+
[[ "$(git -C "$1" config --type=bool --get submodule.recurse 2>/dev/null)" == true ]] || return 0
|
|
167
|
+
git -C "$1" submodule update --recursive 2>&1
|
|
148
168
|
}
|
|
149
169
|
|
|
150
170
|
pull_repo() {
|
|
@@ -157,8 +177,8 @@ pull_repo() {
|
|
|
157
177
|
elif [[ "$ahead" == 0 ]]; then
|
|
158
178
|
# git refuses, touching nothing, when local changes are in the way
|
|
159
179
|
if out="$(git -C "$repo" merge --ff-only --quiet '@{upstream}' 2>&1)"; then
|
|
160
|
-
|
|
161
|
-
elif
|
|
180
|
+
report_synced "$repo" "$rel" "$branch" "$behind" 0 ""
|
|
181
|
+
elif has_local_changes "$repo"; then
|
|
162
182
|
report_dirty "$rel" "$branch" "$behind"
|
|
163
183
|
else
|
|
164
184
|
report_failed "$rel" "$branch" "$out"
|
|
@@ -168,13 +188,13 @@ pull_repo() {
|
|
|
168
188
|
elif has_tracked_changes "$repo"; then
|
|
169
189
|
report_dirty "$rel" "$branch" "$behind"
|
|
170
190
|
elif out="$(git -C "$repo" rebase --quiet '@{upstream}' 2>&1)"; then
|
|
171
|
-
|
|
191
|
+
report_synced "$repo" "$rel" "$branch" "$behind" "$ahead" rebased
|
|
172
192
|
else
|
|
173
193
|
# A rebase that stopped on a conflict must not leave the repository
|
|
174
194
|
# mid-rebase; --abort succeeds only when one is in progress.
|
|
175
195
|
if git -C "$repo" rebase --abort >/dev/null 2>&1; then
|
|
176
196
|
report_failed "$rel" "$branch" "$out"
|
|
177
|
-
elif
|
|
197
|
+
elif has_local_changes "$repo"; then
|
|
178
198
|
report_dirty "$rel" "$branch" "$behind"
|
|
179
199
|
else
|
|
180
200
|
report_failed "$rel" "$branch" "$out"
|
|
@@ -182,12 +202,22 @@ pull_repo() {
|
|
|
182
202
|
fi
|
|
183
203
|
}
|
|
184
204
|
|
|
205
|
+
# The repository was updated; report it, unless syncing submodules failed.
|
|
206
|
+
report_synced() {
|
|
207
|
+
local out
|
|
208
|
+
if out="$(sync_submodules "$1")"; then
|
|
209
|
+
report_updated "$2" "$3" "$4" "$5" "$6"
|
|
210
|
+
else
|
|
211
|
+
report_failed "$2" "$3" "updated, but updating submodules failed:"$'\n'"$out"
|
|
212
|
+
fi
|
|
213
|
+
}
|
|
214
|
+
|
|
185
215
|
# Would local changes stop a fast-forward? Git refuses when a file it has to
|
|
186
216
|
# update is modified locally or exists untracked, so compare the two lists.
|
|
187
217
|
changes_block_update() {
|
|
188
218
|
local repo="$1" incoming local_paths
|
|
189
219
|
incoming="$(git -C "$repo" diff --name-only HEAD '@{upstream}')"
|
|
190
|
-
local_paths="$(git -C "$repo" status --porcelain --untracked-files=all | cut -c4-)"
|
|
220
|
+
local_paths="$(git -C "$repo" status --porcelain --untracked-files=all --ignore-submodules=all | cut -c4-)"
|
|
191
221
|
[[ -n "$incoming" && -n "$local_paths" ]] || return 1
|
|
192
222
|
printf '%s\n' "$local_paths" | grep -qFxf <(printf '%s\n' "$incoming")
|
|
193
223
|
}
|
|
@@ -225,11 +255,12 @@ worker() {
|
|
|
225
255
|
|
|
226
256
|
# --- discovery ---------------------------------------------------------------
|
|
227
257
|
|
|
228
|
-
repos=()
|
|
258
|
+
repos=() nested=()
|
|
229
259
|
walk() {
|
|
230
260
|
local dir="$1" depth="$2"
|
|
231
261
|
if [[ -e "$dir/.git" ]]; then
|
|
232
|
-
repos+=("$dir")
|
|
262
|
+
repos+=("$dir") nested+=(0)
|
|
263
|
+
[[ "$submodules" == 0 ]] || add_submodules "$dir"
|
|
233
264
|
return
|
|
234
265
|
fi
|
|
235
266
|
((depth < max_depth)) || return 0
|
|
@@ -239,6 +270,17 @@ walk() {
|
|
|
239
270
|
walk "${child%/}" $((depth + 1))
|
|
240
271
|
done
|
|
241
272
|
}
|
|
273
|
+
# Checked-out submodules, recursively, in git's order; uninitialized ones
|
|
274
|
+
# have no checkout to update and are left out.
|
|
275
|
+
add_submodules() {
|
|
276
|
+
local sub
|
|
277
|
+
# shellcheck disable=SC2016 # $displaypath is expanded by git submodule foreach
|
|
278
|
+
while IFS= read -r sub; do
|
|
279
|
+
[[ -n "$sub" ]] || continue
|
|
280
|
+
repos+=("$1/$sub") nested+=(1)
|
|
281
|
+
done < <(git -C "$1" submodule foreach --quiet --recursive 'printf "%s\n" "$displaypath"' 2>/dev/null)
|
|
282
|
+
}
|
|
283
|
+
|
|
242
284
|
walk "$root" 0
|
|
243
285
|
|
|
244
286
|
count=${#repos[@]}
|
|
@@ -256,6 +298,8 @@ failed_repos=() dirty_repos=()
|
|
|
256
298
|
|
|
257
299
|
consume() {
|
|
258
300
|
local text="$1" head="${1%%$'\n'*}" body="${1#*$'\n'}"
|
|
301
|
+
# a submodule's lines sit under its parent's
|
|
302
|
+
[[ "${2:-0}" == 0 ]] || body=" ${body//$'\n'/$'\n' }"
|
|
259
303
|
local category="${head%%$'\t'*}" label="${head#*$'\t'}"
|
|
260
304
|
case "$category" in
|
|
261
305
|
updated) updated=$((updated + 1)) ;;
|
|
@@ -272,8 +316,8 @@ if ((jobs > 1 && count > 1)); then
|
|
|
272
316
|
scratch="$(mktemp -d)"
|
|
273
317
|
trap 'rm -rf "$scratch"' EXIT
|
|
274
318
|
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
|
|
319
|
+
export mode rebase submodules root green yellow red dim reset
|
|
320
|
+
export -f worker pull_repo dryrun_repo repo_label indented prepare report_failed report_diverged report_dirty report_current report_updated report_synced commits has_tracked_changes has_local_changes sync_submodules changes_block_update
|
|
277
321
|
# shellcheck disable=SC2016 # the single-quoted body runs inside the child shell
|
|
278
322
|
for ((i = 0; i < count; i++)); do printf '%s\0%s\0' "$i" "${repos[i]}"; done |
|
|
279
323
|
xargs -0 -n 2 -P "$jobs" "$BASH" -c 'worker "$@"' _ "$scratch" &
|
|
@@ -287,12 +331,12 @@ if ((jobs > 1 && count > 1)); then
|
|
|
287
331
|
fi
|
|
288
332
|
sleep 0.1
|
|
289
333
|
done
|
|
290
|
-
consume "$(cat "$scratch/$i.done")"
|
|
334
|
+
consume "$(cat "$scratch/$i.done")" "${nested[i]}"
|
|
291
335
|
done
|
|
292
336
|
wait "$pool" || true
|
|
293
337
|
else
|
|
294
|
-
for
|
|
295
|
-
consume "$("${mode}_repo" "$
|
|
338
|
+
for ((i = 0; i < count; i++)); do
|
|
339
|
+
consume "$("${mode}_repo" "${repos[i]}")" "${nested[i]}"
|
|
296
340
|
done
|
|
297
341
|
fi
|
|
298
342
|
|