@kova1/pullr 0.3.1 → 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 +39 -30
- package/bin/pullr +122 -78
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,13 +12,15 @@ everything current means `cd`-ing into each one.
|
|
|
12
12
|
|
|
13
13
|
```console
|
|
14
14
|
$ pullr ~/work
|
|
15
|
-
+ api (main, 3 new
|
|
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
|
|
|
@@ -32,6 +34,12 @@ brew install kova1max/tap/pullr
|
|
|
32
34
|
npm install -g @kova1/pullr
|
|
33
35
|
```
|
|
34
36
|
|
|
37
|
+
To try it without installing anything:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npx @kova1/pullr --dry-run ~/work
|
|
41
|
+
```
|
|
42
|
+
|
|
35
43
|
Or copy [`bin/pullr`](bin/pullr) anywhere on your `PATH` - it is a single bash
|
|
36
44
|
script with no dependencies beyond `git`.
|
|
37
45
|
|
|
@@ -40,6 +48,26 @@ Every release is also mirrored to
|
|
|
40
48
|
`@kova1max/pullr` (GitHub requires the repo owner as scope). Installing from
|
|
41
49
|
there needs a GitHub token, so the npm registry above is the easier choice.
|
|
42
50
|
|
|
51
|
+
## What it will never do
|
|
52
|
+
|
|
53
|
+
pullr is built to be safe to run across a whole workspace without looking
|
|
54
|
+
first. It will never:
|
|
55
|
+
|
|
56
|
+
- **Create a merge commit.** Pulls are fast-forward only.
|
|
57
|
+
- **Touch a branch that has diverged** from its upstream. It is reported as
|
|
58
|
+
failed and left exactly as it was. Rebasing it is opt-in with `--rebase`,
|
|
59
|
+
and a rebase that hits a conflict is aborted, leaving the repository as it
|
|
60
|
+
was.
|
|
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.
|
|
65
|
+
- **Touch a repository on a detached `HEAD`**, or a branch with no upstream.
|
|
66
|
+
Both are skipped.
|
|
67
|
+
- **Enter a nested repository.** Repositories inside another repository,
|
|
68
|
+
including submodules, are left to their parent.
|
|
69
|
+
- **Follow symlinked directories.**
|
|
70
|
+
|
|
43
71
|
## Usage
|
|
44
72
|
|
|
45
73
|
```
|
|
@@ -53,25 +81,17 @@ pullr [options] [DIR]
|
|
|
53
81
|
| `--max-depth N` | `2` | How many directory levels below `DIR` to search. `0` = only `DIR` itself, `1` = `DIR` and its direct children, ... |
|
|
54
82
|
| `-n`, `--dry-run` | | Show what a run would do without pulling, see below |
|
|
55
83
|
| `-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. |
|
|
84
|
+
| `--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. |
|
|
56
85
|
| `-V`, `--version` | | Print the version |
|
|
57
86
|
| `-h`, `--help` | | Show help |
|
|
58
87
|
|
|
59
|
-
|
|
88
|
+
### Output
|
|
60
89
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
reported as failed.
|
|
67
|
-
- Does not descend into a repository once found - nested repositories and
|
|
68
|
-
submodules are left to their parent.
|
|
69
|
-
- Skips repositories on a detached `HEAD` and branches with no upstream.
|
|
70
|
-
- Does not follow symlinked directories.
|
|
71
|
-
- Exits `1` if any pull failed, `2` on invalid arguments, `0` otherwise.
|
|
72
|
-
|
|
73
|
-
Each repository gets one line: `+` updated, `=` already up to date, `-`
|
|
74
|
-
skipped, `x` failed (with git's output indented below it).
|
|
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.
|
|
75
95
|
|
|
76
96
|
### Dry run
|
|
77
97
|
|
|
@@ -86,7 +106,7 @@ x infra (main)
|
|
|
86
106
|
diverged from upstream (behind 2, ahead 1): cannot fast-forward
|
|
87
107
|
- scratch (main has no upstream)
|
|
88
108
|
|
|
89
|
-
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
|
|
90
110
|
Failed: infra
|
|
91
111
|
```
|
|
92
112
|
|
|
@@ -97,17 +117,6 @@ remote as it is now, then prints the same lines a real run would (with
|
|
|
97
117
|
only updates remote-tracking refs: the working tree and current branch are
|
|
98
118
|
never modified. Exit codes match a real run.
|
|
99
119
|
|
|
100
|
-
## Development
|
|
101
|
-
|
|
102
|
-
```sh
|
|
103
|
-
npm install # installs bats
|
|
104
|
-
npm test # end-to-end tests against real temporary git repos
|
|
105
|
-
npm run lint # shellcheck
|
|
106
|
-
PULLR_BASH=/bin/bash npm test # run the suite under macOS's bash 3.2
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
Releases are cut by GitHub Actions - see [RELEASE.md](RELEASE.md).
|
|
110
|
-
|
|
111
120
|
## License
|
|
112
121
|
|
|
113
122
|
[MIT](LICENSE)
|
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] [DIR]
|
|
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'
|
|
@@ -24,6 +24,8 @@ 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-color Disable colored output. Also disabled when NO_COLOR is
|
|
28
|
+
set (https://no-color.org) or output is not a terminal.
|
|
27
29
|
-V, --version Print the version.
|
|
28
30
|
-h, --help Show this help.
|
|
29
31
|
EOF
|
|
@@ -32,6 +34,7 @@ EOF
|
|
|
32
34
|
mode=pull
|
|
33
35
|
jobs=4
|
|
34
36
|
rebase=0
|
|
37
|
+
color=1
|
|
35
38
|
max_depth=2
|
|
36
39
|
root=""
|
|
37
40
|
|
|
@@ -43,6 +46,7 @@ while (($#)); do
|
|
|
43
46
|
--max-depth=*) max_depth="${1#*=}"; shift ;;
|
|
44
47
|
-n|--dry-run) mode=dryrun; shift ;;
|
|
45
48
|
-r|--rebase) rebase=1; shift ;;
|
|
49
|
+
--no-color) color=0; shift ;;
|
|
46
50
|
-h|--help) usage; exit 0 ;;
|
|
47
51
|
-V|--version) echo "pullr $VERSION"; exit 0 ;;
|
|
48
52
|
-*) echo "Unknown option: $1" >&2; usage >&2; exit 2 ;;
|
|
@@ -58,7 +62,8 @@ root="${root:-.}"
|
|
|
58
62
|
[[ -d "$root" ]] || { echo "Not a directory: $root" >&2; exit 2; }
|
|
59
63
|
root="$(cd "$root" && pwd)"
|
|
60
64
|
|
|
61
|
-
|
|
65
|
+
# https://no-color.org: any non-empty NO_COLOR disables color
|
|
66
|
+
if [[ "$color" == 1 && -z "${NO_COLOR:-}" && -t 1 ]]; then
|
|
62
67
|
green=$'\e[32m' yellow=$'\e[33m' red=$'\e[31m' dim=$'\e[2m' reset=$'\e[0m'
|
|
63
68
|
else
|
|
64
69
|
green="" yellow="" red="" dim="" reset=""
|
|
@@ -79,96 +84,133 @@ indented() {
|
|
|
79
84
|
printf ' %s\n' "${1//$'\n'/$'\n' }"
|
|
80
85
|
}
|
|
81
86
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
86
92
|
if ! branch="$(git -C "$repo" symbolic-ref --quiet --short HEAD)"; then
|
|
87
93
|
echo skipped
|
|
88
94
|
printf '%s-%s %s %s(detached HEAD)%s\n' "$yellow" "$reset" "$rel" "$dim" "$reset"
|
|
89
|
-
return
|
|
95
|
+
return 1
|
|
90
96
|
fi
|
|
91
97
|
if ! git -C "$repo" rev-parse --quiet --verify '@{upstream}' >/dev/null 2>&1; then
|
|
92
98
|
echo skipped
|
|
93
99
|
printf '%s-%s %s %s(%s has no upstream)%s\n' "$yellow" "$reset" "$rel" "$dim" "$branch" "$reset"
|
|
94
|
-
return
|
|
100
|
+
return 1
|
|
95
101
|
fi
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
before="$(git -C "$repo" rev-parse HEAD)"
|
|
100
|
-
if ! out="$(git -C "$repo" pull "$how" 2>&1)"; then
|
|
101
|
-
# a conflicting rebase must not leave the repository mid-rebase
|
|
102
|
-
[[ "$rebase" == 0 ]] || git -C "$repo" rebase --abort >/dev/null 2>&1 || true
|
|
103
|
-
printf 'failed\t%s\n' "$rel"
|
|
104
|
-
printf '%sx%s %s %s(%s)%s\n' "$red" "$reset" "$rel" "$dim" "$branch" "$reset"
|
|
105
|
-
indented "$out"
|
|
106
|
-
return
|
|
102
|
+
if ! out="$(git -C "$repo" fetch --quiet 2>&1)"; then
|
|
103
|
+
report_failed "$rel" "$branch" "$out"
|
|
104
|
+
return 1
|
|
107
105
|
fi
|
|
108
|
-
|
|
106
|
+
counts="$(git -C "$repo" rev-list --left-right --count '@{upstream}...HEAD')"
|
|
107
|
+
behind="${counts%%[[:space:]]*}"
|
|
108
|
+
ahead="${counts##*[[:space:]]}"
|
|
109
|
+
}
|
|
109
110
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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"
|
|
119
136
|
echo updated
|
|
120
|
-
printf '%s+%s %s %s(%s, %s)%s\n' "$green" "$reset" "$
|
|
121
|
-
"$(update_note "$n" "$local_n" rebased)" "$reset"
|
|
137
|
+
printf '%s+%s %s %s(%s, %s)%s\n' "$green" "$reset" "$1" "$dim" "$2" "$note" "$reset"
|
|
122
138
|
}
|
|
123
139
|
|
|
124
|
-
# "
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
note="$n new commit$([[ "$n" == 1 ]] || printf s)"
|
|
128
|
-
[[ "$local_n" == 0 ]] || note+=", $local_n $verb"
|
|
129
|
-
printf '%s' "$note"
|
|
140
|
+
# "1 commit", "3 commits"
|
|
141
|
+
commits() {
|
|
142
|
+
printf '%s commit%s' "$1" "$([[ "$1" == 1 ]] || printf s)"
|
|
130
143
|
}
|
|
131
144
|
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
134
152
|
rel="$(repo_label "$repo")"
|
|
153
|
+
prepare "$repo" "$rel" || return 0
|
|
135
154
|
|
|
136
|
-
if
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
145
182
|
fi
|
|
183
|
+
}
|
|
146
184
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
+
}
|
|
154
194
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
195
|
+
dryrun_repo() {
|
|
196
|
+
local repo="$1" rel branch behind ahead
|
|
197
|
+
rel="$(repo_label "$repo")"
|
|
198
|
+
prepare "$repo" "$rel" || return 0
|
|
159
199
|
|
|
160
|
-
if [[ "$behind"
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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"
|
|
169
212
|
else
|
|
170
|
-
|
|
171
|
-
printf '%s=%s %s %s(%s)%s\n' "$dim" "$reset" "$rel" "$dim" "$branch" "$reset"
|
|
213
|
+
report_updated "$rel" "$branch" "$behind" "$ahead" "to rebase"
|
|
172
214
|
fi
|
|
173
215
|
}
|
|
174
216
|
|
|
@@ -209,8 +251,8 @@ fi
|
|
|
209
251
|
|
|
210
252
|
[[ "$mode" != dryrun ]] || printf '%sDry run: nothing will be pulled.%s\n' "$dim" "$reset"
|
|
211
253
|
|
|
212
|
-
updated=0 current=0 skipped=0 failed=0
|
|
213
|
-
failed_repos=()
|
|
254
|
+
updated=0 current=0 skipped=0 failed=0 dirty=0
|
|
255
|
+
failed_repos=() dirty_repos=()
|
|
214
256
|
|
|
215
257
|
consume() {
|
|
216
258
|
local text="$1" head="${1%%$'\n'*}" body="${1#*$'\n'}"
|
|
@@ -219,6 +261,7 @@ consume() {
|
|
|
219
261
|
updated) updated=$((updated + 1)) ;;
|
|
220
262
|
current) current=$((current + 1)) ;;
|
|
221
263
|
skipped) skipped=$((skipped + 1)) ;;
|
|
264
|
+
dirty) dirty=$((dirty + 1)); dirty_repos+=("$label") ;;
|
|
222
265
|
failed) failed=$((failed + 1)); failed_repos+=("$label") ;;
|
|
223
266
|
*) echo "pullr: unexpected worker result: $text" >&2; exit 1 ;;
|
|
224
267
|
esac
|
|
@@ -230,7 +273,7 @@ if ((jobs > 1 && count > 1)); then
|
|
|
230
273
|
trap 'rm -rf "$scratch"' EXIT
|
|
231
274
|
export GIT_TERMINAL_PROMPT=0
|
|
232
275
|
export mode rebase root green yellow red dim reset
|
|
233
|
-
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
|
|
234
277
|
# shellcheck disable=SC2016 # the single-quoted body runs inside the child shell
|
|
235
278
|
for ((i = 0; i < count; i++)); do printf '%s\0%s\0' "$i" "${repos[i]}"; done |
|
|
236
279
|
xargs -0 -n 2 -P "$jobs" "$BASH" -c 'worker "$@"' _ "$scratch" &
|
|
@@ -255,12 +298,13 @@ fi
|
|
|
255
298
|
|
|
256
299
|
echo
|
|
257
300
|
if [[ "$mode" == dryrun ]]; then
|
|
258
|
-
printf 'Repos: %d %swould update: %d%s up to date: %d %
|
|
259
|
-
"$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"
|
|
260
303
|
else
|
|
261
|
-
printf 'Repos: %d %supdated: %d%s up to date: %d %
|
|
262
|
-
"$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"
|
|
263
306
|
fi
|
|
307
|
+
((dirty == 0)) || printf 'Dirty: %s\n' "${dirty_repos[*]}"
|
|
264
308
|
if ((failed)); then
|
|
265
309
|
printf 'Failed: %s\n' "${failed_repos[*]}"
|
|
266
310
|
exit 1
|