tg.sh: use --no-deref when returning to a detached HEAD
[topgit/pro.git] / tg-revert.sh
blobc0f707aeddd4478d089fd9b872a5f7d8a912ac12
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) [(--[topgit-]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=
35 headstopgit=
37 while [ $# -gt 0 ]; do case "$1" in
38 -h|--help)
39 usage
41 -q|--quiet)
42 quiet=1
44 -l|--list)
45 list=1
47 --short|--short=*|--no-short)
48 short="$1"
50 --hash|--hash-only)
51 hashonly=1
53 --deps|--deps-only)
54 deps=1
56 --rdeps)
57 rdeps=1
59 --tgish-only)
60 tgish=1
62 -f|--force)
63 force=1
65 -i|--interactive)
66 interact=1
68 -n|--dry-run)
69 dryrun=1
71 --no-deps)
72 nodeps=1
74 --no-stash)
75 nostash=1
77 --exclude=*)
78 [ -n "${1#--exclude=}" ] || die "--exclude= requires a ref name"
79 case "${1#--exclude=}" in refs/*) rn="${1#--exclude=}";; *) rn="refs/heads/${1#--exclude=} refs/$topbases/${1#--exclude=}"; esac
80 exclude="$exclude $rn";;
81 --exclude)
82 shift
83 [ -n "$1" ] || die "--exclude requires a ref name"
84 case "$1" in refs/*) rn="$1";; *) rn="refs/heads/$1 refs/$topbases/$1"; esac
85 exclude="$exclude $rn";;
86 --)
87 shift
88 break
90 --stash|--stash"@{"*"}")
91 break
93 -?*)
94 echo "Unknown option: $1" >&2
95 usage 1
98 break
100 esac; shift; done
101 [ -z "$exclude" ] || exclude="$exclude "
103 [ -z "$list$short$hashonly" -o -z "$force$interact$dryrun$nodeps$nostash" ] || usage 1
104 [ -z "$force$interact$dryrun" -o -z "$list$short$hashonly$deps$rdeps" ] || usage 1
105 [ -z "$deps" -o -z "$rdeps" ] || usage 1
106 [ -n "$list$force$interact$dryrun" ] || list=1
107 [ -z "$list" -o -n "$short" ] || if [ -n "$hashonly" ]; then short="--no-short"; else short="--short"; fi
108 [ -n "$1" ] || { echo "Tag name required" >&2; usage 1; }
109 tagname="$1"
110 shift
111 [ "$1" != "--heads-independent" ] || { shift; set -- --heads "$@"; }
112 if [ "$1" = "--topgit-heads" ]; then
113 shift
114 headstopgit=1
115 set -- "--heads" "$@"
117 [ -n "$list" -o "$1" != "--heads" ] || usage 1
118 [ "$tagname" != "--stash" ] || tagname=refs/tgstash
119 case "$tagname" in --stash"@{"*"}")
120 strip="${tagname#--stash??}"
121 strip="${strip%?}"
122 tagname="refs/tgstash@{$strip}"
123 esac
124 refname="$tagname"
125 case "$refname" in HEAD|refs/*);;*)
126 suffix="${refname%@*}"
127 suffix="${refname#$suffix}"
128 refname="${refname%$suffix}"
129 if reftest="$(git rev-parse --revs-only --symbolic-full-name "$refname" -- 2>/dev/null)" &&
130 [ -n "$reftest" ]; then
131 refname="$reftest$suffix"
132 else
133 if hash="$(git rev-parse --quiet --verify "$refname$suffix")"; then
134 refname="$hash"
135 else
136 refname="refs/tags/$refname$suffix"
139 esac
140 reftype=tag
141 case "$refname" in refs/tags/*) tagname="${refname#refs/tags/}";; *) reftype=ref; tagname="$refname"; esac
142 git rev-parse --verify --quiet "$refname^{tag}" -- >/dev/null || die "not annotated/signed tag: $refname"
143 tgf="$(get_temp tag)"
144 trf="$(get_temp refs)"
145 tagdataref="$refname^{tag}"
146 while
147 git cat-file tag "$tagdataref" >"$tgf" || die "cannot read tag: $refname"
148 sed -ne '/^-----BEGIN TOPGIT REFS-----$/,/^-----END TOPGIT REFS-----$/p' <"$tgf" |
149 sed -ne "/^\\($octet20\\) \\(refs\/[^ $tab][^ $tab]*\\)\$/{s//\\2 \\1/;p;}" |
150 sed -e "s,^refs/$oldbases/,refs/$topbases/,g" |
151 sort -u -b -k1,1 >"$trf"
152 ! [ -s "$trf" ]
154 # If it's a tag of a tag, dereference it and try again
155 read -r field tagtype <<-EOT || break
156 $(sed -n '1,/^$/p' <"$tgf" | grep '^type [^ ][^ ]*$' || :)
158 [ "$tagtype" = "tag" ] || break
159 read -r field tagdataref <<-EOT || break
160 $(sed -n '1,/^$/p' <"$tgf" | grep '^object [^ ][^ ]*$' || :)
162 [ -n "$tagdataref" ] || break
163 tagdataref="$tagdataref^{tag}"
164 git rev-parse --verify --quiet "$tagdataref" -- >/dev/null || break
165 done
166 [ -s "$trf" ] || die "$reftype $tagname does not contain a TOPGIT REFS section"
167 rcnt=$(( $(wc -l <"$trf") ))
168 vcnt=$(( $(cut -d ' ' -f 2 <"$trf" | git cat-file --batch-check='%(objectname)' | grep -v ' missing$' | wc -l) ))
169 [ "$rcnt" -eq "$vcnt" ] || die "$reftime $tagname contains $rcnt ref(s) but only $vcnt are still valid"
170 cat "$trf" >"$tg_ref_cache"
171 create_ref_dirs
172 tg_ref_cache_only=1
173 tg_read_only=1
175 get_recorded_ref()
177 printf '%s\n' "$1" | join - "$trf" | cut -d ' ' -f 2 || :
180 show_topgit_heads()
182 topics="$(get_temp topics)"
183 topics2="$(get_temp topics)"
184 deplist="$(get_temp deplist)"
185 <"$trf" >"$topics" \
186 sed -e '\,^refs/'"$topbasesrx"'/,!d' -e 's,^refs/'"$topbasesrx"'/,refs/heads/,' -e 's/ .*//'
187 while read -r oneref; do
188 _rev="$(get_recorded_ref "$oneref")" && [ -n "$_rev" ] || continue
189 printf '%s\n' "$oneref" >>"$topics2"
190 git cat-file blob "$_rev:.topdeps" 2>/dev/null || :
191 done <"$topics" | sed -e 's,^,refs/heads/,' | sort -u >"$deplist"
192 topics="$topics2"
193 join -v 1 "$topics" "$deplist"
196 show_indep_heads()
198 srt="$(get_temp sort)"
199 sort -b -k2,2 <"$trf" >"$srt"
200 git merge-base --independent $(cut -d ' ' -f 2 <"$srt") |
201 sort -b -k1,1 |
202 join -2 2 -o 2.1 - "$srt" |
203 sort
206 show_heads()
208 if [ -n "$headstopgit" ]; then
209 show_topgit_heads "$@"
210 else
211 show_indep_heads "$@"
215 [ $# -ne 0 -o -z "$rdeps$deps" ] || { set -- --heads; headstopgit=1; }
216 [ $# -ne 1 -o -z "$deps" -o "$1" != "--heads" ] || { deps=; set --; }
217 if [ $# -eq 1 -a "$1" = "--heads" ]; then
218 set -- $(show_heads)
221 is_tgish() {
222 case "$1" in
223 refs/"$topbases"/*)
224 ref_exists "refs/heads/${1#refs/$topbases/}"
226 refs/heads/*)
227 ref_exists "refs/$topbases/${1#refs/heads/}"
232 esac
235 refs=
236 for b; do
237 exp=
238 case "$b" in refs/*) exp=1; rn="$b";; *) rn="refs/heads/$b"; esac
239 ref_exists "$rn" || die "not present in tag data (try --list): $rn"
240 case " $refs " in *" $rn "*);;*)
241 refs="${refs:+$refs }$rn"
242 if [ -z "$list" ] && [ -z "$nodeps" -o -z "$exp" ] && is_tgish "$rn"; then
243 case "$rn" in
244 refs/"$topbases"/*)
245 refs="$refs refs/heads/${rn#refs/$topbases/}"
247 refs/heads/*)
248 refs="$refs refs/$topbases/${rn#refs/heads/}"
250 esac
252 esac
253 done
255 show_dep() {
256 case "$exclude" in *" refs/heads/$_dep "*) return; esac
257 case " $seen_deps " in *" $_dep "*) return 0; esac
258 seen_deps="${seen_deps:+$seen_deps }$_dep"
259 [ -z "$tgish" -o -n "$_dep_is_tgish" ] || return 0
260 printf 'refs/heads/%s\n' "$_dep"
261 [ -z "$_dep_is_tgish" ] ||
262 printf 'refs/%s/%s\n' "$topbases" "$_dep"
265 show_deps()
267 no_remotes=1
268 recurse_deps_exclude=
269 while read _b && [ -n "$_b" ]; do
270 case "$exclude" in *" $_b "*) continue; esac
271 if ! is_tgish "$_b"; then
272 [ -z "$tgish" ] || continue
273 printf '%s\n' "$_b"
274 continue
276 case "$_b" in refs/"$topbases"/*) _b="refs/heads/${_b#refs/$topbases/}"; esac
277 _b="${_b#refs/heads/}"
278 case " $recurse_deps_exclude " in *" $_b "*) continue; esac
279 seen_deps=
280 _dep="$_b"; _dep_is_tgish=1; show_dep
281 recurse_deps show_dep "$_b"
282 recurse_deps_exclude="$recurse_deps_exclude $seen_deps"
283 done
286 show_rdep()
288 case "$exclude" in *" refs/heads/$_dep "*) return; esac
289 [ -z "$tgish" -o -n "$_dep_is_tgish" ] || return 0
290 if [ -n "$hashonly" ]; then
291 printf '%s %s\n' "$_depchain" "$(ref_exists_rev_short "refs/heads/$_dep" $short)"
292 else
293 printf '%s %s\n' "$_depchain" "$(ref_exists_rev_short "refs/heads/$_dep" $short)~$_dep"
297 show_rdeps()
299 no_remotes=1
300 show_break=
301 seen_deps=
302 while read _b && [ -n "$_b" ]; do
303 case "$exclude" in *" $_b "*) continue; esac
304 if ! is_tgish "$_b"; then
305 [ -z "$tgish" ] || continue
306 [ -z "$showbreak" ] || echo
307 showbreak=1
308 if [ -n "$hashonly" ]; then
309 printf '%s\n' "$(ref_exists_rev_short "refs/heads/$_b" $short)"
310 else
311 printf '%s\n' "$(ref_exists_rev_short "refs/heads/$_b" $short)~$_b"
313 continue
315 case "$_b" in refs/"$topbases"/*) _b="refs/heads/${_b#refs/$topbases/}"; esac
316 _b="${_b#refs/heads/}"
317 case " $seen_deps " in *" $_b "*) continue; esac
318 seen_deps="$seen_deps $_b"
319 [ -z "$showbreak" ] || echo
320 showbreak=1
322 if [ -n "$hashonly" ]; then
323 printf '%s\n' "$(ref_exists_rev_short "refs/heads/$_b" $short)"
324 else
325 printf '%s\n' "$(ref_exists_rev_short "refs/heads/$_b" $short)~$_b"
327 recurse_preorder=1
328 recurse_deps show_rdep "$_b"
329 } | sed -e 's/[^ ][^ ]*[ ]/ /g' -e 's/~/ /'
330 done
333 refslist() {
334 [ -z "$refs" ] || sed 'y/ /\n/' <<-EOT
335 $refs
339 if [ -n "$list" ]; then
340 if [ -z "$deps$rdeps" ]; then
341 while read -r name rev; do
342 case "$exclude" in *" $name "*) continue; esac
343 [ -z "$refs" ] || case " $refs " in *" $name "*);;*) continue; esac
344 [ -z "$tgish" ] || is_tgish "$name" || continue
345 if [ -n "$hashonly" ]; then
346 printf '%s\n' "$(git rev-parse --verify --quiet $short "$rev" --)"
347 else
348 printf '%s %s\n' "$(git rev-parse --verify --quiet $short "$rev" --)" "$name"
350 done <"$trf"
351 exit 0
353 if [ -n "$deps" ]; then
354 refslist | show_deps | sort -u -b -k1,1 |
355 join - "$trf" |
356 while read -r name rev; do
357 if [ -n "$hashonly" ]; then
358 printf '%s\n' "$(git rev-parse --verify --quiet $short "$rev" --)"
359 else
360 printf '%s %s\n' "$(git rev-parse --verify --quiet $short "$rev" --)" "$name"
362 done
363 exit 0
365 refslist | show_rdeps
366 exit 0
368 insn="$(get_temp isns)"
370 get_short() {
371 [ -n "$interact" ] || { printf '%s' "$1"; return 0; }
372 git rev-parse --verify --quiet --short "$1" --
375 if [ -n "$nodeps" -o -z "$refs" ]; then
376 while read -r name rev; do
377 case "$exclude" in *" $name "*) continue; esac
378 [ -z "$refs" ] || case " $refs " in *" $name "*);;*) continue; esac
379 [ -z "$tgish" ] || is_tgish "$name" || continue
380 printf 'revert %s %s\n' "$(get_short "$rev")" "$name"
381 done <"$trf" | sort -u -b -k3,3 >"$insn"
382 else
383 refslist | show_deps | sort -u -b -k1,1 |
384 join - "$trf" |
385 while read -r name rev; do
386 printf 'revert %s %s\n' "$(get_short "$rev")" "$name"
387 done >"$insn"
389 if [ -n "$interact" ]; then
390 count=$(( $(wc -l <"$insn") ))
391 cat <<EOT >>"$insn"
393 # Revert using $refname data ($count command(s))
395 # Commands:
396 # r, revert = revert ref to specified hash
398 # Note that changing the hash value shown here will have NO EFFECT.
400 # If you remove a line here THAT REVERT WILL BE SKIPPED.
402 # However, if you remove everything, the revert will be aborted.
404 run_editor "$insn" ||
405 die "there was a problem with the editor '$tg_editor'"
406 git stripspace -s <"$insn" >"$insn"+
407 mv -f "$insn"+ "$insn"
408 [ -s "$insn" ] || die "nothing to do"
409 while read -r op hash ref; do
410 [ "$op" = "r" -o "$op" = "revert" ] ||
411 die "invalid op in instruction: $op $hash $ref"
412 case "$ref" in refs/?*);;*)
413 die "invalid ref in instruction: $op $hash $ref"
414 esac
415 ref_exists "$ref" ||
416 die "unknown ref in instruction: $op $hash $ref"
417 done <"$insn"
419 msg="tgrevert: $reftype $tagname ($(( $(wc -l <"$insn") )) command(s))"
420 [ -n "$dryrun" -o -n "$nostash" ] || tg tag -q -q --none-ok -m "$msg" --stash || die "requested --stash failed"
421 refwidth="$(git config --get --int core.abbrev 2>/dev/null)" || :
422 [ -n "$refwidth" ] || refwidth=7
423 [ $refwidth -ge 4 -a $refwidth -le 40 ] || refwidth=7
424 nullref="$(printf '%.*s' $refwidth "$nullsha")"
425 notewidth=$(( $refwidth + 4 + $refwidth ))
426 srh=
427 [ -n "$dryrun" ] || srh="$(git symbolic-ref --quiet HEAD)" || :
428 cut -d ' ' -f 3 <"$insn" | sort -u -b -k1,1 | join - "$trf" |
429 while read -r name rev; do
430 orig="$(git rev-parse --verify --quiet "$name" --)" || :
431 init_reflog "$name"
432 if [ "$rev" != "$orig" ]; then
433 [ -z "$dryrun" -a -n "$quiet" ] ||
434 origsh="$(git rev-parse --verify --short --quiet "$name" --)" || :
435 if [ -z "$dryrun" ]; then
436 if [ -n "$srh" ] && [ "$srh" = "$name" ]; then
437 [ -n "$quiet" ] || echo "Detaching HEAD to revert $name"
438 detachat="$orig"
439 [ -n "$detachat" ] || detachat="$(make_empty_commit)"
440 git update-ref -m "tgrevert: detach HEAD to revert $name" --no-deref HEAD "$detachat"
441 [ -n "$quiet" ] || git --no-pager log -n 1 --format=format:'HEAD is now at %h... %s' HEAD
443 git update-ref -m "$msg" "$name" "$rev"
445 if [ -n "$dryrun" -o -z "$quiet" ]; then
446 revsh="$(git rev-parse --verify --short --quiet "$rev" --)" || :
447 if [ -n "$origsh" ]; then
448 hdr=' '
449 [ -z "$dryrun" ] || hdr='-'
450 printf '%s %s -> %s %s\n' "$hdr" "$origsh" "$revsh" "$name"
451 else
452 hdr='*'
453 [ -z "$dryrun" ] || hdr='-'
454 printf '%s %s -> %s %s\n' "$hdr" "$nullref" "$revsh" "$name"
457 else
458 : #[ -z "$dryrun" -a -n "$quiet" ] || printf "* %-*s %s\n" $notewidth "[no change]" "$name"
460 done
462 exit 0