tg-tag: handle reflog entries with extra tabs
[topgit/pro.git] / tg-tag.sh
blobb94776ed32ea76a138485a2c436f8a86a08a7deb
1 #!/bin/sh
2 # TopGit tag command
3 # Copyright (C) 2015,2017,2018,2021 Kyle J. McKay <mackyle@gmail.com>
4 # All rights reserved
5 # GPLv2
7 USAGE="\
8 Usage: ${tgname:-tg} [...] tag [<option>...] <tagname> [<branch>...]
9 Or: ${tgname:-tg} [...] tag [<option>...] --refs [<branch>...]
10 Or: ${tgname:-tg} [...] tag [<option>...] (-g | --reflog) [<tagname>]
11 Or: ${tgname:-tg} [...] tag --clear <tagname>
12 Or: ${tgname:-tg} [...] tag --delete <tagname>
13 Or: ${tgname:-tg} [...] tag --drop <tagname>@{n}
14 Options:
15 --refs[-only] show TOPGIT REFS but do not actually create a tag
16 --reflog / -g show tag reflog instead of creating a tag
17 --walk-reflogs alias for --reflog
18 --clear clear tag's reflog to @{0} instead of creating a tag
19 --delete delete the tag and its reflog instead of creating
20 --drop drop the tag's specified reflog entry (no create)
21 --quiet / -q suppress info messages (repeating suppresses more)
22 --sign / -s create a signed tag
23 --force / -f replace existing tag with same name
24 -u <keyid> sign with <keyid> (implies '--sign')
25 --local-user <k> alias for '-u <k>'
26 --message <msg> replace default tag message
27 -m <msg> (default message is \"tag ... branches ...\")
28 --file <file> replace default tag message
29 -F <file> with contents of <file>
30 --edit run the editor on the default tag message (default)
31 --no-edit do not run the editor on the default tag message
32 --tree <treeish> force created <tagname>^{tree} to be <treeish>
33 --all use all branches for <branch>... (default is HEAD)
34 --allow-outdated allow outdated TopGit branches to be included
35 --stash use refs/tgstash for <tagname> (implies '--all')
36 --reflog-message use reflog message for '--walk-reflogs'
37 --commit-message use commit message for '--walk-reflogs'
38 --no-type omit non-tag annotation in '--walk-reflogs' mode
39 --max-count <n> show at most <n> reflog entries in '-g' mode
40 -n <n> / -<n> alias for '--max-count <n>'"
42 usage()
44 if [ "${1:-0}" != 0 ]; then
45 printf '%s\n' "$USAGE" >&2
46 else
47 printf '%s\n' "$USAGE"
49 exit ${1:-0}
52 ## Parse options
54 signed=
55 keyid=
56 force=
57 msg=
58 msgfile=
59 noedit=
60 defnoedit=
61 refsonly=
62 maxcount=
63 reflog=
64 outofdateok=
65 anyrefok=
66 defbranch=HEAD
67 stash=
68 anonymous=
69 reflogmsg=
70 notype=
71 setreflogmsg=
72 quiet=0
73 noneok=
74 clear=
75 delete=
76 drop=
77 anonymous=
78 treeish=
79 sawall=
81 is_numeric()
83 [ -n "$1" ] || return 1
84 while [ -n "$1" ]; do
85 case "$1" in
86 [0-9]*)
87 set -- "${1#?}";;
89 break;;
90 esac
91 done
92 [ -z "$1" ]
95 while [ $# -gt 0 ]; do case "$1" in
96 -h|--help)
97 usage
99 -q|--quiet)
100 quiet=$(( $quiet + 1 ))
102 --none-ok)
103 noneok=1
105 --clear)
106 clear=1
108 --delete)
109 delete=1
111 --drop)
112 drop=1
114 -g|--reflog|--walk-reflogs)
115 reflog=1
117 --reflog-message)
118 reflogmsg=1
119 setreflogmsg=1
121 --no-reflog-message|--commit-message)
122 reflogmsg=
123 setreflogmsg=1
125 --no-type)
126 notype=1
128 -s|--sign)
129 signed=1
131 -u|--local-user|--local-user=*)
132 case "$1" in --local-user=*)
133 x="$1"
134 shift
135 set -- --local-user "${x#--local-user=}" "$@"
136 esac
137 if [ $# -lt 2 ]; then
138 echo "The $1 option requires an argument" >&2
139 usage 1
141 shift
142 keyid="$1"
143 signed=1
145 -f|--force)
146 force=1
148 --no-edit)
149 noedit=1
151 --edit)
152 noedit=0
154 --allow-outdated)
155 outofdateok=1
157 --allow-any)
158 anyrefok=1
160 --tree|--tree=*)
161 case "$1" in --tree=*)
162 x="$1"
163 shift
164 set -- --tree "${x#--tree=}" "$@"
165 esac
166 if [ $# -lt 2 ]; then
167 echo "The $1 option requires an argument" >&2
168 usage 1
170 shift
171 treeish="$(git rev-parse --quiet --verify "$1^{tree}" --)" || {
172 echo "Not a valid treeish: $1" >&2
173 exit 1
176 --refs|--refs-only)
177 refsonly=1
179 -m|--message|--message=*)
180 case "$1" in --message=*)
181 x="$1"
182 shift
183 set -- --message "${x#--message=}" "$@"
184 esac
185 if [ $# -lt 2 ]; then
186 echo "The $1 option requires an argument" >&2
187 usage 1
189 shift
190 msg="$1"
192 -F|--file|--file=*)
193 case "$1" in --file=*)
194 x="$1"
195 shift
196 set -- --file "${x#--file=}" "$@"
197 esac
198 if [ $# -lt 2 ]; then
199 echo "The $1 option requires an argument" >&2
200 usage 1
202 shift
203 msgfile="$1"
205 -n|--max-count|--max-count=*|-[1-9]*)
206 case "$1" in --max-count=*)
207 x="$1"
208 shift
209 set -- --max-count "${x#--max-count=}" "$@"
210 esac
211 case "$1" in -[1-9]*)
212 x="${1#-}"
213 shift
214 set -- -n "$x" "$@"
215 esac
216 if [ $# -lt 2 ]; then
217 echo "The $1 option requires an argument" >&2
218 usage 1
220 shift
221 maxcount="$1"
224 shift
225 break
227 --all)
228 sawall=1
229 defbranch=--all
231 --stash|--stash"@{"*"}")
232 if [ -n "$reflog" ]; then
233 case "$2" in -[1-9]*)
234 x1="$1"
235 x2="$2"
236 shift
237 shift
238 set -- "$x2" "$x1" "$@"
239 continue
240 esac
241 else
242 if [ "$2" = "--all" ]; then
243 x1="$1"
244 shift
245 shift
246 set -- "$x1" "$@"
247 sawall=1
248 elif [ "$2" = "--" ]; then
249 x1="$1"
250 shift
251 shift
252 set -- "$x1" "$@"
255 stash=1
256 defbranch=--all
257 break
259 --anonymous)
260 anonymous=1
261 defbranch=--all
262 quiet=2
263 break
265 -?*)
266 echo "Unknown option: $1" >&2
267 usage 1
270 if [ -n "$reflog" ]; then
271 case "$2" in -[1-9]*)
272 x1="$1"
273 x2="$2"
274 shift
275 shift
276 set -- "$x2" "$x1" "$@"
277 continue
278 esac
280 break
282 esac; shift; done
284 [ "$stash$anonymous" != "11" ] || usage 1
285 [ -z "$stash$anonymous" ] || [ -n "$reflog$drop$clear$delete" ] || { outofdateok=1; force=1; defnoedit=1; }
286 [ -z "$sawall" ] || [ $# -gt 0 ] || { outofdateok=1; force=1; defnoedit=1; }
287 [ -n "$noedit" ] || noedit="$defnoedit"
288 [ "$noedit" != "0" ] || noedit=
289 [ -z "$reflog" ] || [ -z "$drop$clear$delete$signed$keyid$force$msg$msgfile$noedit$treeish$refsonly$outofdateok$sawall" ] || usage 1
290 [ -n "$reflog" ] || [ -z "$setreflogmsg$notype$maxcount" ] || usage 1
291 [ -z "$drop$clear$delete" ] || [ -z "$setreflogmsg$notype$maxcount$signed$keyid$force$msg$msgfile$noedit$treeish$refsonly$outofdateok$sawall" ] || usage 1
292 [ -z "$reflog$drop$clear$delete" ] || [ "$reflog$drop$clear$delete" = "1" ] || usage 1
293 [ -z "$maxcount" ] || is_numeric "$maxcount" || die "invalid count: $maxcount"
294 [ -z "$maxcount" ] || [ $maxcount -gt 0 ] || die "invalid count: $maxcount"
295 [ -z "$msg" ] || [ -z "$msgfile" ] || die "only one -F or -m option is allowed."
296 [ -z "$refsonly" ] || set -- refs..only "$@"
297 [ $# -gt 0 ] || [ -z "$reflog$sawall" ] || set -- --stash
298 [ -n "$1" ] || { echo "Tag name required" >&2; usage 1; }
299 tagname="$1"
300 shift
301 [ -z "$sawall" ] || [ $# -eq 0 ] || die "branch names not allowed with --all"
302 [ "$tagname" != "--stash" ] || tagname=refs/tgstash
303 [ "$tagname" != "--anonymous" ] || tagname=TG_STASH
304 case "$tagname" in --stash"@{"*"}")
305 strip="${tagname#--stash??}"
306 strip="${strip%?}"
307 tagname="refs/tgstash@{$strip}"
308 esac
309 refname="$tagname"
310 sfx=
311 sfxis0=
312 case "$refname" in [!@]*"@{"*"}")
313 _pfx="@{"
314 _refonly="${refname%%"$_pfx"*}"
315 sfx="${refname#"$_refonly"}"
316 refname="$_refonly"
317 _numonly="${sfx#??}"
318 _numonly="${_numonly%?}"
319 [ "${_numonly#[0-9]}" != "$_numonly" ] && [ "${_numonly#*[!0-9]}" = "$_numonly" ] || die "invalid suffix: \"$sfx\""
320 if [ "${_numonly#*[!0]}" = "$_numonly" ]; then
321 # transform @{0000000} etc. into @{0}
322 sfx="@{0}"
323 sfxis0=1
324 else
325 # remove any leading zeros
326 _ld0="${_numonly%%[!0]*}"
327 [ -z "$_ld0" ] || _numonly="${_numonly#$_ld0}"
328 sfx="@{$_numonly}"
330 esac
332 v_resolve_full_name() {
333 # `git rev-parse --revs-only --symbolic-full-name` should do this
334 # but its behavior is inadequate in that it will not produce a result
335 # if the input is ambiguous even though `git rev-parse --verify` still
336 # will in that case. Nasty.
337 # Besides, we really don't want to follow symbolic refs anyway and
338 # we would need to do that ourselves because `git rev-parse` does not
339 # have a `--no-deref` option like `git update-ref` does. Ugly.
341 eval "$1="
342 _normref="$(git check-ref-format --normalize --allow-onelevel "$2" 2>/dev/null)" && [ -n "$_normref" ] || return 1
343 eval "$1="'"$_normref"'
344 git rev-parse --verify --quiet "$_normref" -- >/dev/null 2>&1 || return 0
345 case "$_normref" in refs/?*) return 0; esac
346 _found=
347 _rsuffix=
348 # see `git help revisions` for this DWIM list of interpretations
349 for _rprefix in "refs" "refs/tags" "refs/heads" "refs/remotes"; do
350 ! git rev-parse --verify --quiet "$_rprefix/$_normref" -- >/dev/null 2>&1 || { _found=1; break; }
351 done
352 if [ -z "$_found" ]; then
353 _rsuffix="/HEAD"
354 ! git rev-parse --verify --quiet "$_rprefix/$_normref$_rsuffix" -- >/dev/null 2>&1 || _found=1
356 [ -z "$_found" ] || eval "$1="'"$_rprefix/$_normref$_rsuffix"'
357 return 0
360 case "$refname" in [Hh][Ee][Aa][Dd]|"@") refname="HEAD"; esac
361 case "$refname" in [Tt][Gg]_[Ss][Tt][Aa][Ss][Hh]) refname="TG_STASH"; esac
362 case "$refname" in HEAD|TG_STASH|refs/*);;*)
363 if v_resolve_full_name reftest "$refname" && [ -n "$reftest" ]; then
364 if [ -n "$reflog$drop$clear$delete" ]; then
365 refname="$reftest"
366 else
367 case "$reftest" in
368 refs/tags/*|refs/tgstash)
369 refname="$reftest"
372 refname="refs/tags/$refname"
373 esac
375 else
376 refname="refs/tags/$refname"
378 esac
379 refname="$refname$sfx"
380 reftype=tag
381 case "$refname" in refs/tags/*) tagname="${refname#refs/tags/}";; *) reftype=ref; tagname="$refname"; esac
382 logbase="$git_common_dir"
383 [ "${refname%$sfx}" != "HEAD" ] || logbase="$git_dir"
384 [ -z "$reflog$drop$clear$delete" ] || [ $# -eq 0 ] || usage 1
385 if [ -n "$drop$clear$delete" ]; then
386 if [ -n "$sfx" ]; then
387 [ -z "$clear$delete" ] || die "invalid ref name ($sfx suffix not allowed, try --drop): $refname"
388 else
389 [ -z "$drop" ] || die "invalid reflog entry name (@{n} suffix required): $refname"
391 old="$(git rev-parse --verify --quiet --short "${refname%$sfx}" --)" || die "no such ref: ${refname%$sfx}"
392 if [ -n "$delete" ]; then
393 case "$refname" in [Hh][Ee][Aa][Dd])
394 extra=
395 ! symref="$(git symbolic-ref -q --short HEAD 2>/dev/null)" ||
396 extra=" (did you mean to delete \"$symref\"?)"
397 die "HEAD may not be deleted$extra"
398 esac
400 srh="$(git symbolic-ref --quiet HEAD)" &&
401 [ -n "$srh" ] && [ "$srh" = "$refname" ]
402 then
403 orig="$(git rev-parse --verify --quiet "$refname^0" --)" || die "no such committish ref: $refname"
404 [ "$quiet" -gt 0 ] || warn "detaching HEAD to delete $refname"
405 git update-ref -m "tgtag: detach HEAD to delete $refname" --no-deref HEAD "$orig" || die "detach failed"
406 [ "$quiet" -gt 0 ] || warn "$(git --no-pager log -n 1 --format=format:'HEAD is now at %h... %s' HEAD)"
408 git update-ref --no-deref -d "$refname" || die "git update-ref --no-deref -d failed"
409 printf "Deleted $reftype '%s' (was %s)\n" "$tagname" "$old"
410 exit 0
411 elif [ -n "$clear" ]; then
412 [ -f "$logbase/logs/$refname" ] || die "no reflog found for: $refname"
413 [ -s "$logbase/logs/$refname" ] || die "empty reflog found for: $refname"
414 cp -p "$logbase/logs/$refname" "$logbase/logs/$refname^-+" || die "cp failed"
415 awk '
416 NF >= 5 { line[1] = $0 }
417 END {
418 if (1 in line) {
419 if (match(line[1], /^[0-9a-fA-F]+ /)) {
420 old = substr(line[1], 1, RLENGTH - 1)
421 rest = substr(line[1], RLENGTH)
422 gsub(/[0-9a-fA-F]/, "0", old)
423 line[1] = old rest
425 print line[1]
428 ' <"$logbase/logs/$refname^-+" >"$logbase/logs/$refname" || die "reflog clear failed"
429 rm -f "$logbase/logs/$refname^-+"
430 printf "Cleared $reftype '%s' reflog to single @{0} entry\n" "$tagname"
431 exit 0
432 else # -n "$drop"
433 ref="$old"
434 old="$(git rev-parse --verify --short "$refname" -- 2>/dev/null)" || {
435 # if it failed, redo showing STDERR, otherwise suppress STDERR
436 git rev-parse --verify --short "$refname" -- >/dev/null || :
437 exit 1
439 [ -z "$sfxis0" ] || [ "$ref" = "$old" ] || sfxis0=
440 [ -z "$sfxis0" ] || ! git symbolic-ref -q "${refname%$sfx}" -- >/dev/null 2>&1 || sfxis0=
441 if [ -n "$sfxis0" ]; then
442 [ -f "$logbase/logs/${refname%$sfx}" ] || die "no reflog found for: ${refname%$sfx}"
443 [ -s "$logbase/logs/${refname%$sfx}" ] || die "empty reflog found for: ${refname%$sfx}"
444 # make sure @{1} is valid via pseudo stale-fix before using --updateref
445 cnt="$(( $(wc -l <"$logbase/logs/${refname%$sfx}") ))"
446 lastcnt=
447 at1=
448 while
449 # avoid using --updateref if @{0} is the only entry (i.e. less than 2 lines in log)
450 [ $cnt -ge 2 ] && [ "$cnt" != "$lastcnt" ] &&
451 at1="$(git rev-parse --verify --quiet "${refname%$sfx}@{1}" -- 2>/dev/null)" &&
452 [ -n "$at1" ] &&
453 ! git rev-list --no-walk --objects "$at1" -- >/dev/null 2>&1
455 # poor man's --stale-fix that's faster and actually works reliably
456 git reflog delete --rewrite "${refname%$sfx}@{1}" >/dev/null 2>&1 ||
457 die "pseudo stale-fix failed for broken ${refname%$sfx}@{1}"
458 lastcnt="$cnt"
459 at1=
460 cnt="$(( $(wc -l <"$logbase/logs/${refname%$sfx}") ))"
461 done
462 # avoid using --updateref if @{0} is the only entry (i.e. less than 2 lines in log)
463 [ -n "$at1" ] && [ $cnt -ge 2 ] || sfxis0=
465 git reflog delete --rewrite ${sfxis0:+--updateref} "$refname" || die "reflog drop failed"
466 if [ -n "$sfxis0" ]; then
467 # check if we need to clean up
468 check="$(git rev-parse --verify --quiet "${refname%$sfx}" --)" || :
469 [ "${check#*[!0]}" != "$check" ] || check= # all 0's or empty is bad
470 # Git versions prior to 2.4.0 might need some clean up
471 [ -n "$check" ] || git update-ref -d "${refname%$sfx}" >/dev/null 2>&1 || :
473 printf "Dropped $reftype '%s' reflog entry (was %s)\n" "$tagname" "$old"
474 exit 0
477 if [ -n "$reflog" ]; then
478 [ "$refname" = "refs/tgstash" ] || [ -n "$setreflogmsg" ] || reflogmsg=1
479 git rev-parse --verify --quiet "$refname" -- >/dev/null ||
480 die "no such ref: $refname"
481 [ -s "$logbase/logs/$refname" ] ||
482 die "no reflog present for $reftype: $tagname"
483 showref="$refname"
484 [ "$refname" = "HEAD" ] || showref="$(git rev-parse --revs-only --abbrev-ref=strict "$refname" --)"
485 hashcolor=
486 resetcolor=
487 if git config --get-colorbool color.tgtag; then
488 metacolor="$(git config --get-color color.tgtag.meta)"
489 [ -n "$metacolor" ] || metacolor="$(git config --get-color color.diff.meta "bold")"
490 hashcolor="$(git config --get-color color.tgtag.commit)"
491 [ -n "$hashcolor" ] || hashcolor="$(git config --get-color color.diff.commit "yellow")"
492 datecolor="$(git config --get-color color.tgtag.date "bold blue")"
493 timecolor="$(git config --get-color color.tgtag.time "green")"
494 resetcolor="$(git config --get-color "" reset)"
496 setup_strftime
497 output()
499 sed 's/[^ ][^ ]* //' <"$logbase/logs/$refname" |
500 awk '{a[i++]=$0} END {for (j=i-1; j>=0;) print a[j--]}' |
501 git cat-file --batch-check='%(objectname) %(objecttype) %(rest)' |
503 stashnum=-1
504 lastdate=
505 while read -r newrev type rest; do
506 stashnum=$(( $stashnum + 1 ))
507 [ "$type" != "missing" ] || continue
508 ne= rest2=
509 IFS=">" read -r ne rest2 <<-~EOT~ || :
510 ${rest}X
511 ~EOT~
512 es= ez= msg=
513 read -r es ez msg <<-~EOT~ || :
514 ${rest2%X}
515 ~EOT~
516 obj="$(git rev-parse --verify --quiet --short "$newrev" --)"
517 extra=
518 [ "$type" = "tag" ] || [ -n "$notype" ] ||
519 extra="$hashcolor($metacolor$type$resetcolor$hashcolor)$resetcolor "
520 if [ -z "$reflogmsg" ] || [ -z "$msg" ]; then
521 objmsg=
522 if [ "$type" = "tag" ]; then
523 objmsg="$(git cat-file tag "$obj" |
524 sed '1,/^$/d' | sed '/^$/,$d')"
525 elif [ "$type" = "commit" ]; then
526 objmsg="$(git --no-pager log -n 1 --format='format:%s' "$obj" --)"
528 [ -z "$objmsg" ] || msg="$objmsg"
530 read newdate newtime <<-EOT
531 $(strftime "%Y-%m-%d %H:%M:%S" "$es" 2>/dev/null)
533 if [ "$lastdate" != "$newdate" ]; then
534 printf '%s=== %s ===%s\n' "$datecolor" "$newdate" "$resetcolor"
535 lastdate="$newdate"
537 printf '%s %s %s%s@{%s}: %s\n' "$hashcolor$obj$reseutcolor" \
538 "$timecolor$newtime$resetcolor" \
539 "$extra" "$showref" "$stashnum" "$msg"
540 if [ -n "$maxcount" ]; then
541 maxcount=$(( $maxcount - 1 ))
542 [ $maxcount -gt 0 ] || break
544 done
547 page output
548 exit 0
550 [ -z "$signed" ] || [ "$reftype" = "tag" ] || die "signed tags must be under refs/tags"
551 [ $# -gt 0 ] || set -- $defbranch
552 all=
553 if [ $# -eq 1 ] && [ "$1" = "--all" ]; then
554 eval set -- $(git for-each-ref --shell --format="%(refname)" ${anyrefok:+"refs/heads"} "refs/$topbases")
555 outofdateok=1
556 all=1
557 if [ $# -eq 0 ]; then
558 if [ "$quiet" -gt 0 ] && [ -n "$noneok" ]; then
559 exit 0
560 else
561 onlytg=
562 [ -n "$anyrefok" ] || onlytg=" TopGit"
563 die "no$onlytg branches found"
567 [ -n "$refsonly" ] || ensure_ident_available
568 branches=
569 allrefs=
570 extrarefs=
571 tgbranches=
572 tgcount=0
573 othercount=0
574 ignore=
575 newlist=
576 firstprnt=
577 for arg in "$@"; do
578 case "$arg" in "~"?*)
579 [ -z "$firstprnt" ] || die "only one first parent may be specified with ~"
580 firstprnt="$(git rev-parse --verify --quiet "${arg#?}^0" -- 2>/dev/null)" && [ -n "$firstprnt" ] ||
581 die "not a commit-ish: ${arg#?}"
582 esac
583 done
584 while read -r obj typ ref && [ -n "$obj" ] && [ -n "$typ" ]; do
585 [ -n "$ref" ] || [ "$typ" != "missing" ] || die "no such ref: ${obj%???}"
586 case " $ignore " in *" $ref "*) continue; esac
587 if [ "$typ" != "commit" ] && [ "$typ" != "tag" ]; then
588 [ -n "$anyrefok" ] || die "not a committish (is a '$typ') ref: $ref"
589 [ "$quiet" -ge 2 ] || warn "ignoring non-committish (is a '$typ') ref: $ref"
590 ignore="${ignore:+$ignore }$ref"
591 continue
593 case " $newlist " in *" $ref "*);;*)
594 newlist="${newlist:+$newlist }$ref"
595 esac
596 if [ "$typ" = "tag" ]; then
597 [ "$quiet" -ge 2 ] || warn "storing as lightweight tag instead of 'tag' object: $ref"
598 ignore="${ignore:+$ignore }$ref"
600 done <<-EOT
602 printf '%s\n' "$@" | sed 's/^~//; s/^\(.*\)$/\1^{} \1/'
603 printf '%s\n' "$@" | sed 's/^~//; s/^\(.*\)$/\1 \1/'
605 git cat-file --batch-check='%(objectname) %(objecttype) %(rest)' 2>/dev/null ||
608 set -- $newlist
609 errtemp="$(get_temp errs)"
610 for b; do
611 sfn="$b"
612 if [ -z "$all" ]; then
613 [ "${b#-}" = "$b" ] || die "branch names starting with '-' must be fully qualified: $b"
614 sfn="$(git rev-parse --revs-only --symbolic-full-name "$b" -- 2>"$errtemp")" || :
615 [ -n "$sfn" ] || ! [ -s "$errtemp" ] || sfn="$(git rev-parse --revs-only --symbolic-full-name "refs/heads/$b" -- 2>/dev/null)" || :
617 [ -n "$sfn" ] || {
618 if [ -z "$anyrefok" ] || [ -s "$errtemp" ]; then
619 ! [ -s "$errtemp" ] || tail -n 1 <"$errtemp" >&2
620 die "invalid symbolic ref name: $b"
622 fullhash="$(git rev-parse --verify --quiet "$b" --)" || die "no such ref: $b"
623 case " $extrarefs " in *" $b "*);;*)
624 [ "$quiet" -ge 2 ] || warn "including non-symbolic ref only in parents calculation: $b"
625 extrarefs="${extrarefs:+$extrarefs }$fullhash"
626 esac
627 continue
629 case "$sfn" in
630 refs/"$topbases"/*)
631 added=
632 tgish=1
633 ref_exists "refs/heads/${sfn#refs/$topbases/}" || tgish=
634 [ -n "$anyrefok" ] || [ -n "$tgish" ] || [ "$quiet" -ge 2 ] ||
635 warn "including TopGit base that's missing its head: $sfn"
636 case " $allrefs " in *" $sfn "*);;*)
637 allrefs="${allrefs:+$allrefs }$sfn"
638 esac
639 case " $branches " in *" ${sfn#refs/$topbases/} "*);;*)
640 branches="${branches:+$branches }${sfn#refs/$topbases/}"
641 added=1
642 esac
643 if [ -n "$tgish" ]; then
644 case " $allrefs " in *" refs/heads/${sfn#refs/$topbases/} "*);;*)
645 allrefs="${allrefs:+$allrefs }refs/heads/${sfn#refs/$topbases/}"
646 esac
647 case " $tgbranches " in *" ${sfn#refs/$topbases/} "*);;*)
648 tgbranches="${tgbranches:+$tgbranches }${sfn#refs/$topbases/}"
649 added=1
650 esac
651 [ -z "$added" ] || tgcount=$(( $tgcount + 1 ))
652 else
653 [ -z "$added" ] || othercount=$(( $othercount + 1 ))
656 refs/heads/*)
657 added=
658 tgish=1
659 ref_exists "refs/$topbases/${sfn#refs/heads/}" || tgish=
660 [ -n "$anyrefok" ] || [ -n "$tgish" ] ||
661 die "not a TopGit branch: ${sfn#refs/heads/} (use --allow-any option)"
662 case " $allrefs " in *" $b "*);;*)
663 allrefs="${allrefs:+$allrefs }$sfn"
664 esac
665 case " $branches " in *" ${sfn#refs/heads/} "*);;*)
666 branches="${branches:+$branches }${sfn#refs/heads/}"
667 added=1
668 esac
669 if [ -n "$tgish" ]; then
670 case " $allrefs " in *" refs/$topbases/${sfn#refs/heads/} "*);;*)
671 allrefs="${allrefs:+$allrefs }refs/$topbases/${sfn#refs/heads/}"
672 esac
673 case " $tgbranches " in *" ${sfn#refs/heads/} "*);;*)
674 tgbranches="${tgbranches:+$tgbranches }${sfn#refs/heads/}"
675 added=1
676 esac
677 [ -z "$added" ] || tgcount=$(( $tgcount + 1 ))
678 else
679 [ -z "$added" ] || othercount=$(( $othercount + 1 ))
683 [ -n "$anyrefok" ] || die "refusing to include without --allow-any: $sfn"
684 case " $allrefs " in *" $sfn "*);;*)
685 allrefs="${allrefs:+$allrefs }$sfn"
686 esac
687 case " $branches " in *" ${sfn#refs/} "*);;*)
688 branches="${branches:+$branches }${sfn#refs/}"
689 othercount=$(( $othercount + 1 ))
690 esac
692 esac
693 done
695 [ -n "$force" ] ||
696 ! git rev-parse --verify --quiet "$refname" -- >/dev/null ||
697 die "$reftype '$tagname' already exists"
699 desc="tg branch"
700 descpl="tg branches"
701 if [ $othercount -gt 0 ]; then
702 if [ $tgcount -eq 0 ]; then
703 desc="ref"
704 descpl="refs"
705 else
706 descpl="$descpl and refs"
710 get_dep() {
711 case " $seen_deps " in *" $_dep "*) return 0; esac
712 seen_deps="${seen_deps:+$seen_deps }$_dep"
713 printf 'refs/heads/%s\n' "$_dep"
714 [ -z "$_dep_is_tgish" ] || printf 'refs/%s/%s\n' "$topbases" "$_dep"
717 get_deps_internal()
719 no_remotes=1
720 recurse_deps_exclude=
721 for _b; do
722 case " $recurse_deps_exclude " in *" $_b "*) continue; esac
723 seen_deps=
724 _dep="$_b"; _dep_is_tgish=1; get_dep
725 recurse_deps get_dep "$_b"
726 recurse_deps_exclude="$recurse_deps_exclude $seen_deps"
727 done
730 get_deps()
732 get_deps_internal "$@" | sort -u
735 out_of_date=
736 if [ -n "$outofdateok" ]; then
737 if [ -n "$tgbranches" ]; then
738 while read -r dep && [ -n "$dep" ]; do
739 case " $allrefs " in *" $dep "*);;*)
740 ! ref_exists "$dep" ||
741 allrefs="${allrefs:+$allrefs }$dep"
742 esac
743 done <<-EOT
744 $(get_deps $tgbranches)
747 else
748 for b in $tgbranches; do
749 if ! needs_update "$b" >/dev/null; then
750 out_of_date=1
751 echo "branch not up-to-date: $b" >&2
753 done
755 [ -z "$out_of_date" ] || die "all branches to be tagged must be up-to-date"
757 get_refs()
759 printf '%s\n' '-----BEGIN TOPGIT REFS-----'
761 printf '%s\n' $allrefs
762 [ -n "$outofdateok" ] || get_deps $tgbranches
763 } | sort -u | sed 's/^\(.*\)$/\1^0 \1/' |
764 git cat-file --batch-check='%(objectname) %(rest)' 2>/dev/null |
765 grep -v ' missing$' || :
766 printf '%s\n' '-----END TOPGIT REFS-----'
769 if [ -n "$refsonly" ]; then
770 get_refs
771 exit 0
774 stripcomments=
775 if [ -n "$msgfile" ]; then
776 if [ "$msgfile" = "-" ]; then
777 git stripspace >"$git_dir/TAG_EDITMSG"
778 else
779 git stripspace <"$msgfile" >"$git_dir/TAG_EDITMSG"
781 elif [ -n "$msg" ]; then
782 printf '%s\n' "$msg" | git stripspace >"$git_dir/TAG_EDITMSG"
783 else
784 case "$branches" in
785 *" "*)
786 if [ ${#branches} -le 60 ]; then
787 printf '%s\n' "tag $descpl $branches"
788 printf '%s\n' "$updmsg"
789 else
790 printf '%s\n' "tag $(( $(printf '%s' "$branches" | wc -w) )) $descpl" ""
791 for b in $branches; do
792 printf '%s\n' "$b"
793 done
797 printf '%s\n' "tag $desc $branches"
799 esac | git stripspace >"$git_dir/TAG_EDITMSG"
800 if [ -z "$noedit" ]; then
802 cat <<EOT
804 # Please enter a message for tg tag:
805 # $tagname
806 # Lines starting with '#' will be ignored.
808 # $descpl to be tagged:
811 for b in $branches; do
812 printf '%s\n' "# $b"
813 done
814 } >>"$git_dir/TAG_EDITMSG"
815 stripcomments=1
816 run_editor "$git_dir/TAG_EDITMSG" ||
817 die "there was a problem with the editor '$tg_editor'"
820 git -c core.commentchar='#' stripspace ${stripcomments:+--strip-comments} \
821 <"$git_dir/TAG_EDITMSG" >"$git_dir/TGTAG_FINALMSG"
822 [ -s "$git_dir/TGTAG_FINALMSG" ] || die "no tag message?"
823 echo "" >>"$git_dir/TGTAG_FINALMSG"
824 get_refs >>"$git_dir/TGTAG_FINALMSG"
826 v_count_args() { eval "$1="'$(( $# - 1 ))'; }
828 tagtarget=
829 case "$allrefs${extrarefs:+ $extrarefs}" in
830 *" "*)
831 parents="$(git merge-base --independent \
832 $(printf '%s^0 ' $allrefs $extrarefs))" ||
833 die "failed: git merge-base --independent"
836 if [ -n "$firstprnt" ]; then
837 parents="$(git rev-parse --quiet --verify "$allrefs^0" --)" ||
838 die "failed: git rev-parse $allrefs^0"
839 else
840 parents="$allrefs^0"
843 esac
844 if [ -n "$firstprnt" ]; then
845 oldparents="$parents"
846 parents="$firstprnt"
847 for acmt in $oldparents; do
848 [ "$acmt" = "$firstprnt" ] || parents="$parents $acmt"
849 done
850 unset oldparents
852 v_count_args pcnt $parents
853 if [ $pcnt -eq 1 ]; then
854 tagtarget="$parents"
855 [ -z "$treeish" ] ||
856 [ "$(git rev-parse --quiet --verify "$tagtarget^{tree}" --)" = "$treeish" ] ||
857 tagtarget=
859 if [ -z "$tagtarget" ]; then
860 tagtree="${treeish:-$firstprnt}"
861 [ -n "$tagtree" ] || tagtree="$(git mktree </dev/null)"
862 tagtarget="$(printf '%s\n' "tg tag branch consolidation" "" $branches |
863 git commit-tree $tagtree^{tree} $(printf -- '-p %s ' $parents))"
866 init_reflog "$refname"
867 if [ "$reftype" = "tag" ] && [ -n "$signed" ]; then
868 [ "$quiet" -eq 0 ] || exec >/dev/null
869 git tag -F "$git_dir/TGTAG_FINALMSG" ${signed:+-s} ${force:+-f} \
870 ${keyid:+-u} ${keyid} "$tagname" "$tagtarget"
871 else
872 obj="$(git rev-parse --verify --quiet "$tagtarget" --)" ||
873 die "invalid object name: $tagtarget"
874 typ="$(git cat-file -t "$tagtarget" 2>/dev/null)" ||
875 die "invalid object name: $tagtarget"
876 id="$(git var GIT_COMMITTER_IDENT 2>/dev/null)" ||
877 die "could not get GIT_COMMITTER_IDENT"
878 newtag="$({
879 printf '%s\n' "object $obj" "type $typ" "tag $tagname" \
880 "tagger $id" ""
881 cat "$git_dir/TGTAG_FINALMSG"
882 } | git mktag)" || die "git mktag failed"
883 old="$(git rev-parse --verify --short --quiet "$refname" --)" || :
884 updmsg=
885 case "$branches" in
886 *" "*)
887 if [ ${#branches} -le 100 ]; then
888 updmsg="$(printf '%s\n' "tgtag: $branches")"
889 else
890 updmsg="$(printf '%s\n' "tgtag: $(( $(printf '%s' "$branches" | wc -w) )) ${descpl#tg }")"
894 updmsg="$(printf '%s\n' "tgtag: $branches")"
896 esac
897 [ "$refname" != "TG_STASH" ] || ! [ -s "$git_dir/TG_STASH" ] || mv -f "$git_dir/TG_STASH" "$git_dir/ORIG_TG_STASH" >/dev/null 2>&1 || :
898 git update-ref -m "$updmsg" "$refname" "$newtag"
899 [ -z "$old" ] || [ "$quiet" -gt 0 ] || printf "Updated $reftype '%s' (was %s)\n" "$tagname" "$old"
901 rm -f "$git_dir/TAG_EDITMSG" "$git_dir/TGTAG_FINALMSG"