topgit: version 0.19.13
[topgit/pro.git] / tg-revert.sh
blobbe42ed8a21759734455aea39c22cd98473a4a725
1 #!/bin/sh
2 # TopGit revert command
3 # Copyright (C) 2015,2021 Kyle J. McKay <mackyle@gmail.com>
4 # All rights reserved
5 # GPLv2
7 USAGE="\
8 Usage: ${tgname:-tg} [...] revert (-f | -i | -n) [<opt>...] (<tagname> | --stash) [<ref>...]
9 Or: ${tgname:-tg} [...] revert [<opt>...] (<tagname> | --stash) [(--[topgit-]heads] | <ref>...)]
10 Options:
11 --quiet / -q suppress non-dry-run ref change messages
12 --force / -f force revert operation to make changes
13 --dry-run / -n show '-f' changes but don't actually make them
14 --interactive / -i edit revert instruction sheet before proceeding
15 --list / -l list mode (default if no -f, -n or -i present)
16 --short[=<n>] display shortened hashes (default unless --hash)
17 --no-short display full hashes (default for --hash-only)
18 --hash-only show only hashes in list mode (aka '--hash')
19 --no-stash skip preliminary stash before making changes
20 --exclude <ref> exclude <ref> during operation (may be repeated)
21 --tgish-only exclude non-TopGit branches (aka '--tgish')
22 --no-deps do not include dependencies in revert mode
23 --deps include dependencies in list mode
24 --rdeps list relationships like 'tg summary --rdeps'
25 --stash specifies a <tagname> of refs/tgstash
26 --heads list only merge-base --independent heads
27 --heads-independent alternate spelling of '--heads'
28 --topgit-heads list only independent TopGit branch heads"
30 usage()
32 if [ "${1:-0}" != 0 ]; then
33 printf '%s\n' "$USAGE" >&2
34 else
35 printf '%s\n' "$USAGE"
37 exit ${1:-0}
40 ## Parse options
42 force=
43 interact=
44 dryrun=
45 list=
46 deps=
47 rdeps=
48 tgish=
49 nodeps=
50 nostash=
51 exclude=
52 quiet=
53 short=
54 hashonly=
55 headstopgit=
57 while [ $# -gt 0 ]; do case "$1" in
58 -h|--help)
59 usage
61 -q|--quiet)
62 quiet=1
64 -l|--list)
65 list=1
67 --short|--short=*|--no-short)
68 short="$1"
70 --hash|--hash-only)
71 hashonly=1
73 --deps|--deps-only)
74 deps=1
76 --rdeps)
77 rdeps=1
79 --tgish-only|--tgish)
80 tgish=1
82 -f|--force)
83 force=1
85 -i|--interactive)
86 interact=1
88 -n|--dry-run)
89 dryrun=1
91 --no-deps)
92 nodeps=1
94 --no-stash)
95 nostash=1
97 --exclude=*)
98 [ -n "${1#--exclude=}" ] || die "--exclude= requires a ref name"
99 case "${1#--exclude=}" in refs/*) rn="${1#--exclude=}";; *) rn="refs/heads/${1#--exclude=} refs/$topbases/${1#--exclude=}"; esac
100 exclude="$exclude $rn";;
101 --exclude)
102 shift
103 [ -n "$1" ] || die "--exclude requires a ref name"
104 case "$1" in refs/*) rn="$1";; *) rn="refs/heads/$1 refs/$topbases/$1"; esac
105 exclude="$exclude $rn";;
107 shift
108 break
110 --stash|--stash"@{"*"}")
111 break
113 -?*)
114 echo "Unknown option: $1" >&2
115 usage 1
118 break
120 esac; shift; done
121 [ -z "$exclude" ] || exclude="$exclude "
123 [ -z "$list$short$hashonly" ] || [ -z "$force$interact$dryrun$nodeps$nostash" ] || usage 1
124 [ -z "$force$interact$dryrun" ] || [ -z "$list$short$hashonly$deps$rdeps" ] || usage 1
125 [ -z "$deps" ] || [ -z "$rdeps" ] || usage 1
126 [ -n "$list$force$interact$dryrun" ] || list=1
127 [ -z "$list" ] || [ -n "$short" ] || if [ -n "$hashonly" ]; then short="--no-short"; else short="--short"; fi
128 [ -n "$1" ] || { echo "Tag name required" >&2; usage 1; }
129 tagname="$1"
130 shift
131 [ "$1" != "--heads-independent" ] || { shift; set -- --heads "$@"; }
132 if [ "$1" = "--topgit-heads" ]; then
133 shift
134 headstopgit=1
135 set -- "--heads" "$@"
137 [ -n "$list" ] || [ "$1" != "--heads" ] || usage 1
138 [ "$tagname" != "--stash" ] || tagname=refs/tgstash
139 case "$tagname" in --stash"@{"*"}")
140 strip="${tagname#--stash??}"
141 strip="${strip%?}"
142 tagname="refs/tgstash@{$strip}"
143 esac
144 refname="$tagname"
145 case "$refname" in HEAD|refs/*);;*)
146 _pfx="@{"
147 _refonly="${refname%%"$_pfx"*}"
148 suffix="${refname#"$_refonly"}"
149 refname="$_refonly"
152 reftest="$(git rev-parse --revs-only --symbolic-full-name "$refname" -- 2>/dev/null)" &&
153 [ -n "$reftest" ]
154 } ||
156 reftest="$(git rev-parse --revs-only --symbolic-full-name "refs/tags/$refname" -- 2>/dev/null)" &&
157 [ -n "$reftest" ]
159 then
160 refname="$reftest$suffix"
161 else
162 if hash="$(git rev-parse --quiet --verify "$refname$suffix")"; then
163 refname="$hash"
164 else
165 refname="refs/tags/$refname$suffix"
168 esac
169 reftype=tag
170 case "$refname" in refs/tags/*) tagname="${refname#refs/tags/}";; *) reftype=ref; tagname="$refname"; esac
171 git rev-parse --verify --quiet "$refname^{tag}" -- >/dev/null || die "not annotated/signed tag: $refname"
172 tgf="$(get_temp tag)"
173 trf="$(get_temp refs)"
174 tagdataref="$refname^{tag}"
175 while
176 git cat-file tag "$tagdataref" >"$tgf"t || die "cannot read tag: $refname"
177 sed -ne '/^-----BEGIN TOPGIT REFS-----$/,/^-----END TOPGIT REFS-----$/p' <"$tgf"t |
178 sed -ne "/^\\($octethl\\) \\(refs\/[^ $tab][^ $tab]*\\)\$/{s//\\2 \\1/;p;}" |
179 sed -e "s,^refs/$oldbases/,refs/$topbases/,g" |
180 sort -u -b -k1,1 >"$trf"
181 ! [ -s "$trf" ]
183 # If it's a tag of a tag, dereference it and try again
184 read -r field tagtype <<-EOT || break
185 $(sed -n '1,/^$/p' <"$tgf" | grep '^type [^ ][^ ]*$' || :)
187 [ "$tagtype" = "tag" ] || break
188 read -r field tagdataref <<-EOT || break
189 $(sed -n '1,/^$/p' <"$tgf" | grep '^object [^ ][^ ]*$' || :)
191 [ -n "$tagdataref" ] || break
192 tagdataref="$tagdataref^{tag}"
193 git rev-parse --verify --quiet "$tagdataref" -- >/dev/null || break
194 done
195 [ -s "$trf" ] || die "$reftype $tagname does not contain a TOPGIT REFS section"
196 rcnt=$(( $(wc -l <"$trf") ))
197 vcnt=$(( $(cut -d ' ' -f 2 <"$trf" | git cat-file $gcfbopt --batch-check='%(objectname)' | grep -v ' missing$' | wc -l) ))
198 [ "$rcnt" -eq "$vcnt" ] || die "$reftype $tagname contains $rcnt ref(s) but only $vcnt are still valid"
199 cat "$trf" >"$tg_ref_cache"
200 create_ref_dirs
201 tg_ref_cache_only=1
202 tg_read_only=1
204 get_recorded_ref()
206 printf '%s\n' "$1" | join - "$trf" | cut -d ' ' -f 2 || :
209 show_topgit_heads()
211 topics="$(get_temp topics)"
212 topics2="$(get_temp topics)"
213 deplist="$(get_temp deplist)"
214 <"$trf" >"$topics" \
215 sed -e '\,^refs/'"$topbasesrx"'/,!d' -e 's,^refs/'"$topbasesrx"'/,refs/heads/,' -e 's/ .*//'
216 while read -r oneref; do
217 _rev="$(get_recorded_ref "$oneref")" && [ -n "$_rev" ] || continue
218 printf '%s\n' "$oneref" >>"$topics2"
219 git cat-file blob "$_rev:.topdeps" 2>/dev/null | awk '{print}'
220 done <"$topics" | sed -e 's,^,refs/heads/,' | sort -u >"$deplist"
221 topics="$topics2"
222 join -v 1 "$topics" "$deplist"
225 show_indep_heads()
227 srt="$(get_temp sort)"
228 sort -b -k2,2 <"$trf" >"$srt"
229 git merge-base --independent $(cut -d ' ' -f 2 <"$srt") |
230 sort -b -k1,1 |
231 join -2 2 -o 2.1 - "$srt" |
232 sort
235 show_heads()
237 if [ -n "$headstopgit" ]; then
238 show_topgit_heads "$@"
239 else
240 show_indep_heads "$@"
244 [ $# -ne 0 ] || [ -z "$rdeps$deps" ] || { set -- --heads; headstopgit=1; }
245 [ $# -ne 1 ] || [ -z "$deps" ] || [ "$1" != "--heads" ] || { deps=; set --; }
246 if [ $# -eq 1 ] && [ "$1" = "--heads" ]; then
247 set -- $(show_heads)
250 is_tgish() {
251 case "$1" in
252 refs/"$topbases"/*)
253 ref_exists "refs/heads/${1#refs/$topbases/}"
255 refs/heads/*)
256 ref_exists "refs/$topbases/${1#refs/heads/}"
261 esac
264 refs=
265 for b; do
266 exp=
267 case "$b" in refs/*) exp=1; rn="$b";; *) rn="refs/heads/$b"; esac
268 ref_exists "$rn" || die "not present in tag data (try --list): $rn"
269 case " $refs " in *" $rn "*);;*)
270 refs="${refs:+$refs }$rn"
271 if [ -z "$list" ] && { [ -z "$nodeps" ] || [ -z "$exp" ]; } && is_tgish "$rn"; then
272 case "$rn" in
273 refs/"$topbases"/*)
274 refs="$refs refs/heads/${rn#refs/$topbases/}"
276 refs/heads/*)
277 refs="$refs refs/$topbases/${rn#refs/heads/}"
279 esac
281 esac
282 done
284 show_dep() {
285 case "$exclude" in *" refs/heads/$_dep "*) return; esac
286 case " $seen_deps " in *" $_dep "*) return 0; esac
287 seen_deps="${seen_deps:+$seen_deps }$_dep"
288 [ -z "$tgish" ] || [ -n "$_dep_is_tgish" ] || return 0
289 printf 'refs/heads/%s\n' "$_dep"
290 [ -z "$_dep_is_tgish" ] ||
291 printf 'refs/%s/%s\n' "$topbases" "$_dep"
294 show_deps()
296 no_remotes=1
297 recurse_deps_exclude=
298 while read _b && [ -n "$_b" ]; do
299 case "$exclude" in *" $_b "*) continue; esac
300 if ! is_tgish "$_b"; then
301 [ -z "$tgish" ] || continue
302 printf '%s\n' "$_b"
303 continue
305 case "$_b" in refs/"$topbases"/*) _b="refs/heads/${_b#refs/$topbases/}"; esac
306 _b="${_b#refs/heads/}"
307 case " $recurse_deps_exclude " in *" $_b "*) continue; esac
308 seen_deps=
309 _dep="$_b"; _dep_is_tgish=1; show_dep
310 recurse_deps show_dep "$_b"
311 recurse_deps_exclude="$recurse_deps_exclude $seen_deps"
312 done
315 show_rdep()
317 case "$exclude" in *" refs/heads/$_dep "*) return; esac
318 [ -z "$tgish" ] || [ -n "$_dep_is_tgish" ] || return 0
319 v_ref_exists_rev_short _depshort "refs/heads/$_dep" $short
320 if [ -n "$hashonly" ]; then
321 printf '%s %s\n' "$_depchain" "$_depshort"
322 else
323 printf '%s %s\n' "$_depchain" "${_depshort}~$_dep"
327 show_rdeps()
329 no_remotes=1
330 show_break=
331 seen_deps=
332 [ "$short" != "--short" ] || v_get_core_abbrev _dummy
333 while read _b && [ -n "$_b" ]; do
334 case "$exclude" in *" $_b "*) continue; esac
335 if ! is_tgish "$_b"; then
336 [ -z "$tgish" ] || continue
337 [ -z "$showbreak" ] || echo
338 showbreak=1
339 v_ref_exists_rev_short _bshort "refs/heads/$_b" $short
340 if [ -n "$hashonly" ]; then
341 printf '%s\n' "$_bshort"
342 else
343 printf '%s\n' "${_bshort}~$_b"
345 continue
347 case "$_b" in refs/"$topbases"/*) _b="refs/heads/${_b#refs/$topbases/}"; esac
348 _b="${_b#refs/heads/}"
349 case " $seen_deps " in *" $_b "*) continue; esac
350 seen_deps="$seen_deps $_b"
351 [ -z "$showbreak" ] || echo
352 showbreak=1
353 v_ref_exists_rev_short _bshort "refs/heads/$_b" $short
355 if [ -n "$hashonly" ]; then
356 printf '%s\n' "$_bshort"
357 else
358 printf '%s\n' "${_bshort}~$_b"
360 recurse_preorder=1
361 recurse_deps show_rdep "$_b"
362 } | sed -e 's/[^ ][^ ]*[ ]/ /g' -e 's/~/ /'
363 done
366 refslist() {
367 [ -z "$refs" ] || sed 'y/ /\n/' <<-EOT
368 $refs
372 if [ -n "$list" ]; then
373 if [ -z "$deps$rdeps" ]; then
374 # accelerate showing everything in full
375 if [ -z "$refs$exclude" ] && [ z"$short" = z"--no-short" ]; then
376 if [ -n "$hashonly" ]; then
377 <"$trf" sed -n 's/^[^ ][^ ]* \([^ ][^ ]*\)$/\1/p'
378 else
379 <"$trf" sed -n 's/^\([^ ][^ ]*\) \([^ ][^ ]*\)$/\2 \1/p'
381 exit
383 while read -r name rev; do
384 case "$exclude" in *" $name "*) continue; esac
385 [ -z "$refs" ] || case " $refs " in *" $name "*);;*) continue; esac
386 [ -z "$tgish" ] || is_tgish "$name" || continue
387 if [ -n "$hashonly" ]; then
388 printf '%s\n' "$(git rev-parse --verify --quiet $short "$rev" --)"
389 else
390 printf '%s %s\n' "$(git rev-parse --verify --quiet $short "$rev" --)" "$name"
392 done <"$trf"
393 exit 0
395 if [ -n "$deps" ]; then
396 refslist | show_deps | sort -u -b -k1,1 |
397 join - "$trf" |
398 while read -r name rev; do
399 if [ -n "$hashonly" ]; then
400 printf '%s\n' "$(git rev-parse --verify --quiet $short "$rev" --)"
401 else
402 printf '%s %s\n' "$(git rev-parse --verify --quiet $short "$rev" --)" "$name"
404 done
405 exit 0
407 refslist | show_rdeps
408 exit 0
410 insn="$(get_temp isns)"
412 get_short() {
413 [ -n "$interact" ] || { printf '%s' "$1"; return 0; }
414 git rev-parse --verify --quiet --short "$1" --
417 if [ -n "$nodeps" ] || [ -z "$refs" ]; then
418 while read -r name rev; do
419 case "$exclude" in *" $name "*) continue; esac
420 [ -z "$refs" ] || case " $refs " in *" $name "*);;*) continue; esac
421 [ -z "$tgish" ] || is_tgish "$name" || continue
422 printf 'revert %s %s\n' "$(get_short "$rev")" "$name"
423 done <"$trf" | sort -u -b -k3,3 >"$insn"
424 else
425 refslist | show_deps | sort -u -b -k1,1 |
426 join - "$trf" |
427 while read -r name rev; do
428 printf 'revert %s %s\n' "$(get_short "$rev")" "$name"
429 done >"$insn"
431 if [ -n "$interact" ]; then
432 count=$(( $(wc -l <"$insn") ))
433 cat <<EOT >>"$insn"
435 # Revert using $refname data ($count command(s))
437 # Commands:
438 # r, revert = revert ref to specified hash
440 # Note that changing the hash value shown here will have NO EFFECT.
442 # If you remove a line here THAT REVERT WILL BE SKIPPED.
444 # However, if you remove everything, the revert will be aborted.
446 run_editor "$insn" ||
447 die "there was a problem with the editor '$tg_editor'"
448 git -c core.commentchar='#' stripspace -s <"$insn" >"$insn"+
449 mv -f "$insn"+ "$insn"
450 [ -s "$insn" ] || die "nothing to do"
451 while read -r op hash ref; do
452 [ "$op" = "r" ] || [ "$op" = "revert" ] ||
453 die "invalid op in instruction: $op $hash $ref"
454 case "$ref" in refs/?*);;*)
455 die "invalid ref in instruction: $op $hash $ref"
456 esac
457 ref_exists "$ref" ||
458 die "unknown ref in instruction: $op $hash $ref"
459 done <"$insn"
461 msg="tgrevert: $reftype $tagname ($(( $(wc -l <"$insn") )) command(s))"
462 if [ -z "$dryrun" ]; then
463 if [ -z "$nostash" ]; then
464 tg tag -q -q --allow-any --none-ok -m "$msg" --stash || die "requested --stash failed"
465 else
466 tg tag --allow-any --none-ok --anonymous || die "anonymous --stash failed"
469 refwidth="$(git config --get --int core.abbrev 2>/dev/null)" || :
470 [ -n "$refwidth" ] || refwidth=7
471 [ $refwidth -ge 4 ] && [ $refwidth -le 40 ] || refwidth=7
472 nullref="$(printf '%.*s' $refwidth "$nullsha")"
473 notewidth=$(( $refwidth + 4 + $refwidth ))
474 srh=
475 [ -n "$dryrun" ] || srh="$(git symbolic-ref --quiet HEAD)" || :
476 cut -d ' ' -f 3 <"$insn" | sort -u -b -k1,1 | join - "$trf" |
477 while read -r name rev; do
478 orig="$(git rev-parse --verify --quiet "$name^{}" --)" || :
479 init_reflog "$name"
480 if [ "$rev" != "$orig" ]; then
481 [ -z "$dryrun" ] && [ -n "$quiet" ] ||
482 origsh="$(git rev-parse --verify --short --quiet "$name" --)" || :
483 if [ -z "$dryrun" ]; then
484 if [ -n "$srh" ] && [ "$srh" = "$name" ]; then
485 [ -n "$quiet" ] || warn "detaching HEAD to revert $name"
486 detachat="$orig"
487 [ -n "$detachat" ] || detachat="$(make_empty_commit)"
488 git update-ref -m "tgrevert: detach HEAD to revert $name" --no-deref HEAD "$detachat" || die "detach failed"
489 [ -n "$quiet" ] || warn "$(git --no-pager log -n 1 --format=format:'HEAD is now at %h... %s' HEAD)"
491 git update-ref -m "$msg" "$name" "$rev"
493 if [ -n "$dryrun" ] || [ -z "$quiet" ]; then
494 revsh="$(git rev-parse --verify --short --quiet "$rev" --)" || :
495 if [ -n "$origsh" ]; then
496 hdr=' '
497 [ -z "$dryrun" ] || hdr='-'
498 printf '%s %s -> %s %s\n' "$hdr" "$origsh" "$revsh" "$name"
499 else
500 hdr='*'
501 [ -z "$dryrun" ] || hdr='-'
502 printf '%s %s -> %s %s\n' "$hdr" "$nullref" "$revsh" "$name"
505 else
506 : #[ -z "$dryrun" ] && [ -n "$quiet" ] || printf "* %-*s %s\n" $notewidth "[no change]" "$name"
508 done
510 exit 0