tg-*.sh: make sure stripspace uses the correct comment char
[topgit/pro.git] / tg-revert.sh
blobf450187a6f857dd5fdde3c3cb95046ff67b3cd2d
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="\
8 Usage: ${tgname:-tg} [...] revert (-f | -i | -n) [-q] [--tgish-only] [--no-deps] [--no-stash] [--exclude <ref>...] (<tagname> | --stash) [<ref>...]
9 Or: ${tgname:-tg} [...] revert [-l] [--no-short] [--hash] [--tgish-only] [(--deps | --rdeps)] [--exclude <ref>...] (<tagname> | --stash) [(--[topgit-]heads] | <ref>...)]"
11 usage()
13 if [ "${1:-0}" != 0 ]; then
14 printf '%s\n' "$USAGE" >&2
15 else
16 printf '%s\n' "$USAGE"
18 exit ${1:-0}
21 ## Parse options
23 force=
24 interact=
25 dryrun=
26 list=
27 deps=
28 rdeps=
29 tgish=
30 nodeps=
31 nostash=
32 exclude=
33 quiet=
34 short=
35 hashonly=
36 headstopgit=
38 while [ $# -gt 0 ]; do case "$1" in
39 -h|--help)
40 usage
42 -q|--quiet)
43 quiet=1
45 -l|--list)
46 list=1
48 --short|--short=*|--no-short)
49 short="$1"
51 --hash|--hash-only)
52 hashonly=1
54 --deps|--deps-only)
55 deps=1
57 --rdeps)
58 rdeps=1
60 --tgish-only|--tgish)
61 tgish=1
63 -f|--force)
64 force=1
66 -i|--interactive)
67 interact=1
69 -n|--dry-run)
70 dryrun=1
72 --no-deps)
73 nodeps=1
75 --no-stash)
76 nostash=1
78 --exclude=*)
79 [ -n "${1#--exclude=}" ] || die "--exclude= requires a ref name"
80 case "${1#--exclude=}" in refs/*) rn="${1#--exclude=}";; *) rn="refs/heads/${1#--exclude=} refs/$topbases/${1#--exclude=}"; esac
81 exclude="$exclude $rn";;
82 --exclude)
83 shift
84 [ -n "$1" ] || die "--exclude requires a ref name"
85 case "$1" in refs/*) rn="$1";; *) rn="refs/heads/$1 refs/$topbases/$1"; esac
86 exclude="$exclude $rn";;
87 --)
88 shift
89 break
91 --stash|--stash"@{"*"}")
92 break
94 -?*)
95 echo "Unknown option: $1" >&2
96 usage 1
99 break
101 esac; shift; done
102 [ -z "$exclude" ] || exclude="$exclude "
104 [ -z "$list$short$hashonly" ] || [ -z "$force$interact$dryrun$nodeps$nostash" ] || usage 1
105 [ -z "$force$interact$dryrun" ] || [ -z "$list$short$hashonly$deps$rdeps" ] || usage 1
106 [ -z "$deps" ] || [ -z "$rdeps" ] || usage 1
107 [ -n "$list$force$interact$dryrun" ] || list=1
108 [ -z "$list" ] || [ -n "$short" ] || if [ -n "$hashonly" ]; then short="--no-short"; else short="--short"; fi
109 [ -n "$1" ] || { echo "Tag name required" >&2; usage 1; }
110 tagname="$1"
111 shift
112 [ "$1" != "--heads-independent" ] || { shift; set -- --heads "$@"; }
113 if [ "$1" = "--topgit-heads" ]; then
114 shift
115 headstopgit=1
116 set -- "--heads" "$@"
118 [ -n "$list" ] || [ "$1" != "--heads" ] || usage 1
119 [ "$tagname" != "--stash" ] || tagname=refs/tgstash
120 case "$tagname" in --stash"@{"*"}")
121 strip="${tagname#--stash??}"
122 strip="${strip%?}"
123 tagname="refs/tgstash@{$strip}"
124 esac
125 refname="$tagname"
126 case "$refname" in HEAD|refs/*);;*)
127 _pfx="@{"
128 _refonly="${refname%%"$_pfx"*}"
129 suffix="${refname#"$_refonly"}"
130 refname="$_refonly"
133 reftest="$(git rev-parse --revs-only --symbolic-full-name "$refname" -- 2>/dev/null)" &&
134 [ -n "$reftest" ]
135 } ||
137 reftest="$(git rev-parse --revs-only --symbolic-full-name "refs/tags/$refname" -- 2>/dev/null)" &&
138 [ -n "$reftest" ]
140 then
141 refname="$reftest$suffix"
142 else
143 if hash="$(git rev-parse --quiet --verify "$refname$suffix")"; then
144 refname="$hash"
145 else
146 refname="refs/tags/$refname$suffix"
149 esac
150 reftype=tag
151 case "$refname" in refs/tags/*) tagname="${refname#refs/tags/}";; *) reftype=ref; tagname="$refname"; esac
152 git rev-parse --verify --quiet "$refname^{tag}" -- >/dev/null || die "not annotated/signed tag: $refname"
153 tgf="$(get_temp tag)"
154 trf="$(get_temp refs)"
155 tagdataref="$refname^{tag}"
156 while
157 git cat-file tag "$tagdataref" >"$tgf"t || die "cannot read tag: $refname"
158 sed -ne '/^-----BEGIN TOPGIT REFS-----$/,/^-----END TOPGIT REFS-----$/p' <"$tgf"t |
159 sed -ne "/^\\($octethl\\) \\(refs\/[^ $tab][^ $tab]*\\)\$/{s//\\2 \\1/;p;}" |
160 sed -e "s,^refs/$oldbases/,refs/$topbases/,g" |
161 sort -u -b -k1,1 >"$trf"
162 ! [ -s "$trf" ]
164 # If it's a tag of a tag, dereference it and try again
165 read -r field tagtype <<-EOT || break
166 $(sed -n '1,/^$/p' <"$tgf" | grep '^type [^ ][^ ]*$' || :)
168 [ "$tagtype" = "tag" ] || break
169 read -r field tagdataref <<-EOT || break
170 $(sed -n '1,/^$/p' <"$tgf" | grep '^object [^ ][^ ]*$' || :)
172 [ -n "$tagdataref" ] || break
173 tagdataref="$tagdataref^{tag}"
174 git rev-parse --verify --quiet "$tagdataref" -- >/dev/null || break
175 done
176 [ -s "$trf" ] || die "$reftype $tagname does not contain a TOPGIT REFS section"
177 rcnt=$(( $(wc -l <"$trf") ))
178 vcnt=$(( $(cut -d ' ' -f 2 <"$trf" | git cat-file $gcfbopt --batch-check='%(objectname)' | grep -v ' missing$' | wc -l) ))
179 [ "$rcnt" -eq "$vcnt" ] || die "$reftype $tagname contains $rcnt ref(s) but only $vcnt are still valid"
180 cat "$trf" >"$tg_ref_cache"
181 create_ref_dirs
182 tg_ref_cache_only=1
183 tg_read_only=1
185 get_recorded_ref()
187 printf '%s\n' "$1" | join - "$trf" | cut -d ' ' -f 2 || :
190 show_topgit_heads()
192 topics="$(get_temp topics)"
193 topics2="$(get_temp topics)"
194 deplist="$(get_temp deplist)"
195 <"$trf" >"$topics" \
196 sed -e '\,^refs/'"$topbasesrx"'/,!d' -e 's,^refs/'"$topbasesrx"'/,refs/heads/,' -e 's/ .*//'
197 while read -r oneref; do
198 _rev="$(get_recorded_ref "$oneref")" && [ -n "$_rev" ] || continue
199 printf '%s\n' "$oneref" >>"$topics2"
200 git cat-file blob "$_rev:.topdeps" 2>/dev/null | awk '{print}'
201 done <"$topics" | sed -e 's,^,refs/heads/,' | sort -u >"$deplist"
202 topics="$topics2"
203 join -v 1 "$topics" "$deplist"
206 show_indep_heads()
208 srt="$(get_temp sort)"
209 sort -b -k2,2 <"$trf" >"$srt"
210 git merge-base --independent $(cut -d ' ' -f 2 <"$srt") |
211 sort -b -k1,1 |
212 join -2 2 -o 2.1 - "$srt" |
213 sort
216 show_heads()
218 if [ -n "$headstopgit" ]; then
219 show_topgit_heads "$@"
220 else
221 show_indep_heads "$@"
225 [ $# -ne 0 ] || [ -z "$rdeps$deps" ] || { set -- --heads; headstopgit=1; }
226 [ $# -ne 1 ] || [ -z "$deps" ] || [ "$1" != "--heads" ] || { deps=; set --; }
227 if [ $# -eq 1 ] && [ "$1" = "--heads" ]; then
228 set -- $(show_heads)
231 is_tgish() {
232 case "$1" in
233 refs/"$topbases"/*)
234 ref_exists "refs/heads/${1#refs/$topbases/}"
236 refs/heads/*)
237 ref_exists "refs/$topbases/${1#refs/heads/}"
242 esac
245 refs=
246 for b; do
247 exp=
248 case "$b" in refs/*) exp=1; rn="$b";; *) rn="refs/heads/$b"; esac
249 ref_exists "$rn" || die "not present in tag data (try --list): $rn"
250 case " $refs " in *" $rn "*);;*)
251 refs="${refs:+$refs }$rn"
252 if [ -z "$list" ] && { [ -z "$nodeps" ] || [ -z "$exp" ]; } && is_tgish "$rn"; then
253 case "$rn" in
254 refs/"$topbases"/*)
255 refs="$refs refs/heads/${rn#refs/$topbases/}"
257 refs/heads/*)
258 refs="$refs refs/$topbases/${rn#refs/heads/}"
260 esac
262 esac
263 done
265 show_dep() {
266 case "$exclude" in *" refs/heads/$_dep "*) return; esac
267 case " $seen_deps " in *" $_dep "*) return 0; esac
268 seen_deps="${seen_deps:+$seen_deps }$_dep"
269 [ -z "$tgish" ] || [ -n "$_dep_is_tgish" ] || return 0
270 printf 'refs/heads/%s\n' "$_dep"
271 [ -z "$_dep_is_tgish" ] ||
272 printf 'refs/%s/%s\n' "$topbases" "$_dep"
275 show_deps()
277 no_remotes=1
278 recurse_deps_exclude=
279 while read _b && [ -n "$_b" ]; do
280 case "$exclude" in *" $_b "*) continue; esac
281 if ! is_tgish "$_b"; then
282 [ -z "$tgish" ] || continue
283 printf '%s\n' "$_b"
284 continue
286 case "$_b" in refs/"$topbases"/*) _b="refs/heads/${_b#refs/$topbases/}"; esac
287 _b="${_b#refs/heads/}"
288 case " $recurse_deps_exclude " in *" $_b "*) continue; esac
289 seen_deps=
290 _dep="$_b"; _dep_is_tgish=1; show_dep
291 recurse_deps show_dep "$_b"
292 recurse_deps_exclude="$recurse_deps_exclude $seen_deps"
293 done
296 show_rdep()
298 case "$exclude" in *" refs/heads/$_dep "*) return; esac
299 [ -z "$tgish" ] || [ -n "$_dep_is_tgish" ] || return 0
300 v_ref_exists_rev_short _depshort "refs/heads/$_dep" $short
301 if [ -n "$hashonly" ]; then
302 printf '%s %s\n' "$_depchain" "$_depshort"
303 else
304 printf '%s %s\n' "$_depchain" "${_depshort}~$_dep"
308 show_rdeps()
310 no_remotes=1
311 show_break=
312 seen_deps=
313 [ "$short" != "--short" ] || v_get_core_abbrev _dummy
314 while read _b && [ -n "$_b" ]; do
315 case "$exclude" in *" $_b "*) continue; esac
316 if ! is_tgish "$_b"; then
317 [ -z "$tgish" ] || continue
318 [ -z "$showbreak" ] || echo
319 showbreak=1
320 v_ref_exists_rev_short _bshort "refs/heads/$_b" $short
321 if [ -n "$hashonly" ]; then
322 printf '%s\n' "$_bshort"
323 else
324 printf '%s\n' "${_bshort}~$_b"
326 continue
328 case "$_b" in refs/"$topbases"/*) _b="refs/heads/${_b#refs/$topbases/}"; esac
329 _b="${_b#refs/heads/}"
330 case " $seen_deps " in *" $_b "*) continue; esac
331 seen_deps="$seen_deps $_b"
332 [ -z "$showbreak" ] || echo
333 showbreak=1
334 v_ref_exists_rev_short _bshort "refs/heads/$_b" $short
336 if [ -n "$hashonly" ]; then
337 printf '%s\n' "$_bshort"
338 else
339 printf '%s\n' "${_bshort}~$_b"
341 recurse_preorder=1
342 recurse_deps show_rdep "$_b"
343 } | sed -e 's/[^ ][^ ]*[ ]/ /g' -e 's/~/ /'
344 done
347 refslist() {
348 [ -z "$refs" ] || sed 'y/ /\n/' <<-EOT
349 $refs
353 if [ -n "$list" ]; then
354 if [ -z "$deps$rdeps" ]; then
355 # accelerate showing everything in full
356 if [ -z "$refs$exclude" ] && [ z"$short" = z"--no-short" ]; then
357 if [ -n "$hashonly" ]; then
358 <"$trf" sed -n 's/^[^ ][^ ]* \([^ ][^ ]*\)$/\1/p'
359 else
360 <"$trf" sed -n 's/^\([^ ][^ ]*\) \([^ ][^ ]*\)$/\2 \1/p'
362 exit
364 while read -r name rev; do
365 case "$exclude" in *" $name "*) continue; esac
366 [ -z "$refs" ] || case " $refs " in *" $name "*);;*) continue; esac
367 [ -z "$tgish" ] || is_tgish "$name" || continue
368 if [ -n "$hashonly" ]; then
369 printf '%s\n' "$(git rev-parse --verify --quiet $short "$rev" --)"
370 else
371 printf '%s %s\n' "$(git rev-parse --verify --quiet $short "$rev" --)" "$name"
373 done <"$trf"
374 exit 0
376 if [ -n "$deps" ]; then
377 refslist | show_deps | sort -u -b -k1,1 |
378 join - "$trf" |
379 while read -r name rev; do
380 if [ -n "$hashonly" ]; then
381 printf '%s\n' "$(git rev-parse --verify --quiet $short "$rev" --)"
382 else
383 printf '%s %s\n' "$(git rev-parse --verify --quiet $short "$rev" --)" "$name"
385 done
386 exit 0
388 refslist | show_rdeps
389 exit 0
391 insn="$(get_temp isns)"
393 get_short() {
394 [ -n "$interact" ] || { printf '%s' "$1"; return 0; }
395 git rev-parse --verify --quiet --short "$1" --
398 if [ -n "$nodeps" ] || [ -z "$refs" ]; then
399 while read -r name rev; do
400 case "$exclude" in *" $name "*) continue; esac
401 [ -z "$refs" ] || case " $refs " in *" $name "*);;*) continue; esac
402 [ -z "$tgish" ] || is_tgish "$name" || continue
403 printf 'revert %s %s\n' "$(get_short "$rev")" "$name"
404 done <"$trf" | sort -u -b -k3,3 >"$insn"
405 else
406 refslist | show_deps | sort -u -b -k1,1 |
407 join - "$trf" |
408 while read -r name rev; do
409 printf 'revert %s %s\n' "$(get_short "$rev")" "$name"
410 done >"$insn"
412 if [ -n "$interact" ]; then
413 count=$(( $(wc -l <"$insn") ))
414 cat <<EOT >>"$insn"
416 # Revert using $refname data ($count command(s))
418 # Commands:
419 # r, revert = revert ref to specified hash
421 # Note that changing the hash value shown here will have NO EFFECT.
423 # If you remove a line here THAT REVERT WILL BE SKIPPED.
425 # However, if you remove everything, the revert will be aborted.
427 run_editor "$insn" ||
428 die "there was a problem with the editor '$tg_editor'"
429 git -c core.commentchar='#' stripspace -s <"$insn" >"$insn"+
430 mv -f "$insn"+ "$insn"
431 [ -s "$insn" ] || die "nothing to do"
432 while read -r op hash ref; do
433 [ "$op" = "r" ] || [ "$op" = "revert" ] ||
434 die "invalid op in instruction: $op $hash $ref"
435 case "$ref" in refs/?*);;*)
436 die "invalid ref in instruction: $op $hash $ref"
437 esac
438 ref_exists "$ref" ||
439 die "unknown ref in instruction: $op $hash $ref"
440 done <"$insn"
442 msg="tgrevert: $reftype $tagname ($(( $(wc -l <"$insn") )) command(s))"
443 if [ -z "$dryrun" ]; then
444 if [ -z "$nostash" ]; then
445 tg tag -q -q --allow-any --none-ok -m "$msg" --stash || die "requested --stash failed"
446 else
447 tg tag --allow-any --none-ok --anonymous || die "anonymous --stash failed"
450 refwidth="$(git config --get --int core.abbrev 2>/dev/null)" || :
451 [ -n "$refwidth" ] || refwidth=7
452 [ $refwidth -ge 4 ] && [ $refwidth -le 40 ] || refwidth=7
453 nullref="$(printf '%.*s' $refwidth "$nullsha")"
454 notewidth=$(( $refwidth + 4 + $refwidth ))
455 srh=
456 [ -n "$dryrun" ] || srh="$(git symbolic-ref --quiet HEAD)" || :
457 cut -d ' ' -f 3 <"$insn" | sort -u -b -k1,1 | join - "$trf" |
458 while read -r name rev; do
459 orig="$(git rev-parse --verify --quiet "$name^{}" --)" || :
460 init_reflog "$name"
461 if [ "$rev" != "$orig" ]; then
462 [ -z "$dryrun" ] && [ -n "$quiet" ] ||
463 origsh="$(git rev-parse --verify --short --quiet "$name" --)" || :
464 if [ -z "$dryrun" ]; then
465 if [ -n "$srh" ] && [ "$srh" = "$name" ]; then
466 [ -n "$quiet" ] || warn "detaching HEAD to revert $name"
467 detachat="$orig"
468 [ -n "$detachat" ] || detachat="$(make_empty_commit)"
469 git update-ref -m "tgrevert: detach HEAD to revert $name" --no-deref HEAD "$detachat" || die "detach failed"
470 [ -n "$quiet" ] || warn "$(git --no-pager log -n 1 --format=format:'HEAD is now at %h... %s' HEAD)"
472 git update-ref -m "$msg" "$name" "$rev"
474 if [ -n "$dryrun" ] || [ -z "$quiet" ]; then
475 revsh="$(git rev-parse --verify --short --quiet "$rev" --)" || :
476 if [ -n "$origsh" ]; then
477 hdr=' '
478 [ -z "$dryrun" ] || hdr='-'
479 printf '%s %s -> %s %s\n' "$hdr" "$origsh" "$revsh" "$name"
480 else
481 hdr='*'
482 [ -z "$dryrun" ] || hdr='-'
483 printf '%s %s -> %s %s\n' "$hdr" "$nullref" "$revsh" "$name"
486 else
487 : #[ -z "$dryrun" ] && [ -n "$quiet" ] || printf "* %-*s %s\n" $notewidth "[no change]" "$name"
489 done
491 exit 0