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,289 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # git review forget — discard the persistent state a review leaves behind:
4
+ # the --delta markers (--delta) or a review paused with git review save (--saved).
5
+ # Exactly one mode is required.
6
+ #
7
+ # Both kinds of state outlive the review/<branch> and review-fixes/<branch>
8
+ # branches on purpose — a later --delta keeps working, a paused review waits to
9
+ # be resumed — so git review clean deliberately leaves them alone. This command
10
+ # is how you throw them away. To delete the review branches themselves, use
11
+ # git review clean.
12
+ #
13
+ set -eu
14
+
15
+ prog="git review forget"
16
+
17
+ usage() {
18
+ cat <<EOF
19
+ usage: $prog --delta (<branch> | --all | --stale [--dry-run])
20
+ $prog --saved (<branch> | --all) [--dry-run]
21
+
22
+ Discard the persistent state a review leaves behind. Choose one mode:
23
+
24
+ --delta forget the recorded last-reviewed tip used by git review start --delta
25
+ --saved discard a review paused with git review save: delete
26
+ review-saved/<branch>, its banked edits and metadata, and roll back
27
+ the --delta marker it left
28
+
29
+ --delta targets:
30
+ <branch> forget the marker(s) for one source branch (remote and --local)
31
+ --all forget every recorded marker
32
+ --stale forget only markers whose branch no longer exists: remote markers
33
+ when <remote>/<branch> is gone (fetches and prunes first), local
34
+ markers when the local <branch> is gone
35
+ --dry-run with --stale, list what would be forgotten without forgetting it
36
+
37
+ --saved targets:
38
+ <branch> discard the saved review for one source branch
39
+ --all discard every saved review
40
+ --dry-run list what would be discarded without discarding it
41
+ EOF
42
+ }
43
+
44
+ die() {
45
+ echo "error: $1" >&2
46
+ exit 1
47
+ }
48
+
49
+ mode=""
50
+ arg=""
51
+ all=0
52
+ stale=0
53
+ dryrun=0
54
+ while [ $# -gt 0 ]; do
55
+ case "$1" in
56
+ -h | --h)
57
+ usage
58
+ exit 0
59
+ ;;
60
+ --delta)
61
+ [ -z "$mode" ] || [ "$mode" = delta ] || die "use only one of --delta and --saved"
62
+ mode="delta"
63
+ ;;
64
+ --saved)
65
+ [ -z "$mode" ] || [ "$mode" = saved ] || die "use only one of --delta and --saved"
66
+ mode="saved"
67
+ ;;
68
+ --all) all=1 ;;
69
+ --stale) stale=1 ;;
70
+ --dry-run) dryrun=1 ;;
71
+ -*)
72
+ echo "error: unknown option $1" >&2
73
+ usage >&2
74
+ exit 1
75
+ ;;
76
+ *)
77
+ if [ -z "$arg" ]; then
78
+ arg="$1"
79
+ else
80
+ echo "error: unexpected argument $1" >&2
81
+ usage >&2
82
+ exit 1
83
+ fi
84
+ ;;
85
+ esac
86
+ shift
87
+ done
88
+
89
+ # The mode selects which kind of state to discard; without it the request is
90
+ # ambiguous (markers? a saved review?), so there is no safe default.
91
+ if [ -z "$mode" ]; then
92
+ usage >&2
93
+ exit 1
94
+ fi
95
+
96
+ # ── --saved ─────────────────────────────────────────────────────────────────────
97
+ if [ "$mode" = saved ]; then
98
+ [ "$stale" -eq 0 ] || die "--stale only applies to --delta"
99
+
100
+ if [ -n "$arg" ] && [ "$all" -eq 1 ]; then
101
+ die "use either <branch> or --all, not both"
102
+ fi
103
+ if [ -z "$arg" ] && [ "$all" -eq 0 ]; then
104
+ usage >&2
105
+ exit 1
106
+ fi
107
+
108
+ git rev-parse --git-dir >/dev/null 2>&1 || die "not a git repository"
109
+
110
+ cur="$(git symbolic-ref --quiet --short HEAD || true)"
111
+
112
+ # forget_saved_one <src>: discard the saved review for one source branch.
113
+ forget_saved_one() {
114
+ src="$1"
115
+ saved="review-saved/$src"
116
+ if ! git rev-parse --verify --quiet "refs/heads/$saved" >/dev/null; then
117
+ echo "no saved review for $src" >&2
118
+ return
119
+ fi
120
+ if [ "$dryrun" -eq 1 ]; then
121
+ echo "would discard saved review of $src"
122
+ return
123
+ fi
124
+ if [ "$saved" = "$cur" ]; then
125
+ echo "skipping $src (its branch is currently checked out)" >&2
126
+ return
127
+ fi
128
+
129
+ # Roll the --delta marker back to the last actual review before the metadata
130
+ # disappears with the branch. Local reviews track their marker in a separate
131
+ # config section, matching git review start / git review abort.
132
+ if [ "$(git config "branch.$saved.reviewlocal" || true)" = "1" ]; then
133
+ markerkey="reviewworkflowlocal.$src.reviewed"
134
+ else
135
+ markerkey="reviewworkflow.$src.reviewed"
136
+ fi
137
+ prevreviewed="$(git config "branch.$saved.reviewprevreviewed" || true)"
138
+ if [ -n "$prevreviewed" ]; then
139
+ git config "$markerkey" "$prevreviewed"
140
+ else
141
+ git config --unset "$markerkey" 2>/dev/null || true
142
+ fi
143
+
144
+ git for-each-ref --format='%(refname)' "refs/review-saved-edits/$src/" | while read -r ref; do
145
+ git update-ref -d "$ref"
146
+ done
147
+ git branch -D "$saved" >/dev/null
148
+
149
+ echo "discarded saved review of $src"
150
+ }
151
+
152
+ if [ "$all" -eq 1 ]; then
153
+ branches="$(git for-each-ref --format='%(refname:short)' refs/heads/review-saved/)"
154
+ [ -n "$branches" ] || {
155
+ echo "no saved reviews"
156
+ exit 0
157
+ }
158
+ printf '%s\n' "$branches" | while read -r b; do
159
+ forget_saved_one "${b#review-saved/}"
160
+ done
161
+ else
162
+ forget_saved_one "$arg"
163
+ fi
164
+ exit 0
165
+ fi
166
+
167
+ # ── --delta ─────────────────────────────────────────────────────────────────────
168
+ # parse_marker <key>: split a marker key into the source branch ($msrc), a human
169
+ # label ($mlabel) and whether it is a local marker ($mislocal). Local markers (git
170
+ # review start --local) live in their own config section, reviewworkflowlocal.<src>
171
+ # .reviewed, while remote ones are reviewworkflow.<src>.reviewed. Disjoint sections
172
+ # (rather than a .local segment inside one section) keep the two apart even for a
173
+ # branch literally named "<src>.local".
174
+ parse_marker() {
175
+ case "$1" in
176
+ reviewworkflowlocal.*)
177
+ msrc="${1#reviewworkflowlocal.}"
178
+ msrc="${msrc%.reviewed}"
179
+ mlabel=" (local)"
180
+ mislocal=1
181
+ ;;
182
+ *)
183
+ msrc="${1#reviewworkflow.}"
184
+ msrc="${msrc%.reviewed}"
185
+ mlabel=""
186
+ mislocal=0
187
+ ;;
188
+ esac
189
+ }
190
+
191
+ # forget_delta <key> <src> <verb>: unset a marker (or, in dry-run, say it would be).
192
+ forget_delta() {
193
+ if [ "$dryrun" -eq 1 ]; then
194
+ echo "would forget delta marker for $2$3"
195
+ else
196
+ git config --unset "$1" || true
197
+ echo "forgot delta marker for $2$3"
198
+ fi
199
+ }
200
+
201
+ # Exactly one target selector: a branch, --all or --stale.
202
+ selectors=0
203
+ [ -n "$arg" ] && selectors=$((selectors + 1))
204
+ [ "$all" -eq 1 ] && selectors=$((selectors + 1))
205
+ [ "$stale" -eq 1 ] && selectors=$((selectors + 1))
206
+ if [ "$selectors" -eq 0 ]; then
207
+ usage >&2
208
+ exit 1
209
+ fi
210
+ [ "$selectors" -eq 1 ] || die "use only one of <branch>, --all and --stale"
211
+
212
+ # --dry-run is a preview of the inferred --stale set; it means nothing for the
213
+ # explicit targets, where you already named exactly what to forget.
214
+ if [ "$dryrun" -eq 1 ] && [ "$stale" -ne 1 ]; then
215
+ die "--dry-run only applies to --stale"
216
+ fi
217
+
218
+ git rev-parse --git-dir >/dev/null 2>&1 || die "not a git repository"
219
+
220
+ # Remote whose branches back the markers; defaults to origin, override with
221
+ # reviewworkflow.remote.
222
+ remote="$(git config reviewworkflow.remote || echo origin)"
223
+
224
+ # ── one branch ────────────────────────────────────────────────────────────────
225
+ if [ -n "$arg" ]; then
226
+ any=0
227
+ if [ -n "$(git config "reviewworkflow.$arg.reviewed" || true)" ]; then
228
+ forget_delta "reviewworkflow.$arg.reviewed" "$arg" ""
229
+ any=1
230
+ fi
231
+ if [ -n "$(git config "reviewworkflowlocal.$arg.reviewed" || true)" ]; then
232
+ forget_delta "reviewworkflowlocal.$arg.reviewed" "$arg" " (local)"
233
+ any=1
234
+ fi
235
+ [ "$any" -eq 1 ] || echo "no delta marker recorded for $arg"
236
+ exit 0
237
+ fi
238
+
239
+ # Collect every marker into a temp file so the loop runs in the current shell
240
+ # (a pipe into `while` would forget unsets in a subshell). Both sections are
241
+ # gathered: remote markers (reviewworkflow.*) and local ones (reviewworkflowlocal.*).
242
+ tmp="$(mktemp)"
243
+ trap 'rm -f "$tmp"' EXIT
244
+ {
245
+ git config --get-regexp '^reviewworkflow\..*\.reviewed$' 2>/dev/null || true
246
+ git config --get-regexp '^reviewworkflowlocal\..*\.reviewed$' 2>/dev/null || true
247
+ } >"$tmp"
248
+
249
+ if [ ! -s "$tmp" ]; then
250
+ echo "no delta markers recorded"
251
+ exit 0
252
+ fi
253
+
254
+ # ── --stale ───────────────────────────────────────────────────────────────────
255
+ if [ "$stale" -eq 1 ]; then
256
+ # A local marker's staleness is decided entirely by whether its local branch
257
+ # still exists — no remote state involved. Only fetch (to prune deleted
258
+ # remote-tracking refs) when at least one remote marker is present; a set of
259
+ # purely --local markers needs no network at all. The anchored pattern matches
260
+ # reviewworkflow.<src>.reviewed but not reviewworkflowlocal.<src>.reviewed.
261
+ if grep -q '^reviewworkflow\.' "$tmp"; then
262
+ git fetch --prune --quiet "$remote" || die "could not fetch from $remote"
263
+ fi
264
+
265
+ any=0
266
+ while read -r key _; do
267
+ [ -n "$key" ] || continue
268
+ parse_marker "$key"
269
+ if [ "$mislocal" -eq 1 ]; then
270
+ # A local marker is stale when its local branch is gone.
271
+ git rev-parse --verify --quiet "refs/heads/$msrc" >/dev/null && continue
272
+ forget_delta "$key" "$msrc" " (local; $msrc no longer exists)"
273
+ else
274
+ git rev-parse --verify --quiet "refs/remotes/$remote/$msrc" >/dev/null && continue
275
+ forget_delta "$key" "$msrc" " ($remote/$msrc no longer exists)"
276
+ fi
277
+ any=1
278
+ done <"$tmp"
279
+
280
+ [ "$any" -eq 1 ] || echo "no stale delta markers (every branch still exists)"
281
+ exit 0
282
+ fi
283
+
284
+ # ── --all ─────────────────────────────────────────────────────────────────────
285
+ while read -r key _; do
286
+ [ -n "$key" ] || continue
287
+ parse_marker "$key"
288
+ forget_delta "$key" "$msrc" "$mlabel"
289
+ done <"$tmp"
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # git review list — list every review/* branch in progress.
4
+ #
5
+ # git review status reports only the review on the current branch; this shows all
6
+ # of them at once, so you can see what you have open across branches. The current
7
+ # branch is marked with a "*".
8
+ #
9
+ set -eu
10
+
11
+ prog="git review list"
12
+
13
+ usage() {
14
+ cat <<EOF
15
+ usage: $prog
16
+
17
+ List every review/* branch in progress, with its source PR, mode and — in
18
+ commit-by-commit mode — which commit you are on ([k/N]). The branch you are on
19
+ is marked with a "*". Reviews paused with git review save are listed too, under
20
+ "saved".
21
+ EOF
22
+ }
23
+
24
+ case "${1:-}" in
25
+ -h | --h)
26
+ usage
27
+ exit 0
28
+ ;;
29
+ "") ;;
30
+ *)
31
+ echo "error: unexpected argument $1" >&2
32
+ usage >&2
33
+ exit 1
34
+ ;;
35
+ esac
36
+
37
+ git rev-parse --git-dir >/dev/null 2>&1 || {
38
+ echo "error: not a git repository" >&2
39
+ exit 1
40
+ }
41
+
42
+ cur="$(git symbolic-ref --quiet --short HEAD || true)"
43
+ branches="$(git for-each-ref --format='%(refname:short)' refs/heads/review/)"
44
+ saved="$(git for-each-ref --format='%(refname:short)' refs/heads/review-saved/)"
45
+
46
+ if [ -z "$branches" ] && [ -z "$saved" ]; then
47
+ echo "no reviews in progress"
48
+ exit 0
49
+ fi
50
+
51
+ # describe <branch> <marker>: print one line for a review branch, reading its
52
+ # mode and position from the branch config. Used for both active and saved reviews.
53
+ describe() {
54
+ b="$1"
55
+ label="$2"
56
+ src="$(git config "branch.$b.reviewsource" || true)"
57
+ # A branch with no reviewsource is an orphan: either hand-made, or left by a
58
+ # git review save / git review start that died after creating the branch but before
59
+ # writing its metadata. Surface it instead of skipping silently — otherwise an
60
+ # orphan-only state prints nothing at all — so it can be cleaned up (e.g. with
61
+ # git review forget --saved).
62
+ if [ -z "$src" ]; then
63
+ printf '%s %s %s(no metadata)\n' "$label" "$b" "$prefix"
64
+ return
65
+ fi
66
+ tip="$(git config "branch.$b.reviewtip" || true)"
67
+ mode="$(git config "branch.$b.reviewmode" || echo whole)"
68
+
69
+ shorttip="$tip"
70
+ [ -n "$tip" ] && shorttip="$(git rev-parse --short "$tip" 2>/dev/null || echo "$tip")"
71
+
72
+ if [ "$mode" = "step" ]; then
73
+ count="$(git config "branch.$b.reviewcount" || echo '?')"
74
+ step="$(git config "branch.$b.reviewstep" || echo '?')"
75
+ printf '%s %s %sstep [%s/%s] tip %s\n' "$label" "$b" "$prefix" "$step" "$count" "$shorttip"
76
+ else
77
+ printf '%s %s %swhole tip %s\n' "$label" "$b" "$prefix" "$shorttip"
78
+ fi
79
+ }
80
+
81
+ prefix=""
82
+ printf '%s\n' "$branches" | while read -r b; do
83
+ [ -n "$b" ] || continue
84
+ marker=" "
85
+ [ "$b" = "$cur" ] && marker="*"
86
+ describe "$b" "$marker"
87
+ done
88
+
89
+ # Reviews put aside with git review save, tagged so they are not mistaken for
90
+ # active ones. The branch you are on is still marked with "*".
91
+ if [ -n "$saved" ]; then
92
+ prefix="saved "
93
+ printf '%s\n' "$saved" | while read -r b; do
94
+ [ -n "$b" ] || continue
95
+ marker=" "
96
+ [ "$b" = "$cur" ] && marker="*"
97
+ describe "$b" "$marker"
98
+ done
99
+ fi
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # git review next — advance a commit-by-commit review (started with review start --step).
4
+ #
5
+ # Moving banks the edits you made on the current commit into a ref of its own and
6
+ # restores any edits you had previously banked on the commit you move to, so you
7
+ # can walk back and forth without losing work. git review finish replays every
8
+ # banked edit onto the PR tip.
9
+ #
10
+ set -eu
11
+
12
+ prog="git review next"
13
+
14
+ usage() {
15
+ cat <<EOF
16
+ usage: $prog
17
+
18
+ Advance a commit-by-commit review to the next commit. Your edits are remembered;
19
+ run git review prev to go back, or git review finish when you are done.
20
+ EOF
21
+ }
22
+
23
+ case "${1:-}" in
24
+ -h | --h)
25
+ usage
26
+ exit 0
27
+ ;;
28
+ "") ;;
29
+ *)
30
+ echo "error: unexpected argument $1" >&2
31
+ usage >&2
32
+ exit 1
33
+ ;;
34
+ esac
35
+
36
+ # Shared helpers, sourced from the libexec directory the dispatcher points us at.
37
+ # shellcheck source=../git-review-lib.sh
38
+ . "${GIT_REVIEW_LIBEXEC:?}/git-review-lib.sh"
39
+
40
+ load_step_review_meta
41
+
42
+ if [ "$step" -lt "$count" ]; then
43
+ goto_step "$((step + 1))"
44
+ else
45
+ echo "no more commits — run git review finish"
46
+ fi
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # git review prev — step a commit-by-commit review back to the previous commit.
4
+ #
5
+ # Like git review next but backwards: it banks the current commit's edits and
6
+ # restores any edits you had banked on the previous commit, so you can revisit
7
+ # and keep editing them.
8
+ #
9
+ set -eu
10
+
11
+ prog="git review prev"
12
+
13
+ usage() {
14
+ cat <<EOF
15
+ usage: $prog
16
+
17
+ Go back to the previous commit of a commit-by-commit review. Your edits are
18
+ remembered in both directions.
19
+ EOF
20
+ }
21
+
22
+ case "${1:-}" in
23
+ -h | --h)
24
+ usage
25
+ exit 0
26
+ ;;
27
+ "") ;;
28
+ *)
29
+ echo "error: unexpected argument $1" >&2
30
+ usage >&2
31
+ exit 1
32
+ ;;
33
+ esac
34
+
35
+ # Shared helpers, sourced from the libexec directory the dispatcher points us at.
36
+ # shellcheck source=../git-review-lib.sh
37
+ . "${GIT_REVIEW_LIBEXEC:?}/git-review-lib.sh"
38
+
39
+ load_step_review_meta
40
+
41
+ if [ "$step" -gt 1 ]; then
42
+ goto_step "$((step - 1))"
43
+ else
44
+ echo "already at the first commit"
45
+ fi
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env sh
2
+ #
3
+ # git review preview — show the edits you have made so far, without disturbing them.
4
+ #
5
+ # It prints the same diff that git review finish would extract — your review edits
6
+ # on top of the PR tip — but it never commits, never switches branch and never
7
+ # touches your working tree or index, so you go straight back to editing where you
8
+ # left off. Think of it as "what would git review finish give me right now?".
9
+ #
10
+ # Everything is computed in a throwaway index (GIT_INDEX_FILE), the same plumbing
11
+ # style git review save/git review finish use, so running it is a no-op on your state:
12
+ # whole mode capture the working tree (edits + any new files) as a tree and diff
13
+ # it against the tip.
14
+ # step mode reproduce git review finish's replay in the throwaway index — the
15
+ # current step's edits plus every banked edit, applied onto the tip
16
+ # with the same --3way merge — and diff the result against the tip.
17
+ # An edit that genuinely conflicts with the tip is the one case that
18
+ # differs from git review finish: git review finish leaves you conflict
19
+ # markers to resolve, whereas a read-only preview cannot, so it omits
20
+ # that edit and prints a note pointing you at git review finish.
21
+ #
22
+ set -eu
23
+
24
+ prog="git review preview"
25
+
26
+ usage() {
27
+ cat <<EOF
28
+ usage: $prog [--stat]
29
+
30
+ Show the review edits you have made so far on the current review/* branch — the
31
+ same diff git review finish would extract — without committing, switching branch
32
+ or changing your working tree, so you can keep editing where you left off.
33
+
34
+ --stat show a diffstat summary instead of the full diff
35
+ EOF
36
+ }
37
+
38
+ die() {
39
+ echo "error: $1" >&2
40
+ exit 1
41
+ }
42
+
43
+ stat=0
44
+ while [ $# -gt 0 ]; do
45
+ case "$1" in
46
+ -h | --h)
47
+ usage
48
+ exit 0
49
+ ;;
50
+ --stat) stat=1 ;;
51
+ *)
52
+ echo "error: unexpected argument $1" >&2
53
+ usage >&2
54
+ exit 1
55
+ ;;
56
+ esac
57
+ shift
58
+ done
59
+
60
+ cur="$(git symbolic-ref --quiet --short HEAD || true)"
61
+ case "$cur" in
62
+ review/*) ;;
63
+ *)
64
+ echo "not on a review/* branch (HEAD is ${cur:-detached})" >&2
65
+ exit 1
66
+ ;;
67
+ esac
68
+
69
+ src="$(git config "branch.$cur.reviewsource" || true)"
70
+ tip="$(git config "branch.$cur.reviewtip" || true)"
71
+ mode="$(git config "branch.$cur.reviewmode" || echo whole)"
72
+ if [ -z "$src" ] || [ -z "$tip" ]; then
73
+ die "missing review metadata; was $cur created with git review start?"
74
+ fi
75
+
76
+ # A git review finish left mid-conflict has conflict markers in the working tree; a
77
+ # preview built from it would show the markers as content, not your edits. Refuse
78
+ # until they are resolved, mirroring the state git review finish --resume expects.
79
+ if [ "$(git config "branch.$cur.reviewresume" || true)" = "conflict" ]; then
80
+ die "git review finish is mid-conflict; resolve the markers and run git review finish --resume first"
81
+ fi
82
+ if [ -n "$(git ls-files --unmerged)" ]; then
83
+ die "you have unresolved merge conflicts; resolve them first"
84
+ fi
85
+
86
+ # Throwaway indexes live inside the git dir and are removed on exit, so the real
87
+ # index is never touched and a preview leaves no residue.
88
+ gitdir="$(git rev-parse --git-dir)"
89
+ idx="$gitdir/review-preview-index.$$"
90
+ idx2="$gitdir/review-preview-replay.$$"
91
+ # Patches go through files, never shell variables: command substitution corrupts
92
+ # a binary hunk (dropped NUL bytes and trailing newline). curpatch holds the
93
+ # current step's edits, ppatch each banked edit in turn.
94
+ curpatch="$gitdir/review-preview-curpatch.$$"
95
+ ppatch="$gitdir/review-preview-patch.$$"
96
+ trap 'rm -f "$idx" "$idx2" "$curpatch" "$ppatch"' EXIT
97
+
98
+ # worktree_tree: capture the current working tree — staged, unstaged and any new
99
+ # files — as a tree object, the same content git review finish folds in with git add -A.
100
+ worktree_tree() {
101
+ cp "$(git rev-parse --git-path index)" "$idx"
102
+ GIT_INDEX_FILE="$idx" git add -A
103
+ GIT_INDEX_FILE="$idx" git write-tree
104
+ }
105
+
106
+ if [ "$mode" = "step" ]; then
107
+ start="$(git config "branch.$cur.reviewstart" || true)"
108
+ count="$(git config "branch.$cur.reviewcount" || true)"
109
+ step="$(git config "branch.$cur.reviewstep" || true)"
110
+ # A key deleted by a hand-edit (while reviewmode stays "step") would otherwise
111
+ # let set -e kill the bare read silently mid-script; read with || true and report
112
+ # it. reviewstep falls through to the numeric guard below for a precise message.
113
+ if [ -z "$start" ] || [ -z "$count" ]; then
114
+ die "missing review metadata; was $cur created with git review start?"
115
+ fi
116
+ commits="$(git rev-list --reverse --first-parent --no-merges "$start..$tip")"
117
+ # Guard against a step that maps to no commit (corrupt config, hand-edited
118
+ # metadata): otherwise cstep is empty and git diff '' aborts the preview with
119
+ # an opaque exit 128 instead of a clear message.
120
+ total="$(printf '%s\n' "$commits" | grep -c .)"
121
+ case "$step" in
122
+ '' | *[!0-9]*) die "corrupt review metadata: reviewstep is '$step', not a positive integer" ;;
123
+ esac
124
+ if [ "$step" -lt 1 ] || [ "$step" -gt "$total" ]; then
125
+ die "review step $step out of range (1..$total) — corrupt metadata?"
126
+ fi
127
+ cstep="$(printf '%s\n' "$commits" | sed -n "${step}p")"
128
+
129
+ # The current step's edits are not banked yet (they live in the working tree),
130
+ # so derive them from the working tree against the commit being reviewed.
131
+ git diff --binary "$cstep" "$(worktree_tree)" >"$curpatch"
132
+
133
+ # Replay onto the tip exactly like git review finish: walk the steps in order,
134
+ # applying the current step's edits in its slot and each other step's banked
135
+ # edits, into a fresh index seeded with the tip — with the same --3way merge
136
+ # git review finish uses, so an edit whose context the PR later shifted still lands.
137
+ GIT_INDEX_FILE="$idx2" git read-tree "$tip"
138
+ good="$(GIT_INDEX_FILE="$idx2" git write-tree)"
139
+ conflict=0
140
+ i=1
141
+ while [ "$i" -le "$count" ]; do
142
+ if [ "$i" -eq "$step" ]; then
143
+ pf="$curpatch"
144
+ else
145
+ ref="refs/review-edits/$src/$i"
146
+ if git rev-parse --verify --quiet "$ref" >/dev/null; then
147
+ git diff --binary "${ref}^" "$ref" >"$ppatch"
148
+ pf="$ppatch"
149
+ else
150
+ pf=""
151
+ fi
152
+ fi
153
+ if [ -n "$pf" ] && [ -s "$pf" ]; then
154
+ # A --3way apply that conflicts leaves unmerged entries, which write-tree
155
+ # refuses; so apply, then try to capture the tree. On success keep it as
156
+ # the new baseline; on a genuine conflict roll the index back to the last
157
+ # clean tree and note the edit as omitted (git review finish would leave you
158
+ # conflict markers to resolve — a preview cannot, so it skips it).
159
+ if GIT_INDEX_FILE="$idx2" git apply --cached --3way --binary - <"$pf" 2>/dev/null &&
160
+ t="$(GIT_INDEX_FILE="$idx2" git write-tree 2>/dev/null)"; then
161
+ good="$t"
162
+ else
163
+ conflict=1
164
+ GIT_INDEX_FILE="$idx2" git read-tree "$good"
165
+ fi
166
+ fi
167
+ i=$((i + 1))
168
+ done
169
+ tree="$good"
170
+ else
171
+ tree="$(worktree_tree)"
172
+ conflict=0
173
+ fi
174
+
175
+ # A conflict note is about edits, not the diff, so it must be reported whether or
176
+ # not anything could be applied — otherwise an all-overlapping case would wrongly
177
+ # look like "no edits".
178
+ if [ "$conflict" -eq 1 ]; then
179
+ echo "note: some edits overlap the PR tip; git review finish will need a 3-way merge to apply them (they are omitted below)" >&2
180
+ fi
181
+
182
+ if git diff --quiet "$tip" "$tree"; then
183
+ if [ "$conflict" -eq 1 ]; then
184
+ echo "all your edits overlap the PR tip; nothing previews cleanly — run git review finish to resolve them with a 3-way merge" >&2
185
+ else
186
+ echo "no review changes yet — edit some files, then run $prog again"
187
+ fi
188
+ exit 0
189
+ fi
190
+
191
+ if [ "$stat" -eq 1 ]; then
192
+ git diff --stat "$tip" "$tree"
193
+ else
194
+ git diff "$tip" "$tree"
195
+ fi