@kova1/pullr 0.4.0 → 0.5.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 +14 -8
- package/bin/pullr +115 -76
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,11 +14,13 @@ everything current means `cd`-ing into each one.
|
|
|
14
14
|
$ pullr ~/work
|
|
15
15
|
+ api (main, 3 new commits)
|
|
16
16
|
= web (main)
|
|
17
|
+
~ docs (main, 2 commits held back by local changes)
|
|
17
18
|
- scratch (main has no upstream)
|
|
18
19
|
x infra (main)
|
|
19
|
-
|
|
20
|
+
diverged from upstream (behind 2, ahead 1): cannot fast-forward
|
|
20
21
|
|
|
21
|
-
Repos:
|
|
22
|
+
Repos: 5 updated: 1 up to date: 1 dirty: 1 skipped: 1 failed: 1
|
|
23
|
+
Dirty: docs
|
|
22
24
|
Failed: infra
|
|
23
25
|
```
|
|
24
26
|
|
|
@@ -56,8 +58,10 @@ first. It will never:
|
|
|
56
58
|
failed and left exactly as it was. Rebasing it is opt-in with `--rebase`,
|
|
57
59
|
and a rebase that hits a conflict is aborted, leaving the repository as it
|
|
58
60
|
was.
|
|
59
|
-
- **Stash or discard your changes.** There is no auto-stash
|
|
60
|
-
|
|
61
|
+
- **Stash, overwrite or discard your changes.** There is no auto-stash. If
|
|
62
|
+
local edits or untracked files are in the way of incoming changes, the
|
|
63
|
+
repository is reported as dirty and left as it was; edits that don't touch
|
|
64
|
+
incoming files don't stop the update.
|
|
61
65
|
- **Touch a repository on a detached `HEAD`**, or a branch with no upstream.
|
|
62
66
|
Both are skipped.
|
|
63
67
|
- **Enter a nested repository.** Repositories inside another repository,
|
|
@@ -83,9 +87,11 @@ pullr [options] [DIR]
|
|
|
83
87
|
|
|
84
88
|
### Output
|
|
85
89
|
|
|
86
|
-
Each repository gets one line: `+` updated, `=` already up to date,
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
Each repository gets one line: `+` updated, `=` already up to date, `~`
|
|
91
|
+
dirty (local changes are in the way of the update, which is held back), `-`
|
|
92
|
+
skipped, `x` failed (with the reason indented below it). Exits `1` if any
|
|
93
|
+
pull failed, `2` on invalid arguments, `0` otherwise; a dirty repository is
|
|
94
|
+
not a failure.
|
|
89
95
|
|
|
90
96
|
### Dry run
|
|
91
97
|
|
|
@@ -100,7 +106,7 @@ x infra (main)
|
|
|
100
106
|
diverged from upstream (behind 2, ahead 1): cannot fast-forward
|
|
101
107
|
- scratch (main has no upstream)
|
|
102
108
|
|
|
103
|
-
Repos: 4 would update: 1 up to date: 1 skipped: 1 would fail: 1
|
|
109
|
+
Repos: 4 would update: 1 up to date: 1 dirty: 0 skipped: 1 would fail: 1
|
|
104
110
|
Failed: infra
|
|
105
111
|
```
|
|
106
112
|
|
package/bin/pullr
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# Usage: pullr [--jobs N] [--max-depth N] [--dry-run] [--rebase] [--no-color] [DIR]
|
|
4
4
|
set -euo pipefail
|
|
5
5
|
|
|
6
|
-
VERSION="0.
|
|
6
|
+
VERSION="0.5.0" # set by the release workflow
|
|
7
7
|
|
|
8
8
|
usage() {
|
|
9
9
|
cat <<'EOF'
|
|
@@ -84,96 +84,133 @@ indented() {
|
|
|
84
84
|
printf ' %s\n' "${1//$'\n'/$'\n' }"
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
# Shared first half of a pull and a dry run: check the branch, fetch, and
|
|
88
|
+
# count commits. On success it sets the caller's branch, behind and ahead;
|
|
89
|
+
# otherwise it prints the result line and returns 1.
|
|
90
|
+
prepare() {
|
|
91
|
+
local repo="$1" rel="$2" out counts
|
|
91
92
|
if ! branch="$(git -C "$repo" symbolic-ref --quiet --short HEAD)"; then
|
|
92
93
|
echo skipped
|
|
93
94
|
printf '%s-%s %s %s(detached HEAD)%s\n' "$yellow" "$reset" "$rel" "$dim" "$reset"
|
|
94
|
-
return
|
|
95
|
+
return 1
|
|
95
96
|
fi
|
|
96
97
|
if ! git -C "$repo" rev-parse --quiet --verify '@{upstream}' >/dev/null 2>&1; then
|
|
97
98
|
echo skipped
|
|
98
99
|
printf '%s-%s %s %s(%s has no upstream)%s\n' "$yellow" "$reset" "$rel" "$dim" "$branch" "$reset"
|
|
99
|
-
return
|
|
100
|
+
return 1
|
|
100
101
|
fi
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
before="$(git -C "$repo" rev-parse HEAD)"
|
|
105
|
-
if ! out="$(git -C "$repo" pull "$how" 2>&1)"; then
|
|
106
|
-
# a conflicting rebase must not leave the repository mid-rebase
|
|
107
|
-
[[ "$rebase" == 0 ]] || git -C "$repo" rebase --abort >/dev/null 2>&1 || true
|
|
108
|
-
printf 'failed\t%s\n' "$rel"
|
|
109
|
-
printf '%sx%s %s %s(%s)%s\n' "$red" "$reset" "$rel" "$dim" "$branch" "$reset"
|
|
110
|
-
indented "$out"
|
|
111
|
-
return
|
|
102
|
+
if ! out="$(git -C "$repo" fetch --quiet 2>&1)"; then
|
|
103
|
+
report_failed "$rel" "$branch" "$out"
|
|
104
|
+
return 1
|
|
112
105
|
fi
|
|
113
|
-
|
|
106
|
+
counts="$(git -C "$repo" rev-list --left-right --count '@{upstream}...HEAD')"
|
|
107
|
+
behind="${counts%%[[:space:]]*}"
|
|
108
|
+
ahead="${counts##*[[:space:]]}"
|
|
109
|
+
}
|
|
114
110
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
111
|
+
report_failed() {
|
|
112
|
+
printf 'failed\t%s\n' "$1"
|
|
113
|
+
printf '%sx%s %s %s(%s)%s\n' "$red" "$reset" "$1" "$dim" "$2" "$reset"
|
|
114
|
+
indented "$3"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
report_diverged() {
|
|
118
|
+
report_failed "$1" "$2" "diverged from upstream (behind $3, ahead $4): cannot fast-forward"
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
report_dirty() {
|
|
122
|
+
printf 'dirty\t%s\n' "$1"
|
|
123
|
+
printf '%s~%s %s %s(%s, %s held back by local changes)%s\n' \
|
|
124
|
+
"$yellow" "$reset" "$1" "$dim" "$2" "$(commits "$3")" "$reset"
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
report_current() {
|
|
128
|
+
echo current
|
|
129
|
+
printf '%s=%s %s %s(%s)%s\n' "$dim" "$reset" "$1" "$dim" "$2" "$reset"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
report_updated() {
|
|
133
|
+
local note
|
|
134
|
+
note="$3 new commit$([[ "$3" == 1 ]] || printf s)"
|
|
135
|
+
[[ "$4" == 0 ]] || note+=", $4 $5"
|
|
124
136
|
echo updated
|
|
125
|
-
printf '%s+%s %s %s(%s, %s)%s\n' "$green" "$reset" "$
|
|
126
|
-
"$(update_note "$n" "$local_n" rebased)" "$reset"
|
|
137
|
+
printf '%s+%s %s %s(%s, %s)%s\n' "$green" "$reset" "$1" "$dim" "$2" "$note" "$reset"
|
|
127
138
|
}
|
|
128
139
|
|
|
129
|
-
# "
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
note="$n new commit$([[ "$n" == 1 ]] || printf s)"
|
|
133
|
-
[[ "$local_n" == 0 ]] || note+=", $local_n $verb"
|
|
134
|
-
printf '%s' "$note"
|
|
140
|
+
# "1 commit", "3 commits"
|
|
141
|
+
commits() {
|
|
142
|
+
printf '%s commit%s' "$1" "$([[ "$1" == 1 ]] || printf s)"
|
|
135
143
|
}
|
|
136
144
|
|
|
137
|
-
|
|
138
|
-
|
|
145
|
+
# Uncommitted changes to tracked files, staged or not.
|
|
146
|
+
has_tracked_changes() {
|
|
147
|
+
! git -C "$1" diff --quiet HEAD --
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
pull_repo() {
|
|
151
|
+
local repo="$1" rel branch behind ahead out
|
|
139
152
|
rel="$(repo_label "$repo")"
|
|
153
|
+
prepare "$repo" "$rel" || return 0
|
|
140
154
|
|
|
141
|
-
if
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
155
|
+
if [[ "$behind" == 0 ]]; then
|
|
156
|
+
report_current "$rel" "$branch"
|
|
157
|
+
elif [[ "$ahead" == 0 ]]; then
|
|
158
|
+
# git refuses, touching nothing, when local changes are in the way
|
|
159
|
+
if out="$(git -C "$repo" merge --ff-only --quiet '@{upstream}' 2>&1)"; then
|
|
160
|
+
report_updated "$rel" "$branch" "$behind" 0 ""
|
|
161
|
+
elif [[ -n "$(git -C "$repo" status --porcelain)" ]]; then
|
|
162
|
+
report_dirty "$rel" "$branch" "$behind"
|
|
163
|
+
else
|
|
164
|
+
report_failed "$rel" "$branch" "$out"
|
|
165
|
+
fi
|
|
166
|
+
elif [[ "$rebase" == 0 ]]; then
|
|
167
|
+
report_diverged "$rel" "$branch" "$behind" "$ahead"
|
|
168
|
+
elif has_tracked_changes "$repo"; then
|
|
169
|
+
report_dirty "$rel" "$branch" "$behind"
|
|
170
|
+
elif out="$(git -C "$repo" rebase --quiet '@{upstream}' 2>&1)"; then
|
|
171
|
+
report_updated "$rel" "$branch" "$behind" "$ahead" rebased
|
|
172
|
+
else
|
|
173
|
+
# A rebase that stopped on a conflict must not leave the repository
|
|
174
|
+
# mid-rebase; --abort succeeds only when one is in progress.
|
|
175
|
+
if git -C "$repo" rebase --abort >/dev/null 2>&1; then
|
|
176
|
+
report_failed "$rel" "$branch" "$out"
|
|
177
|
+
elif [[ -n "$(git -C "$repo" status --porcelain)" ]]; then
|
|
178
|
+
report_dirty "$rel" "$branch" "$behind"
|
|
179
|
+
else
|
|
180
|
+
report_failed "$rel" "$branch" "$out"
|
|
181
|
+
fi
|
|
150
182
|
fi
|
|
183
|
+
}
|
|
151
184
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
185
|
+
# Would local changes stop a fast-forward? Git refuses when a file it has to
|
|
186
|
+
# update is modified locally or exists untracked, so compare the two lists.
|
|
187
|
+
changes_block_update() {
|
|
188
|
+
local repo="$1" incoming local_paths
|
|
189
|
+
incoming="$(git -C "$repo" diff --name-only HEAD '@{upstream}')"
|
|
190
|
+
local_paths="$(git -C "$repo" status --porcelain --untracked-files=all | cut -c4-)"
|
|
191
|
+
[[ -n "$incoming" && -n "$local_paths" ]] || return 1
|
|
192
|
+
printf '%s\n' "$local_paths" | grep -qFxf <(printf '%s\n' "$incoming")
|
|
193
|
+
}
|
|
159
194
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
195
|
+
dryrun_repo() {
|
|
196
|
+
local repo="$1" rel branch behind ahead
|
|
197
|
+
rel="$(repo_label "$repo")"
|
|
198
|
+
prepare "$repo" "$rel" || return 0
|
|
164
199
|
|
|
165
|
-
if [[ "$behind"
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
200
|
+
if [[ "$behind" == 0 ]]; then
|
|
201
|
+
report_current "$rel" "$branch"
|
|
202
|
+
elif [[ "$ahead" == 0 ]]; then
|
|
203
|
+
if changes_block_update "$repo"; then
|
|
204
|
+
report_dirty "$rel" "$branch" "$behind"
|
|
205
|
+
else
|
|
206
|
+
report_updated "$rel" "$branch" "$behind" 0 ""
|
|
207
|
+
fi
|
|
208
|
+
elif [[ "$rebase" == 0 ]]; then
|
|
209
|
+
report_diverged "$rel" "$branch" "$behind" "$ahead"
|
|
210
|
+
elif has_tracked_changes "$repo"; then
|
|
211
|
+
report_dirty "$rel" "$branch" "$behind"
|
|
174
212
|
else
|
|
175
|
-
|
|
176
|
-
printf '%s=%s %s %s(%s)%s\n' "$dim" "$reset" "$rel" "$dim" "$branch" "$reset"
|
|
213
|
+
report_updated "$rel" "$branch" "$behind" "$ahead" "to rebase"
|
|
177
214
|
fi
|
|
178
215
|
}
|
|
179
216
|
|
|
@@ -214,8 +251,8 @@ fi
|
|
|
214
251
|
|
|
215
252
|
[[ "$mode" != dryrun ]] || printf '%sDry run: nothing will be pulled.%s\n' "$dim" "$reset"
|
|
216
253
|
|
|
217
|
-
updated=0 current=0 skipped=0 failed=0
|
|
218
|
-
failed_repos=()
|
|
254
|
+
updated=0 current=0 skipped=0 failed=0 dirty=0
|
|
255
|
+
failed_repos=() dirty_repos=()
|
|
219
256
|
|
|
220
257
|
consume() {
|
|
221
258
|
local text="$1" head="${1%%$'\n'*}" body="${1#*$'\n'}"
|
|
@@ -224,6 +261,7 @@ consume() {
|
|
|
224
261
|
updated) updated=$((updated + 1)) ;;
|
|
225
262
|
current) current=$((current + 1)) ;;
|
|
226
263
|
skipped) skipped=$((skipped + 1)) ;;
|
|
264
|
+
dirty) dirty=$((dirty + 1)); dirty_repos+=("$label") ;;
|
|
227
265
|
failed) failed=$((failed + 1)); failed_repos+=("$label") ;;
|
|
228
266
|
*) echo "pullr: unexpected worker result: $text" >&2; exit 1 ;;
|
|
229
267
|
esac
|
|
@@ -235,7 +273,7 @@ if ((jobs > 1 && count > 1)); then
|
|
|
235
273
|
trap 'rm -rf "$scratch"' EXIT
|
|
236
274
|
export GIT_TERMINAL_PROMPT=0
|
|
237
275
|
export mode rebase root green yellow red dim reset
|
|
238
|
-
export -f worker pull_repo dryrun_repo repo_label indented
|
|
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
|
|
239
277
|
# shellcheck disable=SC2016 # the single-quoted body runs inside the child shell
|
|
240
278
|
for ((i = 0; i < count; i++)); do printf '%s\0%s\0' "$i" "${repos[i]}"; done |
|
|
241
279
|
xargs -0 -n 2 -P "$jobs" "$BASH" -c 'worker "$@"' _ "$scratch" &
|
|
@@ -260,12 +298,13 @@ fi
|
|
|
260
298
|
|
|
261
299
|
echo
|
|
262
300
|
if [[ "$mode" == dryrun ]]; then
|
|
263
|
-
printf 'Repos: %d %swould update: %d%s up to date: %d %
|
|
264
|
-
"$count" "$green" "$updated" "$reset" "$current" "$yellow" "$skipped" "$reset" "$red" "$failed" "$reset"
|
|
301
|
+
printf 'Repos: %d %swould update: %d%s up to date: %d %sdirty: %d skipped: %d%s %swould fail: %d%s\n' \
|
|
302
|
+
"$count" "$green" "$updated" "$reset" "$current" "$yellow" "$dirty" "$skipped" "$reset" "$red" "$failed" "$reset"
|
|
265
303
|
else
|
|
266
|
-
printf 'Repos: %d %supdated: %d%s up to date: %d %
|
|
267
|
-
"$count" "$green" "$updated" "$reset" "$current" "$yellow" "$skipped" "$reset" "$red" "$failed" "$reset"
|
|
304
|
+
printf 'Repos: %d %supdated: %d%s up to date: %d %sdirty: %d skipped: %d%s %sfailed: %d%s\n' \
|
|
305
|
+
"$count" "$green" "$updated" "$reset" "$current" "$yellow" "$dirty" "$skipped" "$reset" "$red" "$failed" "$reset"
|
|
268
306
|
fi
|
|
307
|
+
((dirty == 0)) || printf 'Dirty: %s\n' "${dirty_repos[*]}"
|
|
269
308
|
if ((failed)); then
|
|
270
309
|
printf 'Failed: %s\n' "${failed_repos[*]}"
|
|
271
310
|
exit 1
|