test-lib.sh: make sure exported variables get cached
[topgit/pro.git] / tg-revert.sh
blob836bfe1ecf0e8c397dc7ad915bf9f4280edf413b
1 #!/bin/sh
2 # TopGit revert command
3 # Copyright (C) 2015 Kyle J. McKay <mackyle@gmail.com>
4 # All rights reserved.
5 # GPLv2
7 USAGE="Usage: ${tgname:-tg} [...] revert (-f | -i | -n) [-q] [--tgish-only] [--no-deps] [--no-stash] [--exclude <ref>...] (<tagname> | --stash) [<ref>...]"
8 USAGE="$USAGE$lf Or: ${tgname:-tg} [...] revert [-l] [--no-short] [--hash] [--tgish-only] [(--deps | --rdeps)] [--exclude <ref>...] (<tagname> | --stash) [(--heads | <ref>...)]"
10 usage()
12 if [ "${1:-0}" != 0 ]; then
13 printf '%s\n' "$USAGE" >&2
14 else
15 printf '%s\n' "$USAGE"
17 exit ${1:-0}
20 ## Parse options
22 force=
23 interact=
24 dryrun=
25 list=
26 deps=
27 rdeps=
28 tgish=
29 nodeps=
30 nostash=
31 exclude=
32 quiet=
33 short=
34 hashonly=
36 while [ $# -gt 0 ]; do case "$1" in
37 -h|--help)
38 usage
40 -q|--quiet)
41 quiet=1
43 -l|--list)
44 list=1
46 --short|--short=*|--no-short)
47 short="$1"
49 --hash|--hash-only)
50 hashonly=1
52 --deps|--deps-only)
53 deps=1
55 --rdeps)
56 rdeps=1
58 --tgish-only)
59 tgish=1
61 -f|--force)
62 force=1
64 -i|--interactive)
65 interact=1
67 -n|--dry-run)
68 dryrun=1
70 --no-deps)
71 nodeps=1
73 --no-stash)
74 nostash=1
76 --exclude=*)
77 [ -n "${1#--exclude=}" ] || die "--exclude= requires a ref name"
78 case "${1#--exclude=}" in refs/*) rn="${1#--exclude=}";; *) rn="refs/heads/${1#--exclude=} refs/$topbases/${1#--exclude=}"; esac
79 exclude="$exclude $rn";;
80 --exclude)
81 shift
82 [ -n "$1" ] || die "--exclude requires a ref name"
83 case "$1" in refs/*) rn="$1";; *) rn="refs/heads/$1 refs/$topbases/$1"; esac
84 exclude="$exclude $rn";;
85 --)
86 shift
87 break
89 --stash|--stash"@{"*"}")
90 break
92 -?*)
93 echo "Unknown option: $1" >&2
94 usage 1
97 break
99 esac; shift; done
100 [ -z "$exclude" ] || exclude="$exclude "
102 [ -z "$list$short$hashonly" -o -z "$force$interact$dryrun$nodeps$nostash" ] || usage 1
103 [ -z "$force$interact$dryrun" -o -z "$list$short$hashonly$deps$rdeps" ] || usage 1
104 [ -z "$deps" -o -z "$rdeps" ] || usage 1
105 [ -n "$list$force$interact$dryrun" ] || list=1
106 [ -z "$list" -o -n "$short" ] || if [ -n "$hashonly" ]; then short="--no-short"; else short="--short"; fi
107 [ -n "$1" ] || { echo "Tag name required" >&2; usage 1; }
108 tagname="$1"
109 shift
110 [ -n "$list" -o "$1" != "--heads" ] || usage 1
111 [ "$tagname" != "--stash" ] || tagname=refs/tgstash
112 case "$tagname" in --stash"@{"*"}")
113 strip="${tagname#--stash??}"
114 strip="${strip%?}"
115 tagname="refs/tgstash@{$strip}"
116 esac
117 refname="$tagname"
118 case "$refname" in HEAD|refs/*);;*)
119 suffix="${refname%@*}"
120 suffix="${refname#$suffix}"
121 refname="${refname%$suffix}"
122 if reftest="$(git rev-parse --revs-only --symbolic-full-name "$refname" -- 2>/dev/null)" &&
123 [ -n "$reftest" ]; then
124 refname="$reftest$suffix"
125 else
126 if hash="$(git rev-parse --quiet --verify "$refname$suffix")"; then
127 refname="$hash"
128 else
129 refname="refs/tags/$refname$suffix"
132 esac
133 reftype=tag
134 case "$refname" in refs/tags/*) tagname="${refname#refs/tags/}";; *) reftype=ref; tagname="$refname"; esac
135 git rev-parse --verify --quiet "$refname^{tag}" -- >/dev/null || die "not annotated/signed tag: $refname"
136 tgf="$(get_temp tag)"
137 trf="$(get_temp refs)"
138 tagdataref="$refname^{tag}"
139 while
140 git cat-file tag "$tagdataref" >"$tgf" || die "cannot read tag: $refname"
141 sed -ne '/^-----BEGIN TOPGIT REFS-----$/,/^-----END TOPGIT REFS-----$/p' <"$tgf" |
142 sed -ne "/^\\($octet20\\) \\(refs\/[^ $tab][^ $tab]*\\)\$/{s//\\2 \\1/;p;}" |
143 sed -e "s,^refs/$oldbases/,refs/$topbases/,g" |
144 LC_ALL=C sort -u -b -k1,1 >"$trf"
145 ! [ -s "$trf" ]
147 # If it's a tag of a tag, dereference it and try again
148 read -r field tagtype <<-EOT || break
149 $(sed -n '1,/^$/p' <"$tgf" | grep '^type [^ ][^ ]*$' || :)
151 [ "$tagtype" = "tag" ] || break
152 read -r field tagdataref <<-EOT || break
153 $(sed -n '1,/^$/p' <"$tgf" | grep '^object [^ ][^ ]*$' || :)
155 [ -n "$tagdataref" ] || break
156 tagdataref="$tagdataref^{tag}"
157 git rev-parse --verify --quiet "$tagdataref" -- >/dev/null || break
158 done
159 [ -s "$trf" ] || die "$reftype $tagname does not contain a TOPGIT REFS section"
160 rcnt=$(( $(wc -l <"$trf") ))
161 vcnt=$(( $(cut -d ' ' -f 2 <"$trf" | git cat-file --batch-check='%(objectname)' | grep -v ' missing$' | wc -l) ))
162 [ "$rcnt" -eq "$vcnt" ] || die "$reftime $tagname contains $rcnt ref(s) but only $vcnt are still valid"
163 cat "$trf" >"$tg_ref_cache"
164 create_ref_dirs
165 tg_ref_cache_only=1
166 tg_read_only=1
168 [ $# -ne 0 -o -z "$rdeps$deps" ] || set -- --heads
169 [ $# -ne 1 -o -z "$deps" -o "$1" != "--heads" ] || { deps=; set --; }
170 if [ $# -eq 1 -a "$1" = "--heads" ]; then
171 srt="$(get_temp sort)"
172 LC_ALL=C sort -b -k2,2 <"$trf" >"$srt"
173 set -- $(
174 git merge-base --independent $(cut -d ' ' -f 2 <"$srt") |
175 LC_ALL=C sort -b -k1,1 |
176 join -2 2 -o 2.1 - "$srt" |
177 LC_ALL=C sort)
180 is_tgish() {
181 case "$1" in
182 refs/"$topbases"/*)
183 ref_exists "refs/heads/${1#refs/$topbases/}"
185 refs/heads/*)
186 ref_exists "refs/$topbases/${1#refs/heads/}"
191 esac
194 refs=
195 for b; do
196 exp=
197 case "$b" in refs/*) exp=1; rn="$b";; *) rn="refs/heads/$b"; esac
198 ref_exists "$rn" || die "not present in tag data (try --list): $rn"
199 case " $refs " in *" $rn "*);;*)
200 refs="${refs:+$refs }$rn"
201 if [ -z "$list" ] && [ -z "$nodeps" -o -z "$exp" ] && is_tgish "$rn"; then
202 case "$rn" in
203 refs/"$topbases"/*)
204 refs="$refs refs/heads/${rn#refs/$topbases/}"
206 refs/heads/*)
207 refs="$refs refs/$topbases/${rn#refs/heads/}"
209 esac
211 esac
212 done
214 show_dep() {
215 case "$exclude" in *" refs/heads/$_dep "*) return; esac
216 case " $seen_deps " in *" $_dep "*) return 0; esac
217 seen_deps="${seen_deps:+$seen_deps }$_dep"
218 [ -z "$tgish" -o -n "$_dep_is_tgish" ] || return 0
219 printf 'refs/heads/%s\n' "$_dep"
220 [ -z "$_dep_is_tgish" ] ||
221 printf 'refs/%s/%s\n' "$topbases" "$_dep"
224 show_deps()
226 no_remotes=1
227 recurse_deps_exclude=
228 while read _b && [ -n "$_b" ]; do
229 case "$exclude" in *" $_b "*) continue; esac
230 if ! is_tgish "$_b"; then
231 [ -z "$tgish" ] || continue
232 printf '%s\n' "$_b"
233 continue
235 case "$_b" in refs/"$topbases"/*) _b="refs/heads/${_b#refs/$topbases/}"; esac
236 _b="${_b#refs/heads/}"
237 case " $recurse_deps_exclude " in *" $_b "*) continue; esac
238 seen_deps=
239 _dep="$_b"; _dep_is_tgish=1; show_dep
240 recurse_deps show_dep "$_b"
241 recurse_deps_exclude="$recurse_deps_exclude $seen_deps"
242 done
245 show_rdep()
247 case "$exclude" in *" refs/heads/$_dep "*) return; esac
248 [ -z "$tgish" -o -n "$_dep_is_tgish" ] || return 0
249 if [ -n "$hashonly" ]; then
250 printf '%s %s\n' "$_depchain" "$(ref_exists_rev_short "refs/heads/$_dep" $short)"
251 else
252 printf '%s %s\n' "$_depchain" "$(ref_exists_rev_short "refs/heads/$_dep" $short)~$_dep"
256 show_rdeps()
258 no_remotes=1
259 show_break=
260 seen_deps=
261 while read _b && [ -n "$_b" ]; do
262 case "$exclude" in *" $_b "*) continue; esac
263 if ! is_tgish "$_b"; then
264 [ -z "$tgish" ] || continue
265 [ -z "$showbreak" ] || echo
266 showbreak=1
267 if [ -n "$hashonly" ]; then
268 printf '%s\n' "$(ref_exists_rev_short "refs/heads/$_b" $short)"
269 else
270 printf '%s\n' "$(ref_exists_rev_short "refs/heads/$_b" $short)~$_b"
272 continue
274 case "$_b" in refs/"$topbases"/*) _b="refs/heads/${_b#refs/$topbases/}"; esac
275 _b="${_b#refs/heads/}"
276 case " $seen_deps " in *" $_b "*) continue; esac
277 seen_deps="$seen_deps $_b"
278 [ -z "$showbreak" ] || echo
279 showbreak=1
281 if [ -n "$hashonly" ]; then
282 printf '%s\n' "$(ref_exists_rev_short "refs/heads/$_b" $short)"
283 else
284 printf '%s\n' "$(ref_exists_rev_short "refs/heads/$_b" $short)~$_b"
286 recurse_preorder=1
287 recurse_deps show_rdep "$_b"
288 } | sed -e 's/[^ ][^ ]*[ ]/ /g' -e 's/~/ /'
289 done
292 refslist() {
293 [ -z "$refs" ] || sed 'y/ /\n/' <<-EOT
294 $refs
298 if [ -n "$list" ]; then
299 if [ -z "$deps$rdeps" ]; then
300 while read -r name rev; do
301 case "$exclude" in *" $name "*) continue; esac
302 [ -z "$refs" ] || case " $refs " in *" $name "*);;*) continue; esac
303 [ -z "$tgish" ] || is_tgish "$name" || continue
304 if [ -n "$hashonly" ]; then
305 printf '%s\n' "$(git rev-parse --verify --quiet $short "$rev" --)"
306 else
307 printf '%s %s\n' "$(git rev-parse --verify --quiet $short "$rev" --)" "$name"
309 done <"$trf"
310 exit 0
312 if [ -n "$deps" ]; then
313 refslist | show_deps | LC_ALL=C sort -u -b -k1,1 |
314 join - "$trf" |
315 while read -r name rev; do
316 if [ -n "$hashonly" ]; then
317 printf '%s\n' "$(git rev-parse --verify --quiet $short "$rev" --)"
318 else
319 printf '%s %s\n' "$(git rev-parse --verify --quiet $short "$rev" --)" "$name"
321 done
322 exit 0
324 refslist | show_rdeps
325 exit 0
327 insn="$(get_temp isns)"
329 get_short() {
330 [ -n "$interact" ] || { printf '%s' "$1"; return 0; }
331 git rev-parse --verify --quiet --short "$1" --
334 if [ -n "$nodeps" -o -z "$refs" ]; then
335 while read -r name rev; do
336 case "$exclude" in *" $name "*) continue; esac
337 [ -z "$refs" ] || case " $refs " in *" $name "*);;*) continue; esac
338 [ -z "$tgish" ] || is_tgish "$name" || continue
339 printf 'revert %s %s\n' "$(get_short "$rev")" "$name"
340 done <"$trf" | LC_ALL=C sort -u -b -k3,3 >"$insn"
341 else
342 refslist | show_deps | LC_ALL=C sort -u -b -k1,1 |
343 join - "$trf" |
344 while read -r name rev; do
345 printf 'revert %s %s\n' "$(get_short "$rev")" "$name"
346 done >"$insn"
348 if [ -n "$interact" ]; then
349 count=$(( $(wc -l <"$insn") ))
350 cat <<EOT >>"$insn"
352 # Revert using $refname data ($count command(s))
354 # Commands:
355 # r, revert = revert ref to specified hash
357 # Note that changing the hash value shown here will have NO EFFECT.
359 # If you remove a line here THAT REVERT WILL BE SKIPPED.
361 # However, if you remove everything, the revert will be aborted.
363 run_editor "$insn" ||
364 die "there was a problem with the editor '$tg_editor'"
365 git stripspace -s <"$insn" >"$insn"+
366 mv -f "$insn"+ "$insn"
367 [ -s "$insn" ] || die "nothing to do"
368 while read -r op hash ref; do
369 [ "$op" = "r" -o "$op" = "revert" ] ||
370 die "invalid op in instruction: $op $hash $ref"
371 case "$ref" in refs/?*);;*)
372 die "invalid ref in instruction: $op $hash $ref"
373 esac
374 ref_exists "$ref" ||
375 die "unknown ref in instruction: $op $hash $ref"
376 done <"$insn"
378 msg="tgrevert: $reftype $tagname ($(( $(wc -l <"$insn") )) command(s))"
379 [ -n "$dryrun" -o -n "$nostash" ] || $tg tag -q -q --none-ok -m "$msg" --stash || die "requested --stash failed"
380 refwidth="$(git config --get --int core.abbrev 2>/dev/null)" || :
381 [ -n "$refwidth" ] || refwidth=7
382 [ $refwidth -ge 4 -a $refwidth -le 40 ] || refwidth=7
383 nullref="$(printf '%.*s' $refwidth "$nullsha")"
384 notewidth=$(( $refwidth + 4 + $refwidth ))
385 srh=
386 [ -n "$dryrun" ] || srh="$(git symbolic-ref --quiet HEAD)" || :
387 cut -d ' ' -f 3 <"$insn" | LC_ALL=C sort -u -b -k1,1 | join - "$trf" |
388 while read -r name rev; do
389 orig="$(git rev-parse --verify --quiet "$name" --)" || :
390 init_reflog "$name"
391 if [ "$rev" != "$orig" ]; then
392 [ -z "$dryrun" -a -n "$quiet" ] ||
393 origsh="$(git rev-parse --verify --short --quiet "$name" --)" || :
394 if [ -z "$dryrun" ]; then
395 if [ -n "$srh" ] && [ "$srh" = "$name" ]; then
396 [ -n "$quiet" ] || echo "Detaching HEAD to revert $name"
397 detachat="$orig"
398 [ -n "$detachat" ] || detachat="$(make_empty_commit)"
399 git update-ref -m "tgrevert: detach HEAD to revert $name" --no-deref HEAD "$detachat"
400 [ -n "$quiet" ] || git log -n 1 --format=format:'HEAD is now at %h... %s' HEAD
402 git update-ref -m "$msg" "$name" "$rev"
404 if [ -n "$dryrun" -o -z "$quiet" ]; then
405 revsh="$(git rev-parse --verify --short --quiet "$rev" --)" || :
406 if [ -n "$origsh" ]; then
407 hdr=' '
408 [ -z "$dryrun" ] || hdr='-'
409 printf '%s %s -> %s %s\n' "$hdr" "$origsh" "$revsh" "$name"
410 else
411 hdr='*'
412 [ -z "$dryrun" ] || hdr='-'
413 printf '%s %s -> %s %s\n' "$hdr" "$nullref" "$revsh" "$name"
416 else
417 : #[ -z "$dryrun" -a -n "$quiet" ] || printf "* %-*s %s\n" $notewidth "[no change]" "$name"
419 done
421 exit 0