-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgit-fix-walk
More file actions
executable file
·286 lines (243 loc) · 7.84 KB
/
git-fix-walk
File metadata and controls
executable file
·286 lines (243 loc) · 7.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
#!/bin/bash
set -e -o pipefail
shortusage() {
cat >&2 <<-EOF
git fix-walk start REF COMMAND {ARGS}
git fix-walk continue
git fix-walk abort
EOF
}
usage() {
shortusage
cat >&2 <<-EOF
fix-walk combines aspects of git bisect and git rebase to walk through
a range of commits, fixing any failing commits as it goes.
fix-walk start begins a fix-walk by specifying the last known good commit
and a command that can be used to test if a commit is good.
fix-walk will then find failing commits and prompt the user to fix them.
Once the user has (generally by amending or adding a commit) fixed the commit,
all remaining commits will be rebased on top of it (note: like a normal rebase,
the user may be prompted to fix conflicts during this operation).
This will repeat until all commits in the range are good, at which time the
original HEAD will be updated (if a branch) to point at the final rebased result.
EOF
}
# Implementation outline:
# start (check: no refs/fix-walk/* exist, no .git/FIX-WALK-COMMAND, no active rebase or bisect)
# set refs/fix-walk/top = HEAD
# set refs/fix-walk/fixing = given REF
# store run command in .git/FIX-WALK-COMMAND for safekeeping
# git bisect start
# goto "run next"
# "run next"
# run command against fix-walk/top
# if success:
# goto "finish"
# git bisect: set fix-walk/top bad, set fix-walk/fixing good
# git bisect run
# set refs/fix-walk/fixing = HEAD
# drop to user for interaction
# continue (refs/fix-walk/fixed does not exist): "commit fixed"
# run command to confirm actually fixed
# set refs/fix-walk/fixed = HEAD
# if fixed-walk/fixing == fixed-walk/top:
# We're done. Goto "finish"
# rebase fix-walk/fixing..fix-walk/top onto fix-walk/fixed and update fix-walk/top
# this rebase may have conflicts. if so, drop to user for interaction
# otherwise goto "rebase finished"
# continue (refs/fix-walk/fixed exists, active rebase): "rebase ongoing"
# Tell user to complete rebase before continuing
# continue (refs/fix-walk/fixed exists, no active rebase): "rebase finished"
# set fix-walk/fixing = fix-walk/fixed
# delete fix-walk/fixed
# goto run next
# abort
# Note this is designed to be idempotent and run even if we're in a weird state.
# if rebase in progress:
# git rebase --abort
# if bisect in progress:
# git bisect reset
# delete refs/fix-walk/*
# delete .git/FIX-WALK-COMMAND
# "finish"
# git bisect reset, restoring original head
# git reset --hard onto fix-walk/top, so original head now points to fully fixed branch
# delete refs/fix-walk/*
# delete .git/FIX-WALK-COMMAND
# WARNING: Uses declare -a / eval to save an array to a file. I _think_ this is safe,
# especially since it's code to be executed anyway.
die() {
local code="$1"
shift
echo "$@" >&2
exit "$code"
}
save_command() {
local gitdir
gitdir=$(git rev-parse --git-dir)
COMMAND=("$@") # note: COMMAND is global
# output one per line
printf "%s\n" "${COMMAND[@]}" > "$gitdir/FIX-WALK-COMMAND"
}
load_command() {
local gitdir
gitdir=$(git rev-parse --git-dir)
COMMAND=() # declares COMMAND array as a global
while read line; do
COMMAND+=("$line")
done <"$gitdir/FIX-WALK-COMMAND"
}
run_command() {
[ -n "${COMMAND[0]}" ] || load_command
[ -n "${COMMAND[0]}" ] || die 3 'Failed to load command'
echo "Running command: ${COMMAND[*]}"
set +e
"${COMMAND[@]}"
code=$?
set -e
return "$code"
}
delete_command_file() {
local gitdir
gitdir=$(git rev-parse --git-dir)
rm -f "$gitdir/FIX-WALK-COMMAND"
}
check_fix_walk_active() {
# err on the side of it being active if in a weird state
local gitdir
gitdir=$(git rev-parse --git-dir)
[ -n "$(git for-each-ref refs/fix-walk/)" ] || [ -f "$gitdir/FIX-WALK-COMMAND" ]
}
check_bisect_active() {
# TODO
echo "WARNING: ASSUMING NO BISECT ACTIVE (fixme)"
return 1
}
check_rebase_active() {
# TODO
echo "WARNING: ASSUMING NO REBASE ACTIVE (fixme)"
return 1
}
start() {
if [ "$#" -lt 2 ]; then
echo "Not enough args for start"
echo
shortusage
return 255
fi
local firstgood="$1"
shift
check_fix_walk_active && die 1 "You appear to already be doing a fix walk. Please 'git fix-walk abort' first."
check_bisect_active && die 1 "You appear to be currently bisecting. Please 'git bisect reset' first."
check_rebase_active && die 1 "You appear to be currently rebasing. Please 'git rebase --abort' first."
git update-ref refs/fix-walk/top HEAD
git update-ref refs/fix-walk/fixing "$firstgood"
save_command "$@"
git bisect start
run_next
}
run_next() {
# Assumptions: fix-walk/fixing is the last known good commit.
# Afterwards, it will be the first known bad commit.
# check if we're done by re-checking if the top commit is still bad
git checkout refs/fix-walk/top
if run_command; then
finish
return
fi
git bisect good refs/fix-walk/fixing
git bisect bad refs/fix-walk/top
echo "Bisecting from $(git rev-parse refs/fix-walk/fixing) to $(git rev-parse refs/fix-walk/top)"
bisect_output=$(mktemp)
git bisect run "${COMMAND[@]}" | tee "$bisect_output"
# git bisect only leaves you at 'last tested', not 'first bad', so we have to parse the output
first_bad=$(grep -E '^[0-9a-f]+ is the first bad commit$' "$bisect_output" | cut -d' ' -f1)
echo "Checking out $first_bad"
git checkout "$first_bad"
git update-ref refs/fix-walk/fixing HEAD
echo
echo "This is the first bad commit. Please fix it (generally with a new commit or by amending this commit) and run 'git fix-walk continue'."
}
commit_fixed() {
# Assumptions: a fixed version of the bad commit is checked out.
# check it's really fixed (worthwhile tradeoff to catch user error)
run_command || die 2 "This commit still has problems. Try again or run 'git fix-walk abort' to reset to your initial state."
git update-ref refs/fix-walk/fixed HEAD
echo "Rebasing $(git rev-parse refs/fix-walk/fixing)..$(git rev-parse refs/fix-walk/top) onto $(git rev-parse refs/fix-walk/fixed)"
git checkout refs/fix-walk/top
set +e
git rebase --onto refs/fix-walk/fixed refs/fix-walk/fixing
code=$?
set -e
if [ "$code" -eq 128 ]; then
echo "The rebase on top of your fix has encountered a conflict. Please resolve it via the normal means and 'git fix-walk continue' when the rebase is complete."
return 0
fi
rebase_finished
}
rebase_ongoing() {
echo "Your rebase appears to still be ongoing. Please complete the rebase before continuing, or 'git fix-walk abort' to reset to your initial state."
}
rebase_finished() {
git update-ref refs/fix-walk/top HEAD
git update-ref refs/fix-walk/fixing refs/fix-walk/fixed
git update-ref -d refs/fix-walk/fixed
run_next
}
finish() {
git bisect reset # this restores the original checkout
git reset --hard refs/fix-walk/top # if original checkout was a branch, this updates it. otherwise just checks out final state as dangling head.
git update-ref -d refs/fix-walk/fixing
git update-ref -d refs/fix-walk/top
delete_command_file
echo "Fix walk done. Your original branch (if any) has been updated to point at the final result."
}
abort() {
# Note this is designed to be idempotent and run even if we're in a weird state.
set +e
check_rebase_active && git rebase --abort
check_bisect_active && git bisect reset
git for-each-ref --format='delete %(refname)' refs/fix-walk | git update-ref --stdin
delete_command_file
set -e
echo "Fix walk aborted."
}
cont() {
# (continue is a reserved word)
# Does dispatch to various next steps for continue.
if ! check_fix_walk_active; then
echo "No fix walk appears to be in progress."
elif ! git show-ref --quiet --verify refs/fix-walk/fixed; then
commit_fixed
elif check_rebase_active; then
rebase_ongoing
else
rebase_finished
fi
}
main() {
action="$1"
shift 1 || true # may fail if no args
case "$action" in
'start')
start "$@"
;;
'continue')
cont "$@"
;;
'abort')
abort "$@"
;;
'help' | '-h' | '--help' | '')
usage
return 255
;;
*)
echo "Unknown subcommand: $action"
echo
shortusage
return 255
;;
esac
}
main "$@"