tg-revert.sh: convert any top-bases in TOPGIT REFS
[topgit/pro.git] / tg-revert.sh
bloba2f6ee84c4ac05d6ba99a641b78d3f75c0d21b4d
1 #!/bin/sh
2 # TopGit revert command
3 # Copyright (C) 2015 Kyle J. McKay <mackyle@gmail.com>
4 # All rights reserved.
5 # GPLv2
7 lf="$(printf '\n.')" && lf="${lf%?}"
8 tab="$(printf '\t.')" && tab="${tab%?}"
9 USAGE="Usage: ${tgname:-tg} [...] revert (-f | -i | -n) [-q] [--tgish-only] [--no-deps] [--no-stash] [--exclude <ref>...] (<tagname> | --stash) [<ref>...]"
10 USAGE="$USAGE$lf Or: ${tgname:-tg} [...] revert [-l] [--tgish-only] [(--deps | --rdeps)] [--exclude <ref>...] (<tagname> | --stash) [(--heads | <ref>...)]"
12 usage()
14 if [ "${1:-0}" != 0 ]; then
15 printf '%s\n' "$USAGE" >&2
16 else
17 printf '%s\n' "$USAGE"
19 exit ${1:-0}
22 ## Parse options
24 force=
25 interact=
26 dryrun=
27 list=
28 deps=
29 rdeps=
30 tgish=
31 nodeps=
32 nostash=
33 exclude=
34 quiet=
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 --deps|--deps-only)
47 deps=1
49 --rdeps)
50 rdeps=1
52 --tgish-only)
53 tgish=1
55 -f|--force)
56 force=1
58 -i|--interactive)
59 interact=1
61 -n|--dry-run)
62 dryrun=1
64 --no-deps)
65 nodeps=1
67 --no-stash)
68 nostash=1
70 --exclude=*)
71 [ -n "${1#--exclude=}" ] || die "--exclude= requires a ref name"
72 case "${1#--exclude=}" in refs/*) rn="${1#--exclude=}";; *) rn="refs/heads/${1#--exclude=} refs/$topbases/${1#--exclude=}"; esac
73 exclude="$exclude $rn";;
74 --exclude)
75 shift
76 [ -n "$1" ] || die "--exclude requires a ref name"
77 case "$1" in refs/*) rn="$1";; *) rn="refs/heads/$1 refs/$topbases/$1"; esac
78 exclude="$exclude $rn";;
79 --)
80 shift
81 break
83 --stash|--stash"@{"*"}")
84 break
86 -?*)
87 echo "Unknown option: $1" >&2
88 usage 1
91 break
93 esac; shift; done
94 [ -z "$exclude" ] || exclude="$exclude "
96 [ -z "$list" -o -z "$force$interact$dryrun$nodeps$nostash" ] || usage 1
97 [ -z "$force$interact$dryrun" -o -z "$list$deps$rdeps" ] || usage 1
98 [ -z "$deps" -o -z "$rdeps" ] || usage 1
99 [ -n "$list$force$interact$dryrun" ] || list=1
100 [ -n "$1" ] || { echo "Tag name required" >&2; usage 1; }
101 tagname="$1"
102 shift
103 [ -n "$list" -o "$1" != "--heads" ] || usage 1
104 [ "$tagname" != "--stash" ] || tagname=refs/tgstash
105 case "$tagname" in --stash"@{"*"}")
106 strip="${tagname#--stash??}"
107 strip="${strip%?}"
108 tagname="refs/tgstash@{$strip}"
109 esac
110 refname="$tagname"
111 case "$refname" in HEAD|refs/*);;*)
112 suffix="${refname%@*}"
113 suffix="${refname#$suffix}"
114 refname="${refname%$suffix}"
115 if reftest="$(git rev-parse --revs-only --symbolic-full-name "$refname" -- 2>/dev/null)" &&
116 [ -n "$reftest" ]; then
117 refname="$reftest$suffix"
118 else
119 if hash="$(git rev-parse --quiet --verify "$refname$suffix")"; then
120 refname="$hash"
121 else
122 refname="refs/tags/$refname$suffix"
125 esac
126 reftype=tag
127 case "$refname" in refs/tags/*) tagname="${refname#refs/tags/}";; *) reftype=ref; tagname="$refname"; esac
128 git rev-parse --verify --quiet "$refname^{tag}" -- >/dev/null || die "not annotated/signed tag: $refname"
129 tgf="$(get_temp tag)"
130 trf="$(get_temp refs)"
131 tagdataref="$refname^{tag}"
132 while
133 git cat-file tag "$tagdataref" >"$tgf" || die "cannot read tag: $refname"
134 sed -ne '/^-----BEGIN TOPGIT REFS-----$/,/^-----END TOPGIT REFS-----$/p' <"$tgf" |
135 sed -ne "/^\\($octet20\\) \\(refs\/[^ $tab][^ $tab]*\\)\$/{s//\\2 \\1/;p;}" |
136 sed -e "s,^refs/top-bases/,refs/$topbases/,g" |
137 LC_ALL=C sort -u -b -k1,1 >"$trf"
138 ! [ -s "$trf" ]
140 # If it's a tag of a tag, dereference it and try again
141 read -r field tagtype <<-EOT || break
142 $(sed -n '1,/^$/p' <"$tgf" | grep '^type [^ ][^ ]*$' || :)
144 [ "$tagtype" = "tag" ] || break
145 read -r field tagdataref <<-EOT || break
146 $(sed -n '1,/^$/p' <"$tgf" | grep '^object [^ ][^ ]*$' || :)
148 [ -n "$tagdataref" ] || break
149 tagdataref="$tagdataref^{tag}"
150 git rev-parse --verify --quiet "$tagdataref" -- >/dev/null || break
151 done
152 [ -s "$trf" ] || die "$reftype $tagname does not contain a TOPGIT REFS section"
153 rcnt=$(( $(wc -l <"$trf") ))
154 vcnt=$(( $(cut -d ' ' -f 2 <"$trf" | git cat-file --batch-check='%(objectname)' | grep -v ' missing$' | wc -l) ))
155 [ "$rcnt" -eq "$vcnt" ] || die "$reftime $tagname contains $rcnt ref(s) but only $vcnt are still valid"
156 cat "$trf" >"$tg_ref_cache"
157 create_ref_dirs
158 tg_ref_cache_only=1
159 tg_read_only=1
161 [ $# -ne 0 -o -z "$rdeps$deps" ] || set -- --heads
162 [ $# -ne 1 -o -z "$deps" -o "$1" != "--heads" ] || { deps=; set --; }
163 if [ $# -eq 1 -a "$1" = "--heads" ]; then
164 srt="$(get_temp sort)"
165 LC_ALL=C sort -b -k2,2 <"$trf" >"$srt"
166 set -- $(
167 git merge-base --independent $(cut -d ' ' -f 2 <"$srt") |
168 LC_ALL=C sort -b -k1,1 |
169 join -2 2 -o 2.1 - "$srt" |
170 LC_ALL=C sort)
173 is_tgish() {
174 case "$1" in
175 refs/heads/*)
176 ref_exists "refs/$topbases/${1#refs/heads/}"
178 refs/"$topbases"/*)
179 ref_exists "refs/heads/${1#refs/$topbases/}"
184 esac
187 refs=
188 for b; do
189 exp=
190 case "$b" in refs/*) exp=1; rn="$b";; *) rn="refs/heads/$b"; esac
191 ref_exists "$rn" || die "not present in tag data (try --list): $rn"
192 case " $refs " in *" $rn "*);;*)
193 refs="${refs:+$refs }$rn"
194 if [ -z "$list" ] && [ -z "$nodeps" -o -z "$exp" ] && is_tgish "$rn"; then
195 case "$rn" in
196 refs/heads/*)
197 refs="$refs refs/$topbases/${rn#refs/heads/}"
199 refs/"$topbases"/*)
200 refs="$refs refs/heads/${rn#refs/$topbases/}"
202 esac
204 esac
205 done
207 show_dep() {
208 case "$exclude" in *" refs/heads/$_dep "*) return; esac
209 case " $seen_deps " in *" $_dep "*) return 0; esac
210 seen_deps="${seen_deps:+$seen_deps }$_dep"
211 [ -z "$tgish" -o -n "$_dep_is_tgish" ] || return 0
212 printf 'refs/heads/%s\n' "$_dep"
213 [ -z "$_dep_is_tgish" ] ||
214 printf 'refs/%s/%s\n' "$topbases" "$_dep"
217 show_deps()
219 no_remotes=1
220 recurse_deps_exclude=
221 while read _b && [ -n "$_b" ]; do
222 case "$exclude" in *" $_b "*) continue; esac
223 if ! is_tgish "$_b"; then
224 [ -z "$tgish" ] || continue
225 printf '%s\n' "$_b"
226 continue
228 case "$_b" in refs/"$topbases"/*) _b="refs/heads/${_b#refs/$topbases/}"; esac
229 _b="${_b#refs/heads/}"
230 case " $recurse_deps_exclude " in *" $_b "*) continue; esac
231 seen_deps=
232 _dep="$_b"; _dep_is_tgish=1; show_dep
233 recurse_deps show_dep "$_b"
234 recurse_deps_exclude="$recurse_deps_exclude $seen_deps"
235 done
238 show_rdep()
240 case "$exclude" in *" refs/heads/$_dep "*) return; esac
241 [ -z "$tgish" -o -n "$_dep_is_tgish" ] || return 0
242 printf '%s %s\n' "$_depchain" "$(ref_exists_rev_short "refs/heads/$_dep")~$_dep"
245 show_rdeps()
247 no_remotes=1
248 show_break=
249 seen_deps=
250 while read _b && [ -n "$_b" ]; do
251 case "$exclude" in *" $_b "*) continue; esac
252 if ! is_tgish "$_b"; then
253 [ -z "$tgish" ] || continue
254 [ -z "$showbreak" ] || echo
255 showbreak=1
256 printf '%s\n' "$_b"
257 continue
259 case "$_b" in refs/"$topbases"/*) _b="refs/heads/${_b#refs/$topbases/}"; esac
260 _b="${_b#refs/heads/}"
261 case " $seen_deps " in *" $_b "*) continue; esac
262 seen_deps="$seen_deps $_b"
263 [ -z "$showbreak" ] || echo
264 showbreak=1
266 echo "$(ref_exists_rev_short "refs/heads/$_b")~$_b"
267 recurse_preorder=1
268 recurse_deps show_rdep "$_b"
269 } | sed -e 's/[^ ][^ ]*[ ]/ /g' -e 's/~/ /'
270 done
273 refslist() {
274 [ -z "$refs" ] || sed 'y/ /\n/' <<-EOT
275 $refs
279 if [ -n "$list" ]; then
280 if [ -z "$deps$rdeps" ]; then
281 while read -r name rev; do
282 case "$exclude" in *" $name "*) continue; esac
283 [ -z "$refs" ] || case " $refs " in *" $name "*);;*) continue; esac
284 [ -z "$tgish" ] || is_tgish "$name" || continue
285 printf '%s %s\n' "$(git rev-parse --verify --quiet --short "$rev" --)" "$name"
286 done <"$trf"
287 exit 0
289 if [ -n "$deps" ]; then
290 refslist | show_deps | LC_ALL=C sort -u -b -k1,1 |
291 join - "$trf" |
292 while read -r name rev; do
293 printf '%s %s\n' "$(git rev-parse --verify --quiet --short "$rev" --)" "$name"
294 done
295 exit 0
297 refslist | show_rdeps
298 exit 0
300 insn="$(get_temp isns)"
302 get_short() {
303 [ -n "$interact" ] || { printf '%s' "$1"; return 0; }
304 git rev-parse --verify --quiet --short "$1" --
307 if [ -n "$nodeps" -o -z "$refs" ]; then
308 while read -r name rev; do
309 case "$exclude" in *" $name "*) continue; esac
310 [ -z "$refs" ] || case " $refs " in *" $name "*);;*) continue; esac
311 [ -z "$tgish" ] || is_tgish "$name" || continue
312 printf 'revert %s %s\n' "$(get_short "$rev")" "$name"
313 done <"$trf" | LC_ALL=C sort -u -b -k3,3 >"$insn"
314 else
315 refslist | show_deps | LC_ALL=C sort -u -b -k1,1 |
316 join - "$trf" |
317 while read -r name rev; do
318 printf 'revert %s %s\n' "$(get_short "$rev")" "$name"
319 done >"$insn"
321 if [ -n "$interact" ]; then
322 count=$(( $(wc -l <"$insn") ))
323 cat <<EOT >>"$insn"
325 # Revert using $refname data ($count command(s))
327 # Commands:
328 # r, revert = revert ref to specified hash
330 # Note that changing the hash value shown here will have NO EFFECT.
332 # If you remove a line here THAT REVERT WILL BE SKIPPED.
334 # However, if you remove everything, the revert will be aborted.
336 run_editor "$insn" ||
337 die "there was a problem with the editor '$tg_editor'"
338 git stripspace -s <"$insn" >"$insn"+
339 mv -f "$insn"+ "$insn"
340 [ -s "$insn" ] || die "nothing to do"
341 while read -r op hash ref; do
342 [ "$op" = "r" -o "$op" = "revert" ] ||
343 die "invalid op in instruction: $op $hash $ref"
344 case "$ref" in refs/?*);;*)
345 die "invalid ref in instruction: $op $hash $ref"
346 esac
347 ref_exists "$ref" ||
348 die "unknown ref in instruction: $op $hash $ref"
349 done <"$insn"
351 msg="tgrevert: $reftype $tagname ($(( $(wc -l <"$insn") )) command(s))"
352 [ -n "$dryrun" -o -n "$nostash" ] || $tg tag -q --none-ok -m "$msg" --stash
353 refwidth="$(git config --get --int core.abbrev 2>/dev/null || :)"
354 [ -n "$refwidth" ] || refwidth=7
355 [ $refwidth -ge 4 -a $refwidth -le 40 ] || refwidth=7
356 nullref="$(printf '%.*s' $refwidth "$nullsha")"
357 notewidth=$(( $refwidth + 4 + $refwidth ))
358 srh=
359 [ -n "$dryrun" ] || srh="$(git symbolic-ref --quiet HEAD || :)"
360 cut -d ' ' -f 3 <"$insn" | LC_ALL=C sort -u -b -k1,1 | join - "$trf" |
361 while read -r name rev; do
362 orig="$(git rev-parse --verify --quiet "$name" -- || :)"
363 if [ -n "$logrefupdates" -o "$name" = "refs/tgstash" ]; then
364 case "$name" in refs/heads/*|refs/"$topbases"/*|refs/tgstash)
365 mkdir -p "$git_dir/logs/$(dirname "$name")" 2>/dev/null || :
366 { >>"$git_dir/logs/$name" || :; } 2>/dev/null
367 esac
369 if [ "$rev" != "$orig" ]; then
370 [ -z "$dryrun" -a -n "$quiet" ] ||
371 origsh="$(git rev-parse --verify --short --quiet "$name" -- || :)"
372 if [ -z "$dryrun" ]; then
373 if [ -n "$srh" ] && [ "$srh" = "$name" ]; then
374 [ -n "$quiet" ] || echo "Detaching HEAD to revert $name"
375 detachat="$orig"
376 [ -n "$detachat" ] || detachat="$(make_empty_commit)"
377 git update-ref -m "tgrevert: detach HEAD to revert $name" --no-deref HEAD "$detachat"
378 [ -n "$quiet" ] || git log -n 1 --format=format:'HEAD is now at %h... %s' HEAD
380 git update-ref -m "$msg" "$name" "$rev"
382 if [ -n "$dryrun" -o -z "$quiet" ]; then
383 revsh="$(git rev-parse --verify --short --quiet "$rev" -- || :)"
384 if [ -n "$origsh" ]; then
385 hdr=' '
386 [ -z "$dryrun" ] || hdr='-'
387 printf '%s %s -> %s %s\n' "$hdr" "$origsh" "$revsh" "$name"
388 else
389 hdr='*'
390 [ -z "$dryrun" ] || hdr='-'
391 printf '%s %s -> %s %s\n' "$hdr" "$nullref" "$revsh" "$name"
394 else
395 : #[ -z "$dryrun" -a -n "$quiet" ] || printf "* %-*s %s\n" $notewidth "[no change]" "$name"
397 done
399 exit 0