tg-tag.sh: carefully resolve full ref names
[topgit/pro.git] / tg-tag.sh
blob5cd1b1c98521b867f0d6b8bcbea427712b34b3e6
1 #!/bin/sh
2 # TopGit tag command
3 # Copyright (C) 2015,2017,2018 Kyle J. McKay <mackyle@gmail.com>
4 # All rights reserved.
5 # GPLv2
7 USAGE="\
8 Usage: ${tgname:-tg} [...] tag [-s | -u <key-id>] [-f] [-q] [--no-edit] [-m <msg> | -F <file>] [--tree <treeish>] (<tagname> | --refs) [<branch>...]
9 Or: ${tgname:-tg} [...] tag (-g | --reflog) [--reflog-message | --commit-message] [--no-type] [-n <number> | -number] [<tagname>]
10 Or: ${tgname:-tg} [...] tag (--clear | --delete) <tagname>
11 Or: ${tgname:-tg} [...] tag --drop <tagname>@{n}"
13 usage()
15 if [ "${1:-0}" != 0 ]; then
16 printf '%s\n' "$USAGE" >&2
17 else
18 printf '%s\n' "$USAGE"
20 exit ${1:-0}
23 ## Parse options
25 signed=
26 keyid=
27 force=
28 msg=
29 msgfile=
30 noedit=
31 defnoedit=
32 refsonly=
33 maxcount=
34 reflog=
35 outofdateok=
36 anyrefok=
37 defbranch=HEAD
38 stash=
39 anonymous=
40 reflogmsg=
41 notype=
42 setreflogmsg=
43 quiet=0
44 noneok=
45 clear=
46 delete=
47 drop=
48 anonymous=
49 treeish=
51 is_numeric()
53 [ -n "$1" ] || return 1
54 while [ -n "$1" ]; do
55 case "$1" in
56 [0-9]*)
57 set -- "${1#?}";;
59 break;;
60 esac
61 done
62 [ -z "$1" ]
65 while [ $# -gt 0 ]; do case "$1" in
66 -h|--help)
67 usage
69 -q|--quiet)
70 quiet=$(( $quiet + 1 ))
72 --none-ok)
73 noneok=1
75 --clear)
76 clear=1
78 --delete)
79 delete=1
81 --drop)
82 drop=1
84 -g|--reflog|--walk-reflogs)
85 reflog=1
87 --reflog-message)
88 reflogmsg=1
89 setreflogmsg=1
91 --no-reflog-message|--commit-message)
92 reflogmsg=
93 setreflogmsg=1
95 --no-type)
96 notype=1
98 -s|--sign)
99 signed=1
101 -u|--local-user|--local-user=*)
102 case "$1" in --local-user=*)
103 x="$1"
104 shift
105 set -- --local-user "${x#--local-user=}" "$@"
106 esac
107 if [ $# -lt 2 ]; then
108 echo "The $1 option requires an argument" >&2
109 usage 1
111 shift
112 keyid="$1"
114 -f|--force)
115 force=1
117 --no-edit)
118 noedit=1
120 --edit)
121 noedit=0
123 --allow-outdated)
124 outofdateok=1
126 --allow-any)
127 anyrefok=1
129 --tree|--tree=*)
130 case "$1" in --tree=*)
131 x="$1"
132 shift
133 set -- --tree "${x#--tree=}" "$@"
134 esac
135 if [ $# -lt 2 ]; then
136 echo "The $1 option requires an argument" >&2
137 usage 1
139 shift
140 treeish="$(git rev-parse --quiet --verify "$1^{tree}" --)" || {
141 echo "Not a valid treeish: $1" >&2
142 exit 1
145 --refs|--refs-only)
146 refsonly=1
148 -m|--message|--message=*)
149 case "$1" in --message=*)
150 x="$1"
151 shift
152 set -- --message "${x#--message=}" "$@"
153 esac
154 if [ $# -lt 2 ]; then
155 echo "The $1 option requires an argument" >&2
156 usage 1
158 shift
159 msg="$1"
161 -F|--file|--file=*)
162 case "$1" in --file=*)
163 x="$1"
164 shift
165 set -- --file "${x#--file=}" "$@"
166 esac
167 if [ $# -lt 2 ]; then
168 echo "The $1 option requires an argument" >&2
169 usage 1
171 shift
172 msgfile="$1"
174 -n|--max-count|--max-count=*|-[1-9]*)
175 case "$1" in --max-count=*)
176 x="$1"
177 shift
178 set -- --max-count "${x#--max-count=}" "$@"
179 esac
180 case "$1" in -[1-9]*)
181 x="${1#-}"
182 shift
183 set -- -n "$x" "$@"
184 esac
185 if [ $# -lt 2 ]; then
186 echo "The $1 option requires an argument" >&2
187 usage 1
189 shift
190 maxcount="$1"
193 shift
194 break
196 --all)
197 break
199 --stash|--stash"@{"*"}")
200 if [ -n "$reflog" ]; then
201 case "$2" in -[1-9]*)
202 x1="$1"
203 x2="$2"
204 shift
205 shift
206 set -- "$x2" "$x1" "$@"
207 continue
208 esac
210 stash=1
211 defbranch=--all
212 break
214 --anonymous)
215 anonymous=1
216 defbranch=--all
217 quiet=2
218 break
220 -?*)
221 echo "Unknown option: $1" >&2
222 usage 1
225 if [ -n "$reflog" ]; then
226 case "$2" in -[1-9]*)
227 x1="$1"
228 x2="$2"
229 shift
230 shift
231 set -- "$x2" "$x1" "$@"
232 continue
233 esac
235 break
237 esac; shift; done
239 [ "$stash$anonymous" != "11" ] || usage 1
240 [ -z "$stash$anonymous" ] || [ -n "$reflog$drop$clear$delete" ] || { outofdateok=1; force=1; defnoedit=1; }
241 [ -n "$noedit" ] || noedit="$defnoedit"
242 [ "$noedit" != "0" ] || noedit=
243 [ -z "$reflog" ] || [ -z "$drop$clear$delete$signed$keyid$force$msg$msgfile$noedit$treeish$refsonly$outofdateok" ] || usage 1
244 [ -n "$reflog" ] || [ -z "$setreflogmsg$notype$maxcount" ] || usage 1
245 [ -z "$drop$clear$delete" ] || [ -z "$setreflogmsg$notype$maxcount$signed$keyid$force$msg$msgfile$noedit$treeish$refsonly$outofdateok" ] || usage 1
246 [ -z "$reflog$drop$clear$delete" ] || [ "$reflog$drop$clear$delete" = "1" ] || usage 1
247 [ -z "$maxcount" ] || is_numeric "$maxcount" || die "invalid count: $maxcount"
248 [ -z "$maxcount" ] || [ $maxcount -gt 0 ] || die "invalid count: $maxcount"
249 [ -z "$msg" ] || [ -z "$msgfile" ] || die "only one -F or -m option is allowed."
250 [ -z "$refsonly" ] || set -- refs..only "$@"
251 [ $# -gt 0 ] || [ -z "$reflog" ] || set -- --stash
252 [ -n "$1" ] || { echo "Tag name required" >&2; usage 1; }
253 tagname="$1"
254 shift
255 [ "$tagname" != "--stash" ] || tagname=refs/tgstash
256 [ "$tagname" != "--anonymous" ] || tagname=TG_STASH
257 case "$tagname" in --stash"@{"*"}")
258 strip="${tagname#--stash??}"
259 strip="${strip%?}"
260 tagname="refs/tgstash@{$strip}"
261 esac
262 refname="$tagname"
263 sfx=
264 sfxis0=
265 case "$refname" in [!@]*"@{"*"}")
266 _pfx="@{"
267 _refonly="${refname%%"$_pfx"*}"
268 sfx="${refname#"$_refonly"}"
269 refname="$_refonly"
270 _numonly="${sfx#??}"
271 _numonly="${_numonly%?}"
272 [ "${_numonly#[0-9]}" != "$_numonly" ] && [ "${_numonly#*[!0-9]}" = "$_numonly" ] || die "invalid suffix: \"$sfx\""
273 if [ "${_numonly#*[!0]}" = "$_numonly" ]; then
274 # transform @{0000000} etc. into @{0}
275 sfx="@{0}"
276 sfxis0=1
277 else
278 # remove any leading zeros
279 _ld0="${_numonly%%[!0]*}"
280 [ -z "$_ld0" ] || _numonly="${_numonly#$_ld0}"
281 sfx="@{$_numonly}"
283 esac
285 v_resolve_full_name() {
286 # `git rev-parse --revs-only --symbolic-full-name` should do this
287 # but its behavior is inadequate in that it will not produce a result
288 # if the input is ambiguous even though `git rev-parse --verify` still
289 # will in that case. Nasty.
290 # Besides, we really don't want to follow symbolic refs anyway and
291 # we would need to do that ourselves because `git rev-parse` does not
292 # have a `--no-deref` option like `git update-ref` does. Ugly.
294 eval "$1="
295 _normref="$(git check-ref-format --normalize --allow-onelevel "$2" 2>/dev/null)" && [ -n "$_normref" ] || return 1
296 eval "$1="'"$_normref"'
297 git rev-parse --verify --quiet "$_normref" -- >/dev/null 2>&1 || return 0
298 case "$_normref" in refs/?*) return 0; esac
299 _found=
300 _rsuffix=
301 # see `git help revisions` for this DWIM list of interpretations
302 for _rprefix in "refs" "refs/tags" "refs/heads" "refs/remotes"; do
303 ! git rev-parse --verify --quiet "$_rprefix/$_normref" -- >/dev/null 2>&1 || { _found=1; break; }
304 done
305 if [ -z "$_found" ]; then
306 _rsuffix="/HEAD"
307 ! git rev-parse --verify --quiet "$_rprefix/$_normref$_rsuffix" -- >/dev/null 2>&1 || _found=1
309 [ -z "$_found" ] || eval "$1="'"$_rprefix/$_normref$_rsuffix"'
310 return 0
313 case "$refname" in [Hh][Ee][Aa][Dd]|"@") refname="HEAD"; esac
314 case "$refname" in [Tt][Gg]_[Ss][Tt][Aa][Ss][Hh]) refname="TG_STASH"; esac
315 case "$refname" in HEAD|TG_STASH|refs/*);;*)
316 if v_resolve_full_name reftest "$refname" && [ -n "$reftest" ]; then
317 if [ -n "$reflog$drop$clear$delete" ]; then
318 refname="$reftest"
319 else
320 case "$reftest" in
321 refs/tags/*|refs/tgstash)
322 refname="$reftest"
325 refname="refs/tags/$refname"
326 esac
328 else
329 refname="refs/tags/$refname"
331 esac
332 refname="$refname$sfx"
333 reftype=tag
334 case "$refname" in refs/tags/*) tagname="${refname#refs/tags/}";; *) reftype=ref; tagname="$refname"; esac
335 logbase="$git_common_dir"
336 [ "${refname%$sfx}" != "HEAD" ] || logbase="$git_dir"
337 [ -z "$reflog$drop$clear$delete" ] || [ $# -eq 0 ] || usage 1
338 if [ -n "$drop$clear$delete" ]; then
339 if [ -n "$sfx" ]; then
340 [ -z "$clear$delete" ] || die "invalid ref name ($sfx suffix not allowed, try --drop): $refname"
341 else
342 [ -z "$drop" ] || die "invalid reflog entry name (@{n} suffix required): $refname"
344 old="$(git rev-parse --verify --quiet --short "${refname%$sfx}" --)" || die "no such ref: ${refname%$sfx}"
345 if [ -n "$delete" ]; then
346 case "$refname" in [Hh][Ee][Aa][Dd])
347 extra=
348 ! symref="$(git symbolic-ref -q --short HEAD 2>/dev/null)" ||
349 extra=" (did you mean to delete \"$symref\"?)"
350 die "HEAD may not be deleted$extra"
351 esac
352 git update-ref --no-deref -d "$refname" || die "git update-ref --no-deref -d failed"
353 printf "Deleted $reftype '%s' (was %s)\n" "$tagname" "$old"
354 exit 0
355 elif [ -n "$clear" ]; then
356 [ -f "$logbase/logs/$refname" ] || die "no reflog found for: $refname"
357 [ -s "$logbase/logs/$refname" ] || die "empty reflog found for: $refname"
358 cp -p "$logbase/logs/$refname" "$logbase/logs/$refname^-+" || die "cp failed"
359 awk '
360 NF >= 5 { line[1] = $0 }
361 END {
362 if (1 in line) {
363 if (match(line[1], /^[0-9a-fA-F]+ /)) {
364 old = substr(line[1], 1, RLENGTH - 1)
365 rest = substr(line[1], RLENGTH)
366 gsub(/[0-9a-fA-F]/, "0", old)
367 line[1] = old rest
369 print line[1]
372 ' <"$logbase/logs/$refname^-+" >"$logbase/logs/$refname" || die "reflog clear failed"
373 rm -f "$logbase/logs/$refname^-+"
374 printf "Cleared $reftype '%s' reflog to single @{0} entry\n" "$tagname"
375 exit 0
376 else
377 old="$(git rev-parse --verify --short "$refname" --)" || exit 1
378 [ -z "$sfxis0" ] || ! git symbolic-ref -q "${refname%$sfx}" -- >/dev/null 2>&1 || sfxis0=
379 if [ -n "$sfxis0" ]; then
380 [ -f "$logbase/logs/${refname%$sfx}" ] || die "no reflog found for: ${refname%$sfx}"
381 [ -s "$logbase/logs/${refname%$sfx}" ] || die "empty reflog found for: ${refname%$sfx}"
382 # make sure @{1} is valid via pseudo stale-fix before using --updateref
383 cnt="$(( $(wc -l <"$logbase/logs/${refname%$sfx}") ))"
384 lastcnt=
385 at1=
386 while
387 # avoid using --updateref if @{0} is the only entry (i.e. less than 2 lines in log)
388 [ $cnt -ge 2 ] && [ "$cnt" != "$lastcnt" ] &&
389 at1="$(git rev-parse --verify --quiet "${refname%$sfx}@{1}" -- 2>/dev/null)" &&
390 [ -n "$at1" ] &&
391 ! git rev-list --no-walk --objects "$at1" -- >/dev/null 2>&1
393 # poor man's --stale-fix that's faster and actually works reliably
394 git reflog delete --rewrite "${refname%$sfx}@{1}" >/dev/null 2>&1 ||
395 die "pseudo stale-fix failed for broken ${refname%$sfx}@{1}"
396 lastcnt="$cnt"
397 at1=
398 cnt="$(( $(wc -l <"$logbase/logs/${refname%$sfx}") ))"
399 done
400 # avoid using --updateref if @{0} is the only entry (i.e. less than 2 lines in log)
401 [ -n "$at1" ] && [ $cnt -ge 2 ] || sfxis0=
403 git reflog delete --rewrite ${sfxis0:+--updateref} "$refname" || die "reflog drop failed"
404 if [ -n "$sfxis0" ]; then
405 # check if we need to clean up
406 check="$(git rev-parse --verify --quiet "${refname%$sfx}" --)" || :
407 [ "${check#*[!0]}" != "$check" ] || check= # all 0's or empty is bad
408 # Git versions prior to 2.4.0 might need some clean up
409 [ -n "$check" ] || git update-ref -d "${refname%$sfx}" >/dev/null 2>&1 || :
411 printf "Dropped $reftype '%s' reflog entry (was %s)\n" "$tagname" "$old"
412 exit 0
415 if [ -n "$reflog" ]; then
416 [ "$refname" = "refs/tgstash" ] || [ -n "$setreflogmsg" ] || reflogmsg=1
417 git rev-parse --verify --quiet "$refname" -- >/dev/null ||
418 die "no such ref: $refname"
419 [ -s "$logbase/logs/$refname" ] ||
420 die "no reflog present for $reftype: $tagname"
421 showref="$refname"
422 [ "$refname" = "HEAD" ] || showref="$(git rev-parse --revs-only --abbrev-ref=strict "$refname" --)"
423 hashcolor=
424 resetcolor=
425 if git config --get-colorbool color.tgtag; then
426 metacolor="$(git config --get-color color.tgtag.meta)"
427 [ -n "$metacolor" ] || metacolor="$(git config --get-color color.diff.meta "bold")"
428 hashcolor="$(git config --get-color color.tgtag.commit)"
429 [ -n "$hashcolor" ] || hashcolor="$(git config --get-color color.diff.commit "yellow")"
430 datecolor="$(git config --get-color color.tgtag.date "bold blue")"
431 timecolor="$(git config --get-color color.tgtag.time "green")"
432 resetcolor="$(git config --get-color "" reset)"
434 setup_strftime
435 output()
437 sed 's/[^ ][^ ]* //' <"$logbase/logs/$refname" |
438 awk '{a[i++]=$0} END {for (j=i-1; j>=0;) print a[j--]}' |
439 git cat-file --batch-check='%(objectname) %(objecttype) %(rest)' |
441 stashnum=-1
442 lastdate=
443 while read -r newrev type rest; do
444 stashnum=$(( $stashnum + 1 ))
445 [ "$type" != "missing" ] || continue
446 IFS="$tab" read -r cmmttr msg <<-~EOT~
447 $rest
448 ~EOT~
449 ne="${cmmttr% *}"
450 ne="${ne% *}"
451 es="${cmmttr#$ne}"
452 es="${es% *}"
453 es="${es# }"
454 obj="$(git rev-parse --verify --quiet --short "$newrev" --)"
455 extra=
456 [ "$type" = "tag" ] || [ -n "$notype" ] ||
457 extra="$hashcolor($metacolor$type$resetcolor$hashcolor)$resetcolor "
458 if [ -z "$reflogmsg" ] || [ -z "$msg" ]; then
459 objmsg=
460 if [ "$type" = "tag" ]; then
461 objmsg="$(git cat-file tag "$obj" |
462 sed '1,/^$/d' | sed '/^$/,$d')"
463 elif [ "$type" = "commit" ]; then
464 objmsg="$(git --no-pager log -n 1 --format='format:%s' "$obj" --)"
466 [ -z "$objmsg" ] || msg="$objmsg"
468 read newdate newtime <<-EOT
469 $(strftime "%Y-%m-%d %H:%M:%S" "$es" 2>/dev/null)
471 if [ "$lastdate" != "$newdate" ]; then
472 printf '%s=== %s ===%s\n' "$datecolor" "$newdate" "$resetcolor"
473 lastdate="$newdate"
475 printf '%s %s %s%s@{%s}: %s\n' "$hashcolor$obj$reseutcolor" \
476 "$timecolor$newtime$resetcolor" \
477 "$extra" "$showref" "$stashnum" "$msg"
478 if [ -n "$maxcount" ]; then
479 maxcount=$(( $maxcount - 1 ))
480 [ $maxcount -gt 0 ] || break
482 done
485 page output
486 exit 0
488 [ -z "$signed" ] || [ "$reftype" = "tag" ] || die "signed tags must be under refs/tags"
489 [ $# -gt 0 ] || set -- $defbranch
490 all=
491 if [ $# -eq 1 ] && [ "$1" = "--all" ]; then
492 eval set -- $(git for-each-ref --shell --format="%(refname)" "refs/$topbases")
493 outofdateok=1
494 all=1
495 if [ $# -eq 0 ]; then
496 if [ "$quiet" -gt 0 ] && [ -n "$noneok" ]; then
497 exit 0
498 else
499 die "no TopGit branches found"
503 ensure_ident_available
504 branches=
505 allrefs=
506 extrarefs=
507 tgbranches=
508 tgcount=0
509 othercount=0
510 ignore=
511 newlist=
512 firstprnt=
513 for arg in "$@"; do
514 case "$arg" in "~"?*)
515 [ -z "$firstprnt" ] || die "only one first parent may be specified with ~"
516 firstprnt="$(git rev-parse --verify --quiet "${arg#?}^0" -- 2>/dev/null)" && [ -n "$firstprnt" ] ||
517 die "not a commit-ish: ${arg#?}"
518 esac
519 done
520 while read -r obj typ ref && [ -n "$obj" ] && [ -n "$typ" ]; do
521 [ -n "$ref" ] || [ "$typ" != "missing" ] || die "no such ref: ${obj%???}"
522 case " $ignore " in *" $ref "*) continue; esac
523 if [ "$typ" != "commit" ] && [ "$typ" != "tag" ]; then
524 [ -n "$anyrefok" ] || die "not a committish (is a '$typ') ref: $ref"
525 [ "$quiet" -ge 2 ] || warn "ignoring non-committish (is a '$typ') ref: $ref"
526 ignore="${ignore:+$ignore }$ref"
527 continue
529 case " $newlist " in *" $ref "*);;*)
530 newlist="${newlist:+$newlist }$ref"
531 esac
532 if [ "$typ" = "tag" ]; then
533 [ "$quiet" -ge 2 ] || warn "storing as lightweight tag instead of 'tag' object: $ref"
534 ignore="${ignore:+$ignore }$ref"
536 done <<-EOT
538 printf '%s\n' "$@" | sed 's/^~//; s/^\(.*\)$/\1^{} \1/'
539 printf '%s\n' "$@" | sed 's/^~//; s/^\(.*\)$/\1 \1/'
541 git cat-file --batch-check='%(objectname) %(objecttype) %(rest)' 2>/dev/null ||
544 set -- $newlist
545 for b; do
546 sfn="$b"
547 [ -n "$all" ] ||
548 sfn="$(git rev-parse --revs-only --symbolic-full-name "$b" -- 2>/dev/null)" || :
549 [ -n "$sfn" ] || {
550 [ -n "$anyrefok" ] || die "no such symbolic ref name: $b"
551 fullhash="$(git rev-parse --verify --quiet "$b" --)" || die "no such ref: $b"
552 case " $extrarefs " in *" $b "*);;*)
553 [ "$quiet" -ge 2 ] || warn "including non-symbolic ref only in parents calculation: $b"
554 extrarefs="${extrarefs:+$extrarefs }$fullhash"
555 esac
556 continue
558 case "$sfn" in
559 refs/"$topbases"/*)
560 added=
561 tgish=1
562 ref_exists "refs/heads/${sfn#refs/$topbases/}" || tgish=
563 [ -n "$anyrefok" ] || [ -n "$tgish" ] || [ "$quiet" -ge 2 ] ||
564 warn "including TopGit base that's missing its head: $sfn"
565 case " $allrefs " in *" $sfn "*);;*)
566 allrefs="${allrefs:+$allrefs }$sfn"
567 esac
568 case " $branches " in *" ${sfn#refs/$topbases/} "*);;*)
569 branches="${branches:+$branches }${sfn#refs/$topbases/}"
570 added=1
571 esac
572 if [ -n "$tgish" ]; then
573 case " $allrefs " in *" refs/heads/${sfn#refs/$topbases/} "*);;*)
574 allrefs="${allrefs:+$allrefs }refs/heads/${sfn#refs/$topbases/}"
575 esac
576 case " $tgbranches " in *" ${sfn#refs/$topbases/} "*);;*)
577 tgbranches="${tgbranches:+$tgbranches }${sfn#refs/$topbases/}"
578 added=1
579 esac
580 [ -z "$added" ] || tgcount=$(( $tgcount + 1 ))
581 else
582 [ -z "$added" ] || othercount=$(( $othercount + 1 ))
585 refs/heads/*)
586 added=
587 tgish=1
588 ref_exists "refs/$topbases/${sfn#refs/heads/}" || tgish=
589 [ -n "$anyrefok" ] || [ -n "$tgish" ] ||
590 die "not a TopGit branch: ${sfn#refs/heads/} (use --allow-any option)"
591 case " $allrefs " in *" $b "*);;*)
592 allrefs="${allrefs:+$allrefs }$sfn"
593 esac
594 case " $branches " in *" ${sfn#refs/heads/} "*);;*)
595 branches="${branches:+$branches }${sfn#refs/heads/}"
596 added=1
597 esac
598 if [ -n "$tgish" ]; then
599 case " $allrefs " in *" refs/$topbases/${sfn#refs/heads/} "*);;*)
600 allrefs="${allrefs:+$allrefs }refs/$topbases/${sfn#refs/heads/}"
601 esac
602 case " $tgbranches " in *" ${sfn#refs/heads/} "*);;*)
603 tgbranches="${tgbranches:+$tgbranches }${sfn#refs/heads/}"
604 added=1
605 esac
606 [ -z "$added" ] || tgcount=$(( $tgcount + 1 ))
607 else
608 [ -z "$added" ] || othercount=$(( $othercount + 1 ))
612 [ -n "$anyrefok" ] || die "refusing to include without --allow-any: $sfn"
613 case " $allrefs " in *" $sfn "*);;*)
614 allrefs="${allrefs:+$allrefs }$sfn"
615 esac
616 case " $branches " in *" ${sfn#refs/} "*);;*)
617 branches="${branches:+$branches }${sfn#refs/}"
618 othercount=$(( $othercount + 1 ))
619 esac
621 esac
622 done
624 [ -n "$force" ] ||
625 ! git rev-parse --verify --quiet "$refname" -- >/dev/null ||
626 die "$reftype '$tagname' already exists"
628 desc="tg branch"
629 descpl="tg branches"
630 if [ $othercount -gt 0 ]; then
631 if [ $tgcount -eq 0 ]; then
632 desc="ref"
633 descpl="refs"
634 else
635 descpl="$descpl and refs"
639 get_dep() {
640 case " $seen_deps " in *" $_dep "*) return 0; esac
641 seen_deps="${seen_deps:+$seen_deps }$_dep"
642 printf 'refs/heads/%s\n' "$_dep"
643 [ -z "$_dep_is_tgish" ] || printf 'refs/%s/%s\n' "$topbases" "$_dep"
646 get_deps_internal()
648 no_remotes=1
649 recurse_deps_exclude=
650 for _b; do
651 case " $recurse_deps_exclude " in *" $_b "*) continue; esac
652 seen_deps=
653 _dep="$_b"; _dep_is_tgish=1; get_dep
654 recurse_deps get_dep "$_b"
655 recurse_deps_exclude="$recurse_deps_exclude $seen_deps"
656 done
659 get_deps()
661 get_deps_internal "$@" | sort -u
664 out_of_date=
665 if [ -n "$outofdateok" ]; then
666 if [ -n "$tgbranches" ]; then
667 while read -r dep && [ -n "$dep" ]; do
668 case " $allrefs " in *" $dep "*);;*)
669 ! ref_exists "$dep" ||
670 allrefs="${allrefs:+$allrefs }$dep"
671 esac
672 done <<-EOT
673 $(get_deps $tgbranches)
676 else
677 for b in $tgbranches; do
678 if ! needs_update "$b" >/dev/null; then
679 out_of_date=1
680 echo "branch not up-to-date: $b"
682 done
684 [ -z "$out_of_date" ] || die "all branches to be tagged must be up-to-date"
686 get_refs()
688 printf '%s\n' '-----BEGIN TOPGIT REFS-----'
690 printf '%s\n' $allrefs
691 [ -n "$outofdateok" ] || get_deps $tgbranches
692 } | sort -u | sed 's/^\(.*\)$/\1^0 \1/' |
693 git cat-file --batch-check='%(objectname) %(rest)' 2>/dev/null |
694 grep -v ' missing$' || :
695 printf '%s\n' '-----END TOPGIT REFS-----'
698 if [ -n "$refsonly" ]; then
699 get_refs
700 exit 0
703 stripcomments=
704 if [ -n "$msgfile" ]; then
705 if [ "$msgfile" = "-" ]; then
706 git stripspace >"$git_dir/TAG_EDITMSG"
707 else
708 git stripspace <"$msgfile" >"$git_dir/TAG_EDITMSG"
710 elif [ -n "$msg" ]; then
711 printf '%s\n' "$msg" | git stripspace >"$git_dir/TAG_EDITMSG"
712 else
713 case "$branches" in
714 *" "*)
715 if [ ${#branches} -le 60 ]; then
716 printf '%s\n' "tag $descpl $branches"
717 printf '%s\n' "$updmsg"
718 else
719 printf '%s\n' "tag $(( $(printf '%s' "$branches" | wc -w) )) $descpl" ""
720 for b in $branches; do
721 printf '%s\n' "$b"
722 done
726 printf '%s\n' "tag $desc $branches"
728 esac | git stripspace >"$git_dir/TAG_EDITMSG"
729 if [ -z "$noedit" ]; then
731 cat <<EOT
733 # Please enter a message for tg tag:
734 # $tagname
735 # Lines starting with '#' will be ignored.
737 # $descpl to be tagged:
740 for b in $branches; do
741 printf '%s\n' "# $b"
742 done
743 } >>"$git_dir/TAG_EDITMSG"
744 stripcomments=1
745 run_editor "$git_dir/TAG_EDITMSG" ||
746 die "there was a problem with the editor '$tg_editor'"
749 git stripspace ${stripcomments:+ --strip-comments} \
750 <"$git_dir/TAG_EDITMSG" >"$git_dir/TGTAG_FINALMSG"
751 [ -s "$git_dir/TGTAG_FINALMSG" ] || die "no tag message?"
752 echo "" >>"$git_dir/TGTAG_FINALMSG"
753 get_refs >>"$git_dir/TGTAG_FINALMSG"
755 v_count_args() { eval "$1="'$(( $# - 1 ))'; }
757 tagtarget=
758 case "$allrefs${extrarefs:+ $extrarefs}" in
759 *" "*)
760 parents="$(git merge-base --independent \
761 $(printf '%s^0 ' $allrefs $extrarefs))" ||
762 die "failed: git merge-base --independent"
765 if [ -n "$firstprnt" ]; then
766 parents="$(git rev-parse --quiet --verify "$allrefs^0" --)" ||
767 die "failed: git rev-parse $allrefs^0"
768 else
769 parents="$allrefs^0"
772 esac
773 if [ -n "$firstprnt" ]; then
774 oldparents="$parents"
775 parents="$firstprnt"
776 for acmt in $oldparents; do
777 [ "$acmt" = "$firstprnt" ] || parents="$parents $acmt"
778 done
779 unset oldparents
781 v_count_args pcnt $parents
782 if [ $pcnt -eq 1 ]; then
783 tagtarget="$parents"
784 [ -z "$treeish" ] ||
785 [ "$(git rev-parse --quiet --verify "$tagtarget^{tree}" --)" = "$treeish" ] ||
786 tagtarget=
788 if [ -z "$tagtarget" ]; then
789 tagtree="${treeish:-$firstprnt}"
790 [ -n "$tagtree" ] || tagtree="$(git hash-object -t tree -w --stdin </dev/null)"
791 tagtarget="$(printf '%s\n' "tg tag branch consolidation" "" $branches |
792 git commit-tree $tagtree^{tree} $(printf -- '-p %s ' $parents))"
795 init_reflog "$refname"
796 if [ "$reftype" = "tag" ] && [ -n "$signed" ]; then
797 [ "$quiet" -eq 0 ] || exec >/dev/null
798 git tag -F "$git_dir/TGTAG_FINALMSG" ${signed:+-s} ${force:+-f} \
799 ${keyid:+-u} ${keyid} "$tagname" "$tagtarget"
800 else
801 obj="$(git rev-parse --verify --quiet "$tagtarget" --)" ||
802 die "invalid object name: $tagtarget"
803 typ="$(git cat-file -t "$tagtarget" 2>/dev/null)" ||
804 die "invalid object name: $tagtarget"
805 id="$(git var GIT_COMMITTER_IDENT 2>/dev/null)" ||
806 die "could not get GIT_COMMITTER_IDENT"
807 newtag="$({
808 printf '%s\n' "object $obj" "type $typ" "tag $tagname" \
809 "tagger $id" ""
810 cat "$git_dir/TGTAG_FINALMSG"
811 } | git mktag)" || die "git mktag failed"
812 old="$(git rev-parse --verify --short --quiet "$refname" --)" || :
813 updmsg=
814 case "$branches" in
815 *" "*)
816 if [ ${#branches} -le 100 ]; then
817 updmsg="$(printf '%s\n' "tgtag: $branches")"
818 else
819 updmsg="$(printf '%s\n' "tgtag: $(( $(printf '%s' "$branches" | wc -w) )) ${descpl#tg }")"
823 updmsg="$(printf '%s\n' "tgtag: $branches")"
825 esac
826 git update-ref -m "$updmsg" "$refname" "$newtag"
827 [ -z "$old" ] || [ "$quiet" -gt 0 ] || printf "Updated $reftype '%s' (was %s)\n" "$tagname" "$old"
829 rm -f "$git_dir/TAG_EDITMSG" "$git_dir/TGTAG_FINALMSG"