git-review-workflow 0.1.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.
@@ -0,0 +1,167 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # git review save — pause the review on the current branch and put it aside.
4
+ #
5
+ # A review in progress lives on a review/<branch> branch. Saving it converts that
6
+ # branch into review-saved/<branch> and returns you to where you started, so you
7
+ # can pick up something else and resume later with git review continue.
8
+ #
9
+ # The saved review carries everything needed to resume exactly where you left off:
10
+ # whole mode the staged PR diff and your uncommitted edits, captured as a
11
+ # commit whose diff against the tip is just your edits.
12
+ # step mode the step you are on, the edits on it, and every edit you have
13
+ # banked on other commits — the banked-edit refs are moved out of
14
+ # refs/review-edits/ (which git review clean prunes) into
15
+ # refs/review-saved-edits/ so a git review clean never touches them.
16
+ #
17
+ set -eu
18
+
19
+ prog="git review save"
20
+
21
+ usage() {
22
+ cat <<EOF
23
+ usage: $prog
24
+
25
+ Pause the review on the current review/* branch: stash it as review-saved/<branch>
26
+ and return to the branch you started from. Resume it later with git review continue.
27
+ EOF
28
+ }
29
+
30
+ die() {
31
+ echo "error: $1" >&2
32
+ exit 1
33
+ }
34
+
35
+ case "${1:-}" in
36
+ -h | --h)
37
+ usage
38
+ exit 0
39
+ ;;
40
+ "") ;;
41
+ *)
42
+ echo "error: unexpected argument $1" >&2
43
+ usage >&2
44
+ exit 1
45
+ ;;
46
+ esac
47
+
48
+ cur="$(git symbolic-ref --quiet --short HEAD || true)"
49
+ case "$cur" in
50
+ review/*) ;;
51
+ *)
52
+ echo "error: not on a review/* branch (HEAD is ${cur:-detached})" >&2
53
+ exit 1
54
+ ;;
55
+ esac
56
+
57
+ src="$(git config "branch.$cur.reviewsource" || true)"
58
+ tip="$(git config "branch.$cur.reviewtip" || true)"
59
+ if [ -z "$src" ] || [ -z "$tip" ]; then
60
+ die "missing review metadata; was $cur created with git review start?"
61
+ fi
62
+
63
+ saved="review-saved/$src"
64
+ git rev-parse --verify --quiet "refs/heads/$saved" >/dev/null &&
65
+ die "$saved already exists; resume it with git review continue or discard it with git review forget --saved $src"
66
+
67
+ mode="$(git config "branch.$cur.reviewmode" || echo whole)"
68
+
69
+ # Where to return to once the review is put aside — the branch you started from,
70
+ # falling back to the base branch, exactly like git review abort.
71
+ ret="$(git config "branch.$cur.reviewreturn" || true)"
72
+ base="$(git config "branch.$cur.reviewbase" || true)"
73
+ target=""
74
+ if [ -n "$ret" ] && git rev-parse --verify --quiet "refs/heads/$ret" >/dev/null; then
75
+ target="$ret"
76
+ elif [ -n "$base" ] && git rev-parse --verify --quiet "refs/heads/$base" >/dev/null; then
77
+ target="$base"
78
+ fi
79
+ [ -n "$target" ] || die "could not determine a branch to return to; switch away manually first"
80
+
81
+ # copy_meta <key>: copy branch.<cur>.<key> to branch.<saved>.<key> when set.
82
+ # The explicit if (rather than a && one-liner) keeps the function's exit status 0
83
+ # when the key is absent, so it does not trip set -e for the optional keys.
84
+ copy_meta() {
85
+ v="$(git config "branch.$cur.$1" || true)"
86
+ if [ -n "$v" ]; then
87
+ git config "branch.$saved.$1" "$v"
88
+ fi
89
+ }
90
+
91
+ if [ "$mode" = "step" ]; then
92
+ start="$(git config "branch.$cur.reviewstart" || true)"
93
+ count="$(git config "branch.$cur.reviewcount" || true)"
94
+ step="$(git config "branch.$cur.reviewstep" || true)"
95
+ # A key deleted by a hand-edit (while reviewmode stays "step") would otherwise
96
+ # let set -e kill the bare read silently mid-script; read with || true and report
97
+ # it. Unlike status/preview there is no later numeric guard, so cover step too.
98
+ if [ -z "$start" ] || [ -z "$count" ] || [ -z "$step" ]; then
99
+ die "missing review metadata; was $cur created with git review start?"
100
+ fi
101
+ commits="$(git rev-list --reverse --first-parent --no-merges "$start..$tip")"
102
+
103
+ # Bank the edits on the commit we are sitting on, the same way review-next does
104
+ # before it moves, so the current step's work joins the other banked steps.
105
+ cstep="$(printf '%s\n' "$commits" | sed -n "${step}p")"
106
+ git add -A
107
+ tree="$(git write-tree)"
108
+ if [ "$tree" != "$(git rev-parse "$cstep^{tree}")" ]; then
109
+ edit="$(git commit-tree "$tree" -p "$cstep" -m "review edits step $step")"
110
+ git update-ref "refs/review-edits/$src/$step" "$edit"
111
+ else
112
+ # Reverting the step back to a clean tree must clear any edits we banked
113
+ # earlier, or they resurrect when the saved review is restored.
114
+ git update-ref -d "refs/review-edits/$src/$step" 2>/dev/null || true
115
+ fi
116
+
117
+ # Move the banked edits aside so git review clean (which prunes refs/review-edits/)
118
+ # leaves the saved review intact, and so they travel with it.
119
+ git for-each-ref --format='%(refname)' "refs/review-edits/$src/" | while read -r ref; do
120
+ n="${ref##*/}"
121
+ git update-ref "refs/review-saved-edits/$src/$n" "$(git rev-parse "$ref")"
122
+ git update-ref -d "$ref"
123
+ done
124
+
125
+ # The visible branch is just an anchor + metadata holder; point it at the tip.
126
+ git update-ref "refs/heads/$saved" "$tip"
127
+ git config "branch.$saved.reviewmode" step
128
+ copy_meta reviewstart
129
+ copy_meta reviewcount
130
+ copy_meta reviewstep
131
+ note="on step $step/$count"
132
+ else
133
+ # whole mode: HEAD is the review's lower bound, the index holds the staged PR
134
+ # diff and the working tree holds that plus your edits. Capture the working tree
135
+ # as a commit parented on the tip, so its diff against the tip is exactly your
136
+ # edits — review-continue replays that to restore the editable state.
137
+ lower="$(git rev-parse HEAD)"
138
+ git add -A
139
+ tree="$(git write-tree)"
140
+ # Parent the saved commit on both the tip and the lower bound. The tip parent
141
+ # makes "git diff tip..saved" exactly your edits; the lower parent keeps the
142
+ # lower bound reachable even when it is a synthetic merge-tree commit (the
143
+ # folded-base case in git review start), so a gc between save and continue cannot
144
+ # prune it out from under "git reset --soft $lower".
145
+ commit="$(git commit-tree "$tree" -p "$tip" -p "$lower" -m "git review save: $src")"
146
+ git update-ref "refs/heads/$saved" "$commit"
147
+ git config "branch.$saved.reviewmode" whole
148
+ git config "branch.$saved.reviewsavedlower" "$lower"
149
+ note="whole-PR review"
150
+ fi
151
+
152
+ # Metadata common to both modes.
153
+ copy_meta reviewsource
154
+ copy_meta reviewbase
155
+ copy_meta reviewtip
156
+ copy_meta reviewprevreviewed
157
+ copy_meta reviewlocal
158
+ git config "branch.$saved.reviewreturn" "$target"
159
+
160
+ # Leave the review branch: drop its working-tree changes (captured above), return
161
+ # to the starting branch and delete review/<src>. Its config goes with it; the
162
+ # saved copy already holds everything.
163
+ git switch -q --discard-changes "$target"
164
+ git branch -D "$cur" >/dev/null
165
+
166
+ echo "saved review of $src ($note) as $saved; returned to $target"
167
+ echo "resume it with git review continue"
@@ -0,0 +1,333 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # git review start — stage a pull request diff on a review/<branch> branch.
4
+ #
5
+ # Two independent axes:
6
+ # range — where the review starts: the merge-base with the base branch
7
+ # (default), your last review (--delta), or an explicit commit
8
+ # (--from <commit>).
9
+ # layout — how you see it: the whole range as one staged diff (default), or
10
+ # one commit at a time (--step, advance with git review next).
11
+ #
12
+ set -eu
13
+
14
+ prog="git review start"
15
+
16
+ usage() {
17
+ cat <<EOF
18
+ usage: $prog [<branch>] [<base> | --base <base> | --delta | --from <commit>] [--step] [--local]
19
+
20
+ Stage a pull request diff on a review/<branch> branch for inline review.
21
+ With no <branch>, review the branch you currently have checked out.
22
+
23
+ <branch> branch to review; omit it to review the current branch
24
+ <base> commit-ish to diff against — a branch, tag or commit
25
+ (required for a full review; set a default with:
26
+ git config reviewworkflow.base <branch>)
27
+ --base <base> the base to diff against; use it to pass a base while
28
+ letting <branch> default to the current branch
29
+ --delta review only the commits added since your last review
30
+ --from <commit> review only the commits after <commit>
31
+ --step review one commit at a time; advance with git review next
32
+ --local review your local branches directly, without fetching
33
+ EOF
34
+ }
35
+
36
+ die() {
37
+ echo "error: $1" >&2
38
+ exit 1
39
+ }
40
+
41
+ src=""
42
+ base=""
43
+ base_explicit=0
44
+ from=""
45
+ delta=0
46
+ step=0
47
+ local_mode=0
48
+ while [ $# -gt 0 ]; do
49
+ case "$1" in
50
+ -h | --h)
51
+ usage
52
+ exit 0
53
+ ;;
54
+ -d | --delta) delta=1 ;;
55
+ --step) step=1 ;;
56
+ --local) local_mode=1 ;;
57
+ --base)
58
+ shift
59
+ [ $# -gt 0 ] || die "--base requires a base"
60
+ [ -n "$1" ] || die "--base requires a base"
61
+ [ "$base_explicit" -eq 0 ] || die "base given more than once"
62
+ base="$1"
63
+ base_explicit=1
64
+ ;;
65
+ --base=*)
66
+ [ "$base_explicit" -eq 0 ] || die "base given more than once"
67
+ base="${1#--base=}"
68
+ [ -n "$base" ] || die "--base requires a base"
69
+ base_explicit=1
70
+ ;;
71
+ --from)
72
+ shift
73
+ [ $# -gt 0 ] || die "--from requires a commit"
74
+ from="$1"
75
+ [ -n "$from" ] || die "--from requires a commit"
76
+ ;;
77
+ --from=*)
78
+ from="${1#--from=}"
79
+ [ -n "$from" ] || die "--from requires a commit"
80
+ ;;
81
+ --)
82
+ # End-of-options, the git convention: everything after is positional,
83
+ # so a branch whose name starts with "-" (or collides with a flag) can
84
+ # still be reviewed, e.g. git review start -- --weird develop.
85
+ shift
86
+ while [ $# -gt 0 ]; do
87
+ if [ -z "$src" ]; then
88
+ src="$1"
89
+ elif [ -z "$base" ]; then
90
+ base="$1"
91
+ base_explicit=1
92
+ else
93
+ die "unexpected argument $1"
94
+ fi
95
+ shift
96
+ done
97
+ break
98
+ ;;
99
+ -*)
100
+ echo "error: unknown option $1" >&2
101
+ usage >&2
102
+ exit 1
103
+ ;;
104
+ *)
105
+ if [ -z "$src" ]; then
106
+ src="$1"
107
+ elif [ -z "$base" ]; then
108
+ base="$1"
109
+ base_explicit=1
110
+ else
111
+ die "unexpected argument $1"
112
+ fi
113
+ ;;
114
+ esac
115
+ shift
116
+ done
117
+
118
+ if [ "$delta" -eq 1 ] && [ -n "$from" ]; then
119
+ die "use only one of --delta and --from"
120
+ fi
121
+
122
+ if [ "$base_explicit" -eq 1 ] && { [ "$delta" -eq 1 ] || [ -n "$from" ]; }; then
123
+ die "base is ignored with --delta/--from; pass only one"
124
+ fi
125
+
126
+ git rev-parse --git-dir >/dev/null 2>&1 || die "not a git repository"
127
+
128
+ # Shared helpers, sourced from the libexec directory the dispatcher points us at.
129
+ # shellcheck source=../git-review-lib.sh
130
+ . "${GIT_REVIEW_LIBEXEC:?}/git-review-lib.sh"
131
+
132
+ # With no <branch>, review the branch you currently have checked out. This is
133
+ # git's own default — push, status, log and rebase all act on the current branch
134
+ # when you name nothing. It only resolves the name; the mode (remote vs --local)
135
+ # is still chosen by flags. The check against the review/*, review-saved/* and
136
+ # review-fixes/* namespaces avoids the nonsense of reviewing a review branch.
137
+ if [ -z "$src" ]; then
138
+ cur="$(git symbolic-ref --quiet --short HEAD || true)"
139
+ [ -n "$cur" ] || die "no branch given and HEAD is detached; name a branch to review"
140
+ case "$cur" in
141
+ review/* | review-saved/* | review-fixes/*) die "refusing to review a review branch (HEAD is $cur); name a branch to review" ;;
142
+ esac
143
+ src="$cur"
144
+ fi
145
+
146
+ # Remote to review from; defaults to origin, override with reviewworkflow.remote.
147
+ remote="$(git config reviewworkflow.remote || echo origin)"
148
+
149
+ # Remember where to return to when the review is aborted.
150
+ returnbranch="$(git symbolic-ref --quiet --short HEAD || true)"
151
+
152
+ if [ -z "$base" ]; then
153
+ base="$(git config reviewworkflow.base || true)"
154
+ fi
155
+
156
+ if ! git diff --quiet || ! git diff --cached --quiet; then
157
+ die "you have local changes; commit or stash them first"
158
+ fi
159
+
160
+ # --local reviews your local branches directly; the default reviews the remote's.
161
+ # The two are kept apart everywhere: which ref the tip comes from, how it is named
162
+ # in messages, and the delta marker — local markers live in their own config
163
+ # section (reviewworkflowlocal.*) so a local and a remote review of the same branch
164
+ # name never overwrite each other's progress. A separate section, rather than a
165
+ # reviewworkflow.<src>.local.reviewed key, is deliberate: the latter would be
166
+ # ambiguous for a branch literally named "<src>.local".
167
+ if [ "$local_mode" -eq 1 ]; then
168
+ srcref="refs/heads/$src"
169
+ srclabel="$src"
170
+ markerkey="reviewworkflowlocal.$src.reviewed"
171
+ else
172
+ git fetch --quiet "$remote" || die "could not update from $remote"
173
+ srcref="refs/remotes/$remote/$src"
174
+ srclabel="$remote/$src"
175
+ markerkey="reviewworkflow.$src.reviewed"
176
+ fi
177
+
178
+ # Resolve the base into a ref and a human label. A base is normally a branch, so
179
+ # try the branch namespace first — the remote's copy, or the local branch with
180
+ # --local — to keep the familiar origin/<base> labels and the merged-base folding
181
+ # below. Anything that is not a branch there still works as a read-only lower
182
+ # bound via any commit-ish: a tag, a SHA, or an origin/<x> spelled out in full.
183
+ # baseref stays empty when the base does not resolve at all; the full-review path
184
+ # treats that as fatal ("not found"), while the delta/from folding treats an
185
+ # unresolvable base as a no-op, exactly as before.
186
+ baseref=""
187
+ baselabel="$base"
188
+ if [ -n "$base" ]; then
189
+ if [ "$local_mode" -eq 1 ]; then
190
+ if git rev-parse --verify --quiet "refs/heads/$base^{commit}" >/dev/null; then
191
+ baseref="refs/heads/$base"
192
+ fi
193
+ else
194
+ if git rev-parse --verify --quiet "refs/remotes/$remote/$base^{commit}" >/dev/null; then
195
+ baseref="refs/remotes/$remote/$base"
196
+ baselabel="$remote/$base"
197
+ fi
198
+ fi
199
+ if [ -z "$baseref" ]; then
200
+ bcommit="$(git rev-parse --verify --quiet "$base^{commit}" || true)"
201
+ if [ -n "$bcommit" ]; then
202
+ baseref="$bcommit"
203
+ fi
204
+ fi
205
+ fi
206
+
207
+ git rev-parse --verify --quiet "$srcref" >/dev/null ||
208
+ die "$srclabel not found"
209
+
210
+ rb="review/$src"
211
+ git rev-parse --verify --quiet "refs/heads/$rb" >/dev/null &&
212
+ die "$rb already exists; run git review clean $src first"
213
+ git rev-parse --verify --quiet "refs/heads/review-saved/$src" >/dev/null &&
214
+ die "you have a saved review of $src; resume it with git review continue $src or discard it with git review forget --saved $src"
215
+
216
+ tip="$(git rev-parse "$srcref")"
217
+ prev="$(git config "$markerkey" || true)"
218
+
219
+ # A remote review targets the remote's copy of the branch, never your local one,
220
+ # which git review start never updates. If a local branch of the same name exists
221
+ # and points elsewhere, you are reviewing a different snapshot than the one you
222
+ # have checked out — and a later git review finish --onto-source, which writes to
223
+ # the local branch, will refuse unless it sits at the reviewed tip. Warn rather than
224
+ # silently review the wrong thing; --local reviews what you have checked out.
225
+ if [ "$local_mode" -eq 0 ]; then
226
+ localtip="$(git rev-parse --verify --quiet "refs/heads/$src" || true)"
227
+ if [ -n "$localtip" ] && [ "$localtip" != "$tip" ]; then
228
+ echo "note: reviewing $srclabel ($(git rev-parse --short "$tip")), which differs from your local $src ($(git rev-parse --short "$localtip")); use --local to review what you have checked out" >&2
229
+ fi
230
+ fi
231
+
232
+ # Resolve the lower bound of the review range (exclusive).
233
+ if [ "$delta" -eq 1 ]; then
234
+ [ -n "$prev" ] || die "no previous review of $src recorded; run a full review first"
235
+ [ "$prev" != "$tip" ] || die "no new commits since your last review of $src"
236
+ git merge-base --is-ancestor "$prev" "$tip" ||
237
+ die "$src was force-pushed since your last review; run a full review instead"
238
+ start="$prev"
239
+ range="since last review ($(git rev-parse --short "$prev"))"
240
+ elif [ -n "$from" ]; then
241
+ start="$(git rev-parse --verify --quiet "$from^{commit}")" || die "unknown commit: $from"
242
+ git merge-base --is-ancestor "$start" "$tip" ||
243
+ die "$from is not an ancestor of $srclabel"
244
+ [ "$start" != "$tip" ] || die "no commits to review after $from"
245
+ range="since $(git rev-parse --short "$start")"
246
+ else
247
+ [ -n "$base" ] ||
248
+ die "no base branch set; pass one as an argument or with --base, or run: git config reviewworkflow.base <branch>"
249
+ [ -n "$baseref" ] ||
250
+ die "$baselabel not found"
251
+ start="$(git merge-base "$baseref" "$srcref")" ||
252
+ die "could not compute merge-base with $baselabel"
253
+ # Nothing to review when the source is already at the base (e.g. running on the
254
+ # base branch itself, or a branch fully merged into it). Mirror the explicit
255
+ # guards on --delta/--from instead of creating an empty review/ branch.
256
+ [ "$start" != "$tip" ] ||
257
+ die "no commits to review; $srclabel is already at $baselabel"
258
+ range="vs $baselabel"
259
+ if [ -n "$prev" ] && [ "$prev" != "$tip" ] && git merge-base --is-ancestor "$prev" "$tip"; then
260
+ n="$(git rev-list --count "$prev..$tip")"
261
+ echo "note: previously reviewed at $(git rev-parse --short "$prev"); $n new commit(s) since (use --delta to review only those)" >&2
262
+ fi
263
+ fi
264
+
265
+ if [ "$step" -eq 1 ]; then
266
+ # --first-parent --no-merges: walk only the branch's own commits, skipping
267
+ # merge commits (e.g. merges of the base branch) so changes that came from
268
+ # the base are never shown as a step.
269
+ commits="$(git rev-list --reverse --first-parent --no-merges "$start..$tip")"
270
+ [ -n "$commits" ] || die "no commits to review in range"
271
+ count="$(printf '%s\n' "$commits" | wc -l | tr -d ' ')"
272
+ first="$(printf '%s\n' "$commits" | sed -n '1p')"
273
+
274
+ git switch -q -c "$rb" "$first" || die "could not create $rb"
275
+ git reset --soft HEAD^
276
+ git config "branch.$rb.reviewsource" "$src"
277
+ git config "branch.$rb.reviewbase" "$base"
278
+ git config "branch.$rb.reviewtip" "$tip"
279
+ git config "branch.$rb.reviewstart" "$start"
280
+ git config "branch.$rb.reviewmode" "step"
281
+ git config "branch.$rb.reviewcount" "$count"
282
+ git config "branch.$rb.reviewstep" "1"
283
+ if [ -n "$prev" ]; then
284
+ git config "branch.$rb.reviewprevreviewed" "$prev"
285
+ fi
286
+ if [ "$local_mode" -eq 1 ]; then
287
+ git config "branch.$rb.reviewlocal" "1"
288
+ fi
289
+ git config "$markerkey" "$tip"
290
+ if [ -n "$returnbranch" ]; then
291
+ git config "branch.$rb.reviewreturn" "$returnbranch"
292
+ fi
293
+
294
+ echo "$rb ready ($range) — walking $count commit(s) one at a time"
295
+ show_commit "$first" 1 "$count"
296
+ exit 0
297
+ fi
298
+
299
+ # Lower bound for the staged diff. Normally it is the range start, but if the
300
+ # base branch has been merged into the PR, fold that already-merged base content
301
+ # into the lower bound so changes that came from the base do not appear as part
302
+ # of the review. This is a no-op when the base was not merged in (it never
303
+ # invents deletions), and degrades gracefully on git without merge-tree.
304
+ lower="$start"
305
+ if [ -n "$baseref" ]; then
306
+ mb="$(git merge-base "$baseref" "$tip" 2>/dev/null || true)"
307
+ if [ -n "$mb" ] && ! git merge-base --is-ancestor "$mb" "$start"; then
308
+ if ltree="$(git merge-tree --write-tree "$start" "$mb" 2>/dev/null)"; then
309
+ lower="$(git commit-tree "$ltree" -p "$start" -m 'review lower bound')"
310
+ else
311
+ echo "note: could not exclude merged base content from the review diff" >&2
312
+ fi
313
+ fi
314
+ fi
315
+
316
+ git switch -q -c "$rb" "$tip" || die "could not create $rb"
317
+ git reset -q --soft "$lower"
318
+
319
+ git config "branch.$rb.reviewsource" "$src"
320
+ git config "branch.$rb.reviewbase" "$base"
321
+ git config "branch.$rb.reviewtip" "$tip"
322
+ if [ -n "$prev" ]; then
323
+ git config "branch.$rb.reviewprevreviewed" "$prev"
324
+ fi
325
+ if [ "$local_mode" -eq 1 ]; then
326
+ git config "branch.$rb.reviewlocal" "1"
327
+ fi
328
+ git config "$markerkey" "$tip"
329
+ if [ -n "$returnbranch" ]; then
330
+ git config "branch.$rb.reviewreturn" "$returnbranch"
331
+ fi
332
+
333
+ echo "$rb ready ($range) — the staged diff is the PR; edit, then run git review finish"
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # git review status — show the state of the review on the current branch.
4
+ #
5
+ set -eu
6
+
7
+ prog="git review status"
8
+
9
+ usage() {
10
+ cat <<EOF
11
+ usage: $prog
12
+
13
+ Show the state of the review on the current review/* branch: source PR, mode,
14
+ and (in commit-by-commit mode) which commit you are on and which steps have
15
+ banked edits.
16
+ EOF
17
+ }
18
+
19
+ case "${1:-}" in
20
+ -h | --h)
21
+ usage
22
+ exit 0
23
+ ;;
24
+ "") ;;
25
+ *)
26
+ echo "error: unexpected argument $1" >&2
27
+ usage >&2
28
+ exit 1
29
+ ;;
30
+ esac
31
+
32
+ cur="$(git symbolic-ref --quiet --short HEAD || true)"
33
+ case "$cur" in
34
+ review/*) ;;
35
+ *)
36
+ echo "not on a review/* branch (HEAD is ${cur:-detached})" >&2
37
+ exit 1
38
+ ;;
39
+ esac
40
+
41
+ src="$(git config "branch.$cur.reviewsource" || true)"
42
+ tip="$(git config "branch.$cur.reviewtip" || true)"
43
+ mode="$(git config "branch.$cur.reviewmode" || echo whole)"
44
+ if [ -z "$src" ] || [ -z "$tip" ]; then
45
+ echo "error: missing review metadata; was $cur created with git review start?" >&2
46
+ exit 1
47
+ fi
48
+
49
+ printf 'review of %s (tip %s)\n' "$src" "$(git rev-parse --short "$tip")"
50
+ printf ' branch %s\n' "$cur"
51
+
52
+ if [ "$mode" = "step" ]; then
53
+ count="$(git config "branch.$cur.reviewcount" || true)"
54
+ step="$(git config "branch.$cur.reviewstep" || true)"
55
+ start="$(git config "branch.$cur.reviewstart" || true)"
56
+ # A key deleted by a hand-edit (while reviewmode stays "step") would otherwise
57
+ # let set -e kill the bare read silently mid-script; read with || true and report
58
+ # it. reviewstep falls through to the numeric guard below for a precise message.
59
+ if [ -z "$count" ] || [ -z "$start" ]; then
60
+ echo "error: missing review metadata; was $cur created with git review start?" >&2
61
+ exit 1
62
+ fi
63
+ commits="$(git rev-list --reverse --first-parent --no-merges "$start..$tip")"
64
+ # Guard against a step that maps to no commit (corrupt config, hand-edited
65
+ # metadata): otherwise cnow ends up empty and git rev-parse '' crashes while
66
+ # the printf around it still "succeeds", printing a corrupt status line.
67
+ total="$(printf '%s\n' "$commits" | grep -c .)"
68
+ case "$step" in
69
+ '' | *[!0-9]*)
70
+ echo "error: corrupt review metadata: reviewstep is '$step', not a positive integer" >&2
71
+ exit 1
72
+ ;;
73
+ esac
74
+ if [ "$step" -lt 1 ] || [ "$step" -gt "$total" ]; then
75
+ echo "error: review step $step out of range (1..$total) — corrupt metadata?" >&2
76
+ exit 1
77
+ fi
78
+ cnow="$(printf '%s\n' "$commits" | sed -n "${step}p")"
79
+ printf ' mode step [%s/%s] on %s %s\n' "$step" "$count" \
80
+ "$(git rev-parse --short "$cnow")" "$(git log -1 --format='%s' "$cnow")"
81
+
82
+ banked=""
83
+ i=1
84
+ while [ "$i" -le "$count" ]; do
85
+ if git rev-parse --verify --quiet "refs/review-edits/$src/$i" >/dev/null; then
86
+ banked="$banked $i"
87
+ fi
88
+ i=$((i + 1))
89
+ done
90
+ printf ' banked %s\n' "${banked:- none}"
91
+
92
+ hint="git review next"
93
+ [ "$step" -gt 1 ] && hint="$hint / git review prev"
94
+ printf ' next edit, then %s (git review finish when done)\n' "$hint"
95
+ else
96
+ base="$(git config "branch.$cur.reviewbase" || true)"
97
+ printf ' mode whole%s\n' "${base:+ (base $base)}"
98
+ printf ' next edit, then git review finish\n'
99
+ fi