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.
- package/LICENSE +21 -0
- package/README.es.md +548 -0
- package/README.md +540 -0
- package/VERSION +1 -0
- package/bin/git-review +104 -0
- package/bin/git-review-lib.sh +121 -0
- package/bin/git-review-verbs/abort +101 -0
- package/bin/git-review-verbs/clean +123 -0
- package/bin/git-review-verbs/compare +159 -0
- package/bin/git-review-verbs/continue +178 -0
- package/bin/git-review-verbs/finish +494 -0
- package/bin/git-review-verbs/forget +289 -0
- package/bin/git-review-verbs/list +99 -0
- package/bin/git-review-verbs/next +46 -0
- package/bin/git-review-verbs/prev +45 -0
- package/bin/git-review-verbs/preview +195 -0
- package/bin/git-review-verbs/save +167 -0
- package/bin/git-review-verbs/start +333 -0
- package/bin/git-review-verbs/status +99 -0
- package/completions/git-review-workflow.bash +143 -0
- package/completions/git-review-workflow.fish +140 -0
- package/completions/git-review-workflow.zsh +153 -0
- package/package.json +39 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
#
|
|
3
|
+
# git-review-lib.sh — helpers shared by the git review step commands.
|
|
4
|
+
#
|
|
5
|
+
# This file is *sourced, never run*. The verbs that need the helpers below
|
|
6
|
+
# (start, next, prev, continue, compare) load it as
|
|
7
|
+
# "${GIT_REVIEW_LIBEXEC:?}/git-review-lib.sh" — GIT_REVIEW_LIBEXEC is exported by
|
|
8
|
+
# the git-review dispatcher before it execs the verb, and points at the real
|
|
9
|
+
# directory where the dispatcher, this lib and the git-review-verbs/ directory
|
|
10
|
+
# live together (installed as libexec, not on PATH). It only defines functions,
|
|
11
|
+
# so sourcing it has no side effects.
|
|
12
|
+
|
|
13
|
+
# show_commit <commit> <n> <total>
|
|
14
|
+
# Print a commit's diffstat first and its identifying header last, so the header
|
|
15
|
+
# stays next to the prompt instead of scrolling off the top when the diffstat is
|
|
16
|
+
# long for a commit that touches many files.
|
|
17
|
+
show_commit() {
|
|
18
|
+
git --no-pager show --stat --format='' "$1"
|
|
19
|
+
printf -- '----\n[%s/%s] %s\n%s\n\n%s\n----\nreview this commit, edit files, then run git review next\n' \
|
|
20
|
+
"$2" "$3" "$(git rev-parse --short "$1")" \
|
|
21
|
+
"$(git show -s --format='%an <%ae>' "$1")" \
|
|
22
|
+
"$(git show -s --format='%s%n%n%b' "$1")"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# load_step_review_meta
|
|
26
|
+
# Confirm HEAD is on a review/* branch started with --step and load its metadata
|
|
27
|
+
# into the globals the caller and goto_step rely on: cur, src, tip, start, count,
|
|
28
|
+
# step, commits and total. Exits with a diagnostic on any inconsistency (wrong
|
|
29
|
+
# branch, wrong mode, or missing/corrupt metadata).
|
|
30
|
+
load_step_review_meta() {
|
|
31
|
+
cur="$(git symbolic-ref --quiet --short HEAD || true)"
|
|
32
|
+
[ -n "$cur" ] || {
|
|
33
|
+
echo "error: not on a branch" >&2
|
|
34
|
+
exit 1
|
|
35
|
+
}
|
|
36
|
+
case "$cur" in
|
|
37
|
+
review/*) ;;
|
|
38
|
+
*)
|
|
39
|
+
echo "error: not on a review/* branch (HEAD is $cur)" >&2
|
|
40
|
+
exit 1
|
|
41
|
+
;;
|
|
42
|
+
esac
|
|
43
|
+
|
|
44
|
+
mode="$(git config "branch.$cur.reviewmode" || true)"
|
|
45
|
+
[ "$mode" = "step" ] || {
|
|
46
|
+
echo "error: $cur was not started with git review start --step" >&2
|
|
47
|
+
exit 1
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
src="$(git config "branch.$cur.reviewsource" || true)"
|
|
51
|
+
tip="$(git config "branch.$cur.reviewtip" || true)"
|
|
52
|
+
start="$(git config "branch.$cur.reviewstart" || true)"
|
|
53
|
+
count="$(git config "branch.$cur.reviewcount" || true)"
|
|
54
|
+
step="$(git config "branch.$cur.reviewstep" || true)"
|
|
55
|
+
|
|
56
|
+
# A key deleted by a hand-edit (while reviewmode stays "step") would otherwise
|
|
57
|
+
# let set -e kill us silently mid-script; read with || true and report it.
|
|
58
|
+
if [ -z "$src" ] || [ -z "$tip" ] || [ -z "$start" ] || [ -z "$count" ]; then
|
|
59
|
+
echo "error: missing review metadata; was $cur created with git review start?" >&2
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
commits="$(git rev-list --reverse --first-parent --no-merges "$start..$tip")"
|
|
64
|
+
|
|
65
|
+
# Guard against a step that maps to no commit (corrupt config, hand-edited
|
|
66
|
+
# metadata): otherwise goto_step's sed yields an empty commit and git rev-parse
|
|
67
|
+
# '^{tree}' crashes mid-move.
|
|
68
|
+
total="$(printf '%s\n' "$commits" | grep -c .)"
|
|
69
|
+
case "$count" in
|
|
70
|
+
*[!0-9]*)
|
|
71
|
+
echo "error: corrupt review metadata: reviewcount is '$count', not a positive integer" >&2
|
|
72
|
+
exit 1
|
|
73
|
+
;;
|
|
74
|
+
esac
|
|
75
|
+
[ "$count" -ge 1 ] || {
|
|
76
|
+
echo "error: corrupt review metadata: reviewcount is '$count', not a positive integer" >&2
|
|
77
|
+
exit 1
|
|
78
|
+
}
|
|
79
|
+
case "$step" in
|
|
80
|
+
'' | *[!0-9]*)
|
|
81
|
+
echo "error: corrupt review metadata: reviewstep is '$step', not a positive integer" >&2
|
|
82
|
+
exit 1
|
|
83
|
+
;;
|
|
84
|
+
esac
|
|
85
|
+
if [ "$step" -lt 1 ] || [ "$step" -gt "$total" ]; then
|
|
86
|
+
echo "error: review step $step out of range (1..$total) — corrupt metadata?" >&2
|
|
87
|
+
exit 1
|
|
88
|
+
fi
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# goto_step <target>
|
|
92
|
+
# Move a --step review to step <target>: bank the current commit's edits, reset
|
|
93
|
+
# clean to the target commit, restore the target's previously banked edits (if
|
|
94
|
+
# any), then soft-reset so its diff is staged. Relies on the globals set by
|
|
95
|
+
# load_step_review_meta (cur, src, count, step, commits).
|
|
96
|
+
goto_step() {
|
|
97
|
+
target="$1"
|
|
98
|
+
cstep="$(printf '%s\n' "$commits" | sed -n "${step}p")"
|
|
99
|
+
git add -A
|
|
100
|
+
tree="$(git write-tree)"
|
|
101
|
+
if [ "$tree" != "$(git rev-parse "$cstep^{tree}")" ]; then
|
|
102
|
+
edit="$(git commit-tree "$tree" -p "$cstep" -m "review edits step $step")"
|
|
103
|
+
git update-ref "refs/review-edits/$src/$step" "$edit"
|
|
104
|
+
else
|
|
105
|
+
# Reverting the step back to a clean tree must clear any edits we banked
|
|
106
|
+
# earlier, or they resurrect on the next visit / at git review finish.
|
|
107
|
+
git update-ref -d "refs/review-edits/$src/$step" 2>/dev/null || true
|
|
108
|
+
fi
|
|
109
|
+
ctarget="$(printf '%s\n' "$commits" | sed -n "${target}p")"
|
|
110
|
+
git reset -q --hard "$ctarget"
|
|
111
|
+
ref="refs/review-edits/$src/$target"
|
|
112
|
+
if git rev-parse --verify --quiet "$ref" >/dev/null; then
|
|
113
|
+
git diff --binary "${ref}^" "$ref" | git apply || {
|
|
114
|
+
echo "error: could not restore banked edits for step $target" >&2
|
|
115
|
+
exit 1
|
|
116
|
+
}
|
|
117
|
+
fi
|
|
118
|
+
git reset -q --soft "$ctarget^"
|
|
119
|
+
git config "branch.$cur.reviewstep" "$target"
|
|
120
|
+
show_commit "$ctarget" "$target" "$count"
|
|
121
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
#
|
|
3
|
+
# git review abort — cancel the review on the current branch in one step.
|
|
4
|
+
#
|
|
5
|
+
# Returns you to the branch you started from (or the base), deletes the
|
|
6
|
+
# review/<branch> branch and its banked edits. Because the review was cancelled,
|
|
7
|
+
# it also rolls the --delta marker back to your last actual review.
|
|
8
|
+
#
|
|
9
|
+
set -eu
|
|
10
|
+
|
|
11
|
+
prog="git review abort"
|
|
12
|
+
|
|
13
|
+
usage() {
|
|
14
|
+
cat <<EOF
|
|
15
|
+
usage: $prog
|
|
16
|
+
|
|
17
|
+
Cancel the current review: return to the starting branch, then delete the
|
|
18
|
+
review/<branch> branch and any banked commit-by-commit edits.
|
|
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
|
+
cur="$(git symbolic-ref --quiet --short HEAD || true)"
|
|
36
|
+
case "$cur" in
|
|
37
|
+
review/*) ;;
|
|
38
|
+
*)
|
|
39
|
+
echo "error: not on a review/* branch (HEAD is ${cur:-detached})" >&2
|
|
40
|
+
exit 1
|
|
41
|
+
;;
|
|
42
|
+
esac
|
|
43
|
+
|
|
44
|
+
src="$(git config "branch.$cur.reviewsource" || true)"
|
|
45
|
+
[ -n "$src" ] || {
|
|
46
|
+
echo "error: missing review metadata; was $cur created with git review start?" >&2
|
|
47
|
+
exit 1
|
|
48
|
+
}
|
|
49
|
+
base="$(git config "branch.$cur.reviewbase" || true)"
|
|
50
|
+
ret="$(git config "branch.$cur.reviewreturn" || true)"
|
|
51
|
+
prevreviewed="$(git config "branch.$cur.reviewprevreviewed" || true)"
|
|
52
|
+
|
|
53
|
+
# A local review tracks its delta progress under a separate config section
|
|
54
|
+
# (reviewworkflowlocal.*); roll back the matching one so a cancelled review never
|
|
55
|
+
# corrupts either side's progress.
|
|
56
|
+
if [ "$(git config "branch.$cur.reviewlocal" || true)" = "1" ]; then
|
|
57
|
+
markerkey="reviewworkflowlocal.$src.reviewed"
|
|
58
|
+
else
|
|
59
|
+
markerkey="reviewworkflow.$src.reviewed"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
target=""
|
|
63
|
+
if [ -n "$ret" ] && git rev-parse --verify --quiet "refs/heads/$ret" >/dev/null; then
|
|
64
|
+
target="$ret"
|
|
65
|
+
elif [ -n "$base" ] && git rev-parse --verify --quiet "refs/heads/$base" >/dev/null; then
|
|
66
|
+
target="$base"
|
|
67
|
+
fi
|
|
68
|
+
[ -n "$target" ] || {
|
|
69
|
+
echo "error: could not determine a branch to return to; switch away manually, then run git review clean $src" >&2
|
|
70
|
+
exit 1
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
git switch -q --discard-changes "$target"
|
|
74
|
+
git branch -D "$cur" >/dev/null
|
|
75
|
+
|
|
76
|
+
# This review was cancelled, not completed: undo the reviewed marker it set,
|
|
77
|
+
# restoring the tip of your last actual review (or clearing it if there was none)
|
|
78
|
+
# so a later --delta does not skip commits you never reviewed.
|
|
79
|
+
if [ -n "$prevreviewed" ]; then
|
|
80
|
+
git config "$markerkey" "$prevreviewed"
|
|
81
|
+
else
|
|
82
|
+
git config --unset "$markerkey" 2>/dev/null || true
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
git for-each-ref --format='%(refname)' "refs/review-edits/$src/" | while read -r ref; do
|
|
86
|
+
git update-ref -d "$ref"
|
|
87
|
+
done
|
|
88
|
+
|
|
89
|
+
# Cancelling the review tears down everything it spawned. If you had already run
|
|
90
|
+
# git review finish, that left a review-fixes/<branch> and a finish undo point
|
|
91
|
+
# (refs/review-undo/<branch>/*) behind; drop them too so an abort leaves nothing
|
|
92
|
+
# dangling. The undo *config* rode on branch.review/<branch> and was already
|
|
93
|
+
# removed when the branch was deleted above.
|
|
94
|
+
if git rev-parse --verify --quiet "refs/heads/review-fixes/$src" >/dev/null; then
|
|
95
|
+
git branch -D "review-fixes/$src" >/dev/null
|
|
96
|
+
fi
|
|
97
|
+
git for-each-ref --format='%(refname)' "refs/review-undo/$src/" | while read -r ref; do
|
|
98
|
+
git update-ref -d "$ref"
|
|
99
|
+
done
|
|
100
|
+
|
|
101
|
+
echo "aborted review of $src; returned to $target"
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
#
|
|
3
|
+
# git review clean — delete the review/<branch> and review-fixes/<branch> branches.
|
|
4
|
+
#
|
|
5
|
+
# With no <branch> it deletes all of them. It never deletes the branch you are
|
|
6
|
+
# currently on. The recorded last-reviewed tip (used by --delta) is left alone;
|
|
7
|
+
# discard it with git review forget --delta.
|
|
8
|
+
#
|
|
9
|
+
set -eu
|
|
10
|
+
|
|
11
|
+
prog="git review clean"
|
|
12
|
+
|
|
13
|
+
usage() {
|
|
14
|
+
cat <<EOF
|
|
15
|
+
usage: $prog [branch]
|
|
16
|
+
|
|
17
|
+
Delete the review/<branch> and review-fixes/<branch> branches.
|
|
18
|
+
With no <branch>, delete all of them.
|
|
19
|
+
|
|
20
|
+
The --delta marker is left untouched; use git review forget --delta to discard it.
|
|
21
|
+
EOF
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
arg=""
|
|
25
|
+
while [ $# -gt 0 ]; do
|
|
26
|
+
case "$1" in
|
|
27
|
+
-h | --h)
|
|
28
|
+
usage
|
|
29
|
+
exit 0
|
|
30
|
+
;;
|
|
31
|
+
-*)
|
|
32
|
+
echo "error: unknown option $1" >&2
|
|
33
|
+
usage >&2
|
|
34
|
+
exit 1
|
|
35
|
+
;;
|
|
36
|
+
*)
|
|
37
|
+
if [ -z "$arg" ]; then
|
|
38
|
+
arg="$1"
|
|
39
|
+
else
|
|
40
|
+
echo "error: unexpected argument $1" >&2
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
shift
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
cur="$(git symbolic-ref --quiet --short HEAD || true)"
|
|
49
|
+
|
|
50
|
+
if [ -n "$arg" ]; then
|
|
51
|
+
list="review/$arg review-fixes/$arg"
|
|
52
|
+
else
|
|
53
|
+
list="$(git for-each-ref --format='%(refname:short)' refs/heads/review/ refs/heads/review-fixes/)"
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Track whether any cleanup actually happened so the closing message is honest:
|
|
57
|
+
# deleting orphaned refs counts as work even when no review branches exist.
|
|
58
|
+
cleaned=0
|
|
59
|
+
|
|
60
|
+
# No branches is not a stopping point: banked edit refs can still be orphaned
|
|
61
|
+
# and need dropping, so fall through to that cleanup instead of bailing here.
|
|
62
|
+
if [ -n "$list" ]; then
|
|
63
|
+
# Word splitting is intentional: branch names cannot contain whitespace.
|
|
64
|
+
# shellcheck disable=SC2086
|
|
65
|
+
for b in $list; do
|
|
66
|
+
git rev-parse --verify --quiet "refs/heads/$b" >/dev/null || continue
|
|
67
|
+
if [ "$b" = "$cur" ]; then
|
|
68
|
+
echo "skipping $b (currently checked out)" >&2
|
|
69
|
+
continue
|
|
70
|
+
fi
|
|
71
|
+
git branch -D "$b"
|
|
72
|
+
cleaned=1
|
|
73
|
+
done
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# Drop any banked commit-by-commit edit refs, plus any git review finish undo points
|
|
77
|
+
# (refs/review-undo/* and their branch.review/<branch>.reviewundo* config) left
|
|
78
|
+
# behind by a finish that was never aborted.
|
|
79
|
+
if [ -n "$arg" ]; then
|
|
80
|
+
editns="refs/review-edits/$arg/"
|
|
81
|
+
undons="refs/review-undo/$arg/"
|
|
82
|
+
undobranches="$arg"
|
|
83
|
+
else
|
|
84
|
+
editns="refs/review-edits/"
|
|
85
|
+
undons="refs/review-undo/"
|
|
86
|
+
undobranches="$(git for-each-ref --format='%(refname)' refs/review-undo/ |
|
|
87
|
+
sed -n 's#^refs/review-undo/\(.*\)/[^/]*$#\1#p' | sort -u)"
|
|
88
|
+
fi
|
|
89
|
+
for ns in "$editns" "$undons"; do
|
|
90
|
+
# Read refs into a variable first: a `... | while read` pipeline runs the
|
|
91
|
+
# loop body in a subshell, so a `cleaned=1` set there would not survive.
|
|
92
|
+
refs="$(git for-each-ref --format='%(refname)' "$ns")"
|
|
93
|
+
[ -n "$refs" ] || continue
|
|
94
|
+
echo "$refs" | while read -r ref; do
|
|
95
|
+
git update-ref -d "$ref"
|
|
96
|
+
done
|
|
97
|
+
cleaned=1
|
|
98
|
+
done
|
|
99
|
+
# Word splitting is intentional: branch names cannot contain whitespace.
|
|
100
|
+
# shellcheck disable=SC2086
|
|
101
|
+
for b in $undobranches; do
|
|
102
|
+
for k in reviewundohead reviewundokind reviewundosrccreated \
|
|
103
|
+
reviewundoeditstep reviewundoeditold reviewundoouthead reviewundoouttree; do
|
|
104
|
+
git config --unset "branch.review/$b.$k" 2>/dev/null || true
|
|
105
|
+
done
|
|
106
|
+
done
|
|
107
|
+
|
|
108
|
+
if [ "$cleaned" -eq 0 ]; then
|
|
109
|
+
# Saved reviews live in review-saved/*, a namespace git review clean deliberately
|
|
110
|
+
# leaves alone (it owns review/* and review-fixes/* only). Point at them when
|
|
111
|
+
# nothing else was found, so "no review branches" does not read as wrong to
|
|
112
|
+
# someone staring at a review-saved/<branch> they expected this to clear.
|
|
113
|
+
if [ -n "$arg" ]; then
|
|
114
|
+
saved="$(git for-each-ref --format='%(refname:short)' "refs/heads/review-saved/$arg")"
|
|
115
|
+
else
|
|
116
|
+
saved="$(git for-each-ref --format='%(refname:short)' refs/heads/review-saved/)"
|
|
117
|
+
fi
|
|
118
|
+
if [ -n "$saved" ]; then
|
|
119
|
+
echo "no review/ or review-fixes/ branches to clean; you have a saved review — resume it with git review continue or discard it with git review forget --saved" >&2
|
|
120
|
+
else
|
|
121
|
+
echo "no review branches found" >&2
|
|
122
|
+
fi
|
|
123
|
+
fi
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
#
|
|
3
|
+
# git review compare <a> <b> — stage the diff between two commit-ish, read-only.
|
|
4
|
+
#
|
|
5
|
+
# Unlike git review start, compare has no writable source branch: it materialises
|
|
6
|
+
# the diff <a>..<b> (with <a> the lower bound, <b> the tip) as a staged, read-only
|
|
7
|
+
# review you can annotate inline. There is nothing to extract, so git review finish
|
|
8
|
+
# refuses on it. With --step it walks <a>..<b> one commit at a time, like start
|
|
9
|
+
# --step, advancing with git review next.
|
|
10
|
+
#
|
|
11
|
+
set -eu
|
|
12
|
+
|
|
13
|
+
prog="git review compare"
|
|
14
|
+
|
|
15
|
+
usage() {
|
|
16
|
+
cat <<EOF
|
|
17
|
+
usage: $prog <a> <b> [--step]
|
|
18
|
+
|
|
19
|
+
Stage the diff between two commit-ish on a review/<b> branch for inline reading.
|
|
20
|
+
This is a read-only review: there is no branch to write back to, so git review
|
|
21
|
+
finish refuses on it.
|
|
22
|
+
|
|
23
|
+
<a> the lower bound (a branch, tag or commit) — what to diff from
|
|
24
|
+
<b> the upper bound — what to diff to; names the review branch
|
|
25
|
+
--step read one commit at a time; advance with git review next
|
|
26
|
+
EOF
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
die() {
|
|
30
|
+
echo "error: $1" >&2
|
|
31
|
+
exit 1
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
a=""
|
|
35
|
+
b=""
|
|
36
|
+
have_a=0
|
|
37
|
+
have_b=0
|
|
38
|
+
step=0
|
|
39
|
+
# Collect the two positional commit-ish; mirror start's parser, including -- as the
|
|
40
|
+
# end-of-options separator so a commit-ish that starts with "-" stays positional.
|
|
41
|
+
add_positional() {
|
|
42
|
+
if [ "$have_a" -eq 0 ]; then
|
|
43
|
+
a="$1"
|
|
44
|
+
have_a=1
|
|
45
|
+
elif [ "$have_b" -eq 0 ]; then
|
|
46
|
+
b="$1"
|
|
47
|
+
have_b=1
|
|
48
|
+
else
|
|
49
|
+
die "unexpected argument $1"
|
|
50
|
+
fi
|
|
51
|
+
}
|
|
52
|
+
while [ $# -gt 0 ]; do
|
|
53
|
+
case "$1" in
|
|
54
|
+
-h | --h)
|
|
55
|
+
usage
|
|
56
|
+
exit 0
|
|
57
|
+
;;
|
|
58
|
+
--step) step=1 ;;
|
|
59
|
+
--)
|
|
60
|
+
shift
|
|
61
|
+
while [ $# -gt 0 ]; do
|
|
62
|
+
add_positional "$1"
|
|
63
|
+
shift
|
|
64
|
+
done
|
|
65
|
+
break
|
|
66
|
+
;;
|
|
67
|
+
-*)
|
|
68
|
+
echo "error: unknown option $1" >&2
|
|
69
|
+
usage >&2
|
|
70
|
+
exit 1
|
|
71
|
+
;;
|
|
72
|
+
*)
|
|
73
|
+
add_positional "$1"
|
|
74
|
+
;;
|
|
75
|
+
esac
|
|
76
|
+
shift
|
|
77
|
+
done
|
|
78
|
+
|
|
79
|
+
if [ "$have_a" -ne 1 ] || [ "$have_b" -ne 1 ]; then
|
|
80
|
+
die "need two commit-ish to compare: $prog <a> <b>"
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
git rev-parse --git-dir >/dev/null 2>&1 || die "not a git repository"
|
|
84
|
+
|
|
85
|
+
# Shared helpers, sourced from the libexec directory the dispatcher points us at.
|
|
86
|
+
# shellcheck source=../git-review-lib.sh
|
|
87
|
+
. "${GIT_REVIEW_LIBEXEC:?}/git-review-lib.sh"
|
|
88
|
+
|
|
89
|
+
acommit="$(git rev-parse --verify --quiet "$a^{commit}")" || die "unknown commit: $a"
|
|
90
|
+
bcommit="$(git rev-parse --verify --quiet "$b^{commit}")" || die "unknown commit: $b"
|
|
91
|
+
[ "$acommit" != "$bcommit" ] || die "$a and $b are the same commit; nothing to compare"
|
|
92
|
+
|
|
93
|
+
if ! git diff --quiet || ! git diff --cached --quiet; then
|
|
94
|
+
die "you have local changes; commit or stash them first"
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# Remember where to return to when the compare is aborted.
|
|
98
|
+
returnbranch="$(git symbolic-ref --quiet --short HEAD || true)"
|
|
99
|
+
|
|
100
|
+
# Pick a refname-safe identity for the review branch from <b> (the tip). A tag or
|
|
101
|
+
# branch name passes through unchanged, for a readable review/<b>; anything git
|
|
102
|
+
# would reject as a ref component (HEAD~3, a^, x:y) falls back to <b>'s short hash,
|
|
103
|
+
# so the branch and the per-step edit refs (refs/review-edits/<name>/*) are always
|
|
104
|
+
# valid. This name is the review's identity for status/list/clean, never a writeback
|
|
105
|
+
# target — compare is read-only.
|
|
106
|
+
if git check-ref-format "review/$b" 2>/dev/null; then
|
|
107
|
+
name="$b"
|
|
108
|
+
else
|
|
109
|
+
name="$(git rev-parse --short "$bcommit")"
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
rb="review/$name"
|
|
113
|
+
git rev-parse --verify --quiet "refs/heads/$rb" >/dev/null &&
|
|
114
|
+
die "$rb already exists; run git review clean $name first"
|
|
115
|
+
git rev-parse --verify --quiet "refs/heads/review-saved/$name" >/dev/null &&
|
|
116
|
+
die "you have a saved review of $name; resume it with git review continue $name or discard it with git review forget --saved $name"
|
|
117
|
+
|
|
118
|
+
if [ "$step" -eq 1 ]; then
|
|
119
|
+
# --first-parent --no-merges: walk only the range's own commits, skipping merge
|
|
120
|
+
# commits, exactly like start --step so git review next/prev behave identically.
|
|
121
|
+
commits="$(git rev-list --reverse --first-parent --no-merges "$acommit..$bcommit")"
|
|
122
|
+
[ -n "$commits" ] || die "no commits to compare in range $a..$b"
|
|
123
|
+
count="$(printf '%s\n' "$commits" | wc -l | tr -d ' ')"
|
|
124
|
+
first="$(printf '%s\n' "$commits" | sed -n '1p')"
|
|
125
|
+
|
|
126
|
+
git switch -q -c "$rb" "$first" || die "could not create $rb"
|
|
127
|
+
git reset --soft HEAD^
|
|
128
|
+
git config "branch.$rb.reviewsource" "$name"
|
|
129
|
+
git config "branch.$rb.reviewbase" "$a"
|
|
130
|
+
git config "branch.$rb.reviewtip" "$bcommit"
|
|
131
|
+
git config "branch.$rb.reviewstart" "$acommit"
|
|
132
|
+
git config "branch.$rb.reviewmode" "step"
|
|
133
|
+
git config "branch.$rb.reviewcount" "$count"
|
|
134
|
+
git config "branch.$rb.reviewstep" "1"
|
|
135
|
+
git config "branch.$rb.reviewreadonly" "1"
|
|
136
|
+
if [ -n "$returnbranch" ]; then
|
|
137
|
+
git config "branch.$rb.reviewreturn" "$returnbranch"
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
echo "$rb ready (compare $a..$b, read-only) — walking $count commit(s) one at a time"
|
|
141
|
+
show_commit "$first" 1 "$count"
|
|
142
|
+
exit 0
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# Whole range: create review/<name> at <b> and soft-reset to <a> so the staged diff
|
|
146
|
+
# is exactly <a>..<b>, with a clean working tree — the same shape as start, minus the
|
|
147
|
+
# merged-base folding (compare's bounds are explicit) and any writeback metadata.
|
|
148
|
+
git switch -q -c "$rb" "$bcommit" || die "could not create $rb"
|
|
149
|
+
git reset -q --soft "$acommit"
|
|
150
|
+
|
|
151
|
+
git config "branch.$rb.reviewsource" "$name"
|
|
152
|
+
git config "branch.$rb.reviewbase" "$a"
|
|
153
|
+
git config "branch.$rb.reviewtip" "$bcommit"
|
|
154
|
+
git config "branch.$rb.reviewreadonly" "1"
|
|
155
|
+
if [ -n "$returnbranch" ]; then
|
|
156
|
+
git config "branch.$rb.reviewreturn" "$returnbranch"
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
echo "$rb ready (compare $a..$b, read-only) — the staged diff is for reading; git review finish will refuse"
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
#
|
|
3
|
+
# git review continue — resume a review put aside with git review save.
|
|
4
|
+
#
|
|
5
|
+
# It turns review-saved/<branch> back into the active review/<branch>, restoring
|
|
6
|
+
# the exact state you left: in whole mode the staged PR diff with your edits in the
|
|
7
|
+
# working tree; in step mode the commit you were on, its edits, and every other
|
|
8
|
+
# banked edit — so review-next / review-prev keep working as before.
|
|
9
|
+
#
|
|
10
|
+
# With no argument it resumes the only saved review, or lists them if there is more
|
|
11
|
+
# than one. Name a branch to pick a specific one.
|
|
12
|
+
#
|
|
13
|
+
set -eu
|
|
14
|
+
|
|
15
|
+
prog="git review continue"
|
|
16
|
+
|
|
17
|
+
usage() {
|
|
18
|
+
cat <<EOF
|
|
19
|
+
usage: $prog [<branch>]
|
|
20
|
+
|
|
21
|
+
Resume a review saved with git review save. With no argument, resume the only
|
|
22
|
+
saved review (or list them if there is more than one).
|
|
23
|
+
EOF
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
die() {
|
|
27
|
+
echo "error: $1" >&2
|
|
28
|
+
exit 1
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
arg=""
|
|
32
|
+
while [ $# -gt 0 ]; do
|
|
33
|
+
case "$1" in
|
|
34
|
+
-h | --h)
|
|
35
|
+
usage
|
|
36
|
+
exit 0
|
|
37
|
+
;;
|
|
38
|
+
-*)
|
|
39
|
+
echo "error: unknown option $1" >&2
|
|
40
|
+
usage >&2
|
|
41
|
+
exit 1
|
|
42
|
+
;;
|
|
43
|
+
*)
|
|
44
|
+
if [ -z "$arg" ]; then
|
|
45
|
+
arg="$1"
|
|
46
|
+
else
|
|
47
|
+
die "unexpected argument $1"
|
|
48
|
+
fi
|
|
49
|
+
;;
|
|
50
|
+
esac
|
|
51
|
+
shift
|
|
52
|
+
done
|
|
53
|
+
|
|
54
|
+
git rev-parse --git-dir >/dev/null 2>&1 || die "not a git repository"
|
|
55
|
+
|
|
56
|
+
# Shared helpers, sourced from the libexec directory the dispatcher points us at.
|
|
57
|
+
# shellcheck source=../git-review-lib.sh
|
|
58
|
+
. "${GIT_REVIEW_LIBEXEC:?}/git-review-lib.sh"
|
|
59
|
+
|
|
60
|
+
# Pick the saved review to resume.
|
|
61
|
+
if [ -n "$arg" ]; then
|
|
62
|
+
src="$arg"
|
|
63
|
+
saved="review-saved/$src"
|
|
64
|
+
git rev-parse --verify --quiet "refs/heads/$saved" >/dev/null ||
|
|
65
|
+
die "no saved review for $src (looked for $saved)"
|
|
66
|
+
else
|
|
67
|
+
branches="$(git for-each-ref --format='%(refname:short)' refs/heads/review-saved/)"
|
|
68
|
+
[ -n "$branches" ] || die "no saved reviews; save one with git review save"
|
|
69
|
+
n="$(printf '%s\n' "$branches" | wc -l | tr -d ' ')"
|
|
70
|
+
if [ "$n" -gt 1 ]; then
|
|
71
|
+
echo "more than one saved review — name the one to resume:" >&2
|
|
72
|
+
printf '%s\n' "$branches" | sed -n 's#^review-saved/# git review continue #p' >&2
|
|
73
|
+
exit 1
|
|
74
|
+
fi
|
|
75
|
+
saved="$branches"
|
|
76
|
+
src="${saved#review-saved/}"
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
rb="review/$src"
|
|
80
|
+
git rev-parse --verify --quiet "refs/heads/$rb" >/dev/null &&
|
|
81
|
+
die "$rb is already active; finish or abort it before resuming the saved one"
|
|
82
|
+
|
|
83
|
+
# Resuming materialises a working tree, so refuse to clobber local changes.
|
|
84
|
+
if ! git diff --quiet || ! git diff --cached --quiet; then
|
|
85
|
+
die "you have local changes; commit or stash them first"
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
mode="$(git config "branch.$saved.reviewmode" || echo whole)"
|
|
89
|
+
tip="$(git config "branch.$saved.reviewtip" || true)"
|
|
90
|
+
# A saved review missing its tip (hand-edited or half-deleted config) would let
|
|
91
|
+
# set -e kill us silently while restoring; report it instead.
|
|
92
|
+
[ -n "$tip" ] || die "missing review metadata for $saved; was it created with git review start?"
|
|
93
|
+
|
|
94
|
+
# restore_meta <key>: copy branch.<saved>.<key> to branch.<rb>.<key> when set.
|
|
95
|
+
# Explicit if so the function returns 0 for absent optional keys (set -e safety).
|
|
96
|
+
restore_meta() {
|
|
97
|
+
v="$(git config "branch.$saved.$1" || true)"
|
|
98
|
+
if [ -n "$v" ]; then
|
|
99
|
+
git config "branch.$rb.$1" "$v"
|
|
100
|
+
fi
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if [ "$mode" = "step" ]; then
|
|
104
|
+
start="$(git config "branch.$saved.reviewstart" || true)"
|
|
105
|
+
count="$(git config "branch.$saved.reviewcount" || true)"
|
|
106
|
+
step="$(git config "branch.$saved.reviewstep" || true)"
|
|
107
|
+
if [ -z "$start" ] || [ -z "$count" ] || [ -z "$step" ]; then
|
|
108
|
+
die "missing review metadata for $saved; was it created with git review start?"
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# Bring the banked edits back to where git review next/prev expect them.
|
|
112
|
+
git for-each-ref --format='%(refname)' "refs/review-saved-edits/$src/" | while read -r ref; do
|
|
113
|
+
n="${ref##*/}"
|
|
114
|
+
git update-ref "refs/review-edits/$src/$n" "$(git rev-parse "$ref")"
|
|
115
|
+
git update-ref -d "$ref"
|
|
116
|
+
done
|
|
117
|
+
|
|
118
|
+
commits="$(git rev-list --reverse --first-parent --no-merges "$start..$tip")"
|
|
119
|
+
cstep="$(printf '%s\n' "$commits" | sed -n "${step}p")"
|
|
120
|
+
|
|
121
|
+
# Land on the saved step exactly like review-next's goto_step does: hard-reset
|
|
122
|
+
# to the commit, re-apply its banked edits, then soft-reset so its diff is staged.
|
|
123
|
+
git switch -q -c "$rb" "$cstep"
|
|
124
|
+
ref="refs/review-edits/$src/$step"
|
|
125
|
+
if git rev-parse --verify --quiet "$ref" >/dev/null; then
|
|
126
|
+
git diff --binary "${ref}^" "$ref" | git apply ||
|
|
127
|
+
die "could not restore your edits for step $step"
|
|
128
|
+
fi
|
|
129
|
+
git reset -q --soft "$cstep^"
|
|
130
|
+
|
|
131
|
+
git config "branch.$rb.reviewmode" step
|
|
132
|
+
restore_meta reviewstart
|
|
133
|
+
restore_meta reviewcount
|
|
134
|
+
git config "branch.$rb.reviewstep" "$step"
|
|
135
|
+
restore_meta reviewsource
|
|
136
|
+
restore_meta reviewbase
|
|
137
|
+
restore_meta reviewtip
|
|
138
|
+
restore_meta reviewprevreviewed
|
|
139
|
+
restore_meta reviewlocal
|
|
140
|
+
restore_meta reviewreturn
|
|
141
|
+
|
|
142
|
+
git branch -D "$saved" >/dev/null
|
|
143
|
+
|
|
144
|
+
echo "resumed review of $src — step $step/$count"
|
|
145
|
+
show_commit "$cstep" "$step" "$count"
|
|
146
|
+
else
|
|
147
|
+
# whole mode: rebuild the pristine review state (PR diff staged at the lower
|
|
148
|
+
# bound) and replay your edits into the working tree, leaving exactly what you
|
|
149
|
+
# had when you saved.
|
|
150
|
+
# Without || true a deleted reviewsavedlower (hand-edited or half-deleted
|
|
151
|
+
# config) would let set -e kill us mid-restore with no message; report it and
|
|
152
|
+
# bail before creating the review branch, the way the step path validates its
|
|
153
|
+
# keys above.
|
|
154
|
+
lower="$(git config "branch.$saved.reviewsavedlower" || true)"
|
|
155
|
+
[ -n "$lower" ] || die "missing review metadata for $saved; was it created with git review start?"
|
|
156
|
+
|
|
157
|
+
git switch -q -c "$rb" "$tip"
|
|
158
|
+
git reset -q --soft "$lower"
|
|
159
|
+
# Pipe the saved edits straight from git diff into git apply. Capturing the
|
|
160
|
+
# patch in a shell variable drops its NUL bytes and trailing newline, which
|
|
161
|
+
# corrupts a binary hunk and silently lost a binary edit on resume. The step
|
|
162
|
+
# path above (refs/review-edits) already pipes the diff directly.
|
|
163
|
+
if ! git diff --quiet "$tip" "$saved"; then
|
|
164
|
+
git diff --binary "$tip" "$saved" | git apply ||
|
|
165
|
+
die "could not restore your edits"
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
restore_meta reviewsource
|
|
169
|
+
restore_meta reviewbase
|
|
170
|
+
restore_meta reviewtip
|
|
171
|
+
restore_meta reviewprevreviewed
|
|
172
|
+
restore_meta reviewlocal
|
|
173
|
+
restore_meta reviewreturn
|
|
174
|
+
|
|
175
|
+
git branch -D "$saved" >/dev/null
|
|
176
|
+
|
|
177
|
+
echo "resumed review of $src — the staged diff is the PR; edit, then run git review finish"
|
|
178
|
+
fi
|