various: use "command" instead of "subcommand"
[topgit/pro.git] / tg-tag.sh
blob6240f31b6cf19c8e22cdbf208aca0029272e49be
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=
50 sawall=
52 is_numeric()
54 [ -n "$1" ] || return 1
55 while [ -n "$1" ]; do
56 case "$1" in
57 [0-9]*)
58 set -- "${1#?}";;
60 break;;
61 esac
62 done
63 [ -z "$1" ]
66 while [ $# -gt 0 ]; do case "$1" in
67 -h|--help)
68 usage
70 -q|--quiet)
71 quiet=$(( $quiet + 1 ))
73 --none-ok)
74 noneok=1
76 --clear)
77 clear=1
79 --delete)
80 delete=1
82 --drop)
83 drop=1
85 -g|--reflog|--walk-reflogs)
86 reflog=1
88 --reflog-message)
89 reflogmsg=1
90 setreflogmsg=1
92 --no-reflog-message|--commit-message)
93 reflogmsg=
94 setreflogmsg=1
96 --no-type)
97 notype=1
99 -s|--sign)
100 signed=1
102 -u|--local-user|--local-user=*)
103 case "$1" in --local-user=*)
104 x="$1"
105 shift
106 set -- --local-user "${x#--local-user=}" "$@"
107 esac
108 if [ $# -lt 2 ]; then
109 echo "The $1 option requires an argument" >&2
110 usage 1
112 shift
113 keyid="$1"
115 -f|--force)
116 force=1
118 --no-edit)
119 noedit=1
121 --edit)
122 noedit=0
124 --allow-outdated)
125 outofdateok=1
127 --allow-any)
128 anyrefok=1
130 --tree|--tree=*)
131 case "$1" in --tree=*)
132 x="$1"
133 shift
134 set -- --tree "${x#--tree=}" "$@"
135 esac
136 if [ $# -lt 2 ]; then
137 echo "The $1 option requires an argument" >&2
138 usage 1
140 shift
141 treeish="$(git rev-parse --quiet --verify "$1^{tree}" --)" || {
142 echo "Not a valid treeish: $1" >&2
143 exit 1
146 --refs|--refs-only)
147 refsonly=1
149 -m|--message|--message=*)
150 case "$1" in --message=*)
151 x="$1"
152 shift
153 set -- --message "${x#--message=}" "$@"
154 esac
155 if [ $# -lt 2 ]; then
156 echo "The $1 option requires an argument" >&2
157 usage 1
159 shift
160 msg="$1"
162 -F|--file|--file=*)
163 case "$1" in --file=*)
164 x="$1"
165 shift
166 set -- --file "${x#--file=}" "$@"
167 esac
168 if [ $# -lt 2 ]; then
169 echo "The $1 option requires an argument" >&2
170 usage 1
172 shift
173 msgfile="$1"
175 -n|--max-count|--max-count=*|-[1-9]*)
176 case "$1" in --max-count=*)
177 x="$1"
178 shift
179 set -- --max-count "${x#--max-count=}" "$@"
180 esac
181 case "$1" in -[1-9]*)
182 x="${1#-}"
183 shift
184 set -- -n "$x" "$@"
185 esac
186 if [ $# -lt 2 ]; then
187 echo "The $1 option requires an argument" >&2
188 usage 1
190 shift
191 maxcount="$1"
194 shift
195 break
197 --all)
198 sawall=1
199 defbranch=--all
201 --stash|--stash"@{"*"}")
202 if [ -n "$reflog" ]; then
203 case "$2" in -[1-9]*)
204 x1="$1"
205 x2="$2"
206 shift
207 shift
208 set -- "$x2" "$x1" "$@"
209 continue
210 esac
211 else
212 if [ "$2" = "--all" ]; then
213 x1="$1"
214 shift
215 shift
216 set -- "$x1" "$@"
217 sawall=1
218 elif [ "$2" = "--" ]; then
219 x1="$1"
220 shift
221 shift
222 set -- "$x1" "$@"
225 stash=1
226 defbranch=--all
227 break
229 --anonymous)
230 anonymous=1
231 defbranch=--all
232 quiet=2
233 break
235 -?*)
236 echo "Unknown option: $1" >&2
237 usage 1
240 if [ -n "$reflog" ]; then
241 case "$2" in -[1-9]*)
242 x1="$1"
243 x2="$2"
244 shift
245 shift
246 set -- "$x2" "$x1" "$@"
247 continue
248 esac
250 break
252 esac; shift; done
254 [ "$stash$anonymous" != "11" ] || usage 1
255 [ -z "$stash$anonymous" ] || [ -n "$reflog$drop$clear$delete" ] || { outofdateok=1; force=1; defnoedit=1; }
256 [ -z "$sawall" ] || [ $# -gt 0 ] || { outofdateok=1; force=1; defnoedit=1; }
257 [ -n "$noedit" ] || noedit="$defnoedit"
258 [ "$noedit" != "0" ] || noedit=
259 [ -z "$reflog" ] || [ -z "$drop$clear$delete$signed$keyid$force$msg$msgfile$noedit$treeish$refsonly$outofdateok$sawall" ] || usage 1
260 [ -n "$reflog" ] || [ -z "$setreflogmsg$notype$maxcount" ] || usage 1
261 [ -z "$drop$clear$delete" ] || [ -z "$setreflogmsg$notype$maxcount$signed$keyid$force$msg$msgfile$noedit$treeish$refsonly$outofdateok$sawall" ] || usage 1
262 [ -z "$reflog$drop$clear$delete" ] || [ "$reflog$drop$clear$delete" = "1" ] || usage 1
263 [ -z "$maxcount" ] || is_numeric "$maxcount" || die "invalid count: $maxcount"
264 [ -z "$maxcount" ] || [ $maxcount -gt 0 ] || die "invalid count: $maxcount"
265 [ -z "$msg" ] || [ -z "$msgfile" ] || die "only one -F or -m option is allowed."
266 [ -z "$refsonly" ] || set -- refs..only "$@"
267 [ $# -gt 0 ] || [ -z "$reflog$sawall" ] || set -- --stash
268 [ -n "$1" ] || { echo "Tag name required" >&2; usage 1; }
269 tagname="$1"
270 shift
271 [ -z "$sawall" ] || [ $# -eq 0 ] || die "branch names not allowed with --all"
272 [ "$tagname" != "--stash" ] || tagname=refs/tgstash
273 [ "$tagname" != "--anonymous" ] || tagname=TG_STASH
274 case "$tagname" in --stash"@{"*"}")
275 strip="${tagname#--stash??}"
276 strip="${strip%?}"
277 tagname="refs/tgstash@{$strip}"
278 esac
279 refname="$tagname"
280 sfx=
281 sfxis0=
282 case "$refname" in [!@]*"@{"*"}")
283 _pfx="@{"
284 _refonly="${refname%%"$_pfx"*}"
285 sfx="${refname#"$_refonly"}"
286 refname="$_refonly"
287 _numonly="${sfx#??}"
288 _numonly="${_numonly%?}"
289 [ "${_numonly#[0-9]}" != "$_numonly" ] && [ "${_numonly#*[!0-9]}" = "$_numonly" ] || die "invalid suffix: \"$sfx\""
290 if [ "${_numonly#*[!0]}" = "$_numonly" ]; then
291 # transform @{0000000} etc. into @{0}
292 sfx="@{0}"
293 sfxis0=1
294 else
295 # remove any leading zeros
296 _ld0="${_numonly%%[!0]*}"
297 [ -z "$_ld0" ] || _numonly="${_numonly#$_ld0}"
298 sfx="@{$_numonly}"
300 esac
302 v_resolve_full_name() {
303 # `git rev-parse --revs-only --symbolic-full-name` should do this
304 # but its behavior is inadequate in that it will not produce a result
305 # if the input is ambiguous even though `git rev-parse --verify` still
306 # will in that case. Nasty.
307 # Besides, we really don't want to follow symbolic refs anyway and
308 # we would need to do that ourselves because `git rev-parse` does not
309 # have a `--no-deref` option like `git update-ref` does. Ugly.
311 eval "$1="
312 _normref="$(git check-ref-format --normalize --allow-onelevel "$2" 2>/dev/null)" && [ -n "$_normref" ] || return 1
313 eval "$1="'"$_normref"'
314 git rev-parse --verify --quiet "$_normref" -- >/dev/null 2>&1 || return 0
315 case "$_normref" in refs/?*) return 0; esac
316 _found=
317 _rsuffix=
318 # see `git help revisions` for this DWIM list of interpretations
319 for _rprefix in "refs" "refs/tags" "refs/heads" "refs/remotes"; do
320 ! git rev-parse --verify --quiet "$_rprefix/$_normref" -- >/dev/null 2>&1 || { _found=1; break; }
321 done
322 if [ -z "$_found" ]; then
323 _rsuffix="/HEAD"
324 ! git rev-parse --verify --quiet "$_rprefix/$_normref$_rsuffix" -- >/dev/null 2>&1 || _found=1
326 [ -z "$_found" ] || eval "$1="'"$_rprefix/$_normref$_rsuffix"'
327 return 0
330 case "$refname" in [Hh][Ee][Aa][Dd]|"@") refname="HEAD"; esac
331 case "$refname" in [Tt][Gg]_[Ss][Tt][Aa][Ss][Hh]) refname="TG_STASH"; esac
332 case "$refname" in HEAD|TG_STASH|refs/*);;*)
333 if v_resolve_full_name reftest "$refname" && [ -n "$reftest" ]; then
334 if [ -n "$reflog$drop$clear$delete" ]; then
335 refname="$reftest"
336 else
337 case "$reftest" in
338 refs/tags/*|refs/tgstash)
339 refname="$reftest"
342 refname="refs/tags/$refname"
343 esac
345 else
346 refname="refs/tags/$refname"
348 esac
349 refname="$refname$sfx"
350 reftype=tag
351 case "$refname" in refs/tags/*) tagname="${refname#refs/tags/}";; *) reftype=ref; tagname="$refname"; esac
352 logbase="$git_common_dir"
353 [ "${refname%$sfx}" != "HEAD" ] || logbase="$git_dir"
354 [ -z "$reflog$drop$clear$delete" ] || [ $# -eq 0 ] || usage 1
355 if [ -n "$drop$clear$delete" ]; then
356 if [ -n "$sfx" ]; then
357 [ -z "$clear$delete" ] || die "invalid ref name ($sfx suffix not allowed, try --drop): $refname"
358 else
359 [ -z "$drop" ] || die "invalid reflog entry name (@{n} suffix required): $refname"
361 old="$(git rev-parse --verify --quiet --short "${refname%$sfx}" --)" || die "no such ref: ${refname%$sfx}"
362 if [ -n "$delete" ]; then
363 case "$refname" in [Hh][Ee][Aa][Dd])
364 extra=
365 ! symref="$(git symbolic-ref -q --short HEAD 2>/dev/null)" ||
366 extra=" (did you mean to delete \"$symref\"?)"
367 die "HEAD may not be deleted$extra"
368 esac
370 srh="$(git symbolic-ref --quiet HEAD)" &&
371 [ -n "$srh" ] && [ "$srh" = "$refname" ]
372 then
373 orig="$(git rev-parse --verify --quiet "$refname^0" --)" || die "no such committish ref: $refname"
374 [ "$quiet" -gt 0 ] || warn "detaching HEAD to delete $refname"
375 git update-ref -m "tgtag: detach HEAD to delete $refname" --no-deref HEAD "$orig" || die "detach failed"
376 [ "$quiet" -gt 0 ] || warn "$(git --no-pager log -n 1 --format=format:'HEAD is now at %h... %s' HEAD)"
378 git update-ref --no-deref -d "$refname" || die "git update-ref --no-deref -d failed"
379 printf "Deleted $reftype '%s' (was %s)\n" "$tagname" "$old"
380 exit 0
381 elif [ -n "$clear" ]; then
382 [ -f "$logbase/logs/$refname" ] || die "no reflog found for: $refname"
383 [ -s "$logbase/logs/$refname" ] || die "empty reflog found for: $refname"
384 cp -p "$logbase/logs/$refname" "$logbase/logs/$refname^-+" || die "cp failed"
385 awk '
386 NF >= 5 { line[1] = $0 }
387 END {
388 if (1 in line) {
389 if (match(line[1], /^[0-9a-fA-F]+ /)) {
390 old = substr(line[1], 1, RLENGTH - 1)
391 rest = substr(line[1], RLENGTH)
392 gsub(/[0-9a-fA-F]/, "0", old)
393 line[1] = old rest
395 print line[1]
398 ' <"$logbase/logs/$refname^-+" >"$logbase/logs/$refname" || die "reflog clear failed"
399 rm -f "$logbase/logs/$refname^-+"
400 printf "Cleared $reftype '%s' reflog to single @{0} entry\n" "$tagname"
401 exit 0
402 else # -n "$drop"
403 ref="$old"
404 old="$(git rev-parse --verify --short "$refname" -- 2>/dev/null)" || {
405 # if it failed, redo showing STDERR, otherwise suppress STDERR
406 git rev-parse --verify --short "$refname" -- >/dev/null || :
407 exit 1
409 [ -z "$sfxis0" ] || [ "$ref" = "$old" ] || sfxis0=
410 [ -z "$sfxis0" ] || ! git symbolic-ref -q "${refname%$sfx}" -- >/dev/null 2>&1 || sfxis0=
411 if [ -n "$sfxis0" ]; then
412 [ -f "$logbase/logs/${refname%$sfx}" ] || die "no reflog found for: ${refname%$sfx}"
413 [ -s "$logbase/logs/${refname%$sfx}" ] || die "empty reflog found for: ${refname%$sfx}"
414 # make sure @{1} is valid via pseudo stale-fix before using --updateref
415 cnt="$(( $(wc -l <"$logbase/logs/${refname%$sfx}") ))"
416 lastcnt=
417 at1=
418 while
419 # avoid using --updateref if @{0} is the only entry (i.e. less than 2 lines in log)
420 [ $cnt -ge 2 ] && [ "$cnt" != "$lastcnt" ] &&
421 at1="$(git rev-parse --verify --quiet "${refname%$sfx}@{1}" -- 2>/dev/null)" &&
422 [ -n "$at1" ] &&
423 ! git rev-list --no-walk --objects "$at1" -- >/dev/null 2>&1
425 # poor man's --stale-fix that's faster and actually works reliably
426 git reflog delete --rewrite "${refname%$sfx}@{1}" >/dev/null 2>&1 ||
427 die "pseudo stale-fix failed for broken ${refname%$sfx}@{1}"
428 lastcnt="$cnt"
429 at1=
430 cnt="$(( $(wc -l <"$logbase/logs/${refname%$sfx}") ))"
431 done
432 # avoid using --updateref if @{0} is the only entry (i.e. less than 2 lines in log)
433 [ -n "$at1" ] && [ $cnt -ge 2 ] || sfxis0=
435 git reflog delete --rewrite ${sfxis0:+--updateref} "$refname" || die "reflog drop failed"
436 if [ -n "$sfxis0" ]; then
437 # check if we need to clean up
438 check="$(git rev-parse --verify --quiet "${refname%$sfx}" --)" || :
439 [ "${check#*[!0]}" != "$check" ] || check= # all 0's or empty is bad
440 # Git versions prior to 2.4.0 might need some clean up
441 [ -n "$check" ] || git update-ref -d "${refname%$sfx}" >/dev/null 2>&1 || :
443 printf "Dropped $reftype '%s' reflog entry (was %s)\n" "$tagname" "$old"
444 exit 0
447 if [ -n "$reflog" ]; then
448 [ "$refname" = "refs/tgstash" ] || [ -n "$setreflogmsg" ] || reflogmsg=1
449 git rev-parse --verify --quiet "$refname" -- >/dev/null ||
450 die "no such ref: $refname"
451 [ -s "$logbase/logs/$refname" ] ||
452 die "no reflog present for $reftype: $tagname"
453 showref="$refname"
454 [ "$refname" = "HEAD" ] || showref="$(git rev-parse --revs-only --abbrev-ref=strict "$refname" --)"
455 hashcolor=
456 resetcolor=
457 if git config --get-colorbool color.tgtag; then
458 metacolor="$(git config --get-color color.tgtag.meta)"
459 [ -n "$metacolor" ] || metacolor="$(git config --get-color color.diff.meta "bold")"
460 hashcolor="$(git config --get-color color.tgtag.commit)"
461 [ -n "$hashcolor" ] || hashcolor="$(git config --get-color color.diff.commit "yellow")"
462 datecolor="$(git config --get-color color.tgtag.date "bold blue")"
463 timecolor="$(git config --get-color color.tgtag.time "green")"
464 resetcolor="$(git config --get-color "" reset)"
466 setup_strftime
467 output()
469 sed 's/[^ ][^ ]* //' <"$logbase/logs/$refname" |
470 awk '{a[i++]=$0} END {for (j=i-1; j>=0;) print a[j--]}' |
471 git cat-file --batch-check='%(objectname) %(objecttype) %(rest)' |
473 stashnum=-1
474 lastdate=
475 while read -r newrev type rest; do
476 stashnum=$(( $stashnum + 1 ))
477 [ "$type" != "missing" ] || continue
478 IFS="$tab" read -r cmmttr msg <<-~EOT~
479 $rest
480 ~EOT~
481 ne="${cmmttr% *}"
482 ne="${ne% *}"
483 es="${cmmttr#$ne}"
484 es="${es% *}"
485 es="${es# }"
486 obj="$(git rev-parse --verify --quiet --short "$newrev" --)"
487 extra=
488 [ "$type" = "tag" ] || [ -n "$notype" ] ||
489 extra="$hashcolor($metacolor$type$resetcolor$hashcolor)$resetcolor "
490 if [ -z "$reflogmsg" ] || [ -z "$msg" ]; then
491 objmsg=
492 if [ "$type" = "tag" ]; then
493 objmsg="$(git cat-file tag "$obj" |
494 sed '1,/^$/d' | sed '/^$/,$d')"
495 elif [ "$type" = "commit" ]; then
496 objmsg="$(git --no-pager log -n 1 --format='format:%s' "$obj" --)"
498 [ -z "$objmsg" ] || msg="$objmsg"
500 read newdate newtime <<-EOT
501 $(strftime "%Y-%m-%d %H:%M:%S" "$es" 2>/dev/null)
503 if [ "$lastdate" != "$newdate" ]; then
504 printf '%s=== %s ===%s\n' "$datecolor" "$newdate" "$resetcolor"
505 lastdate="$newdate"
507 printf '%s %s %s%s@{%s}: %s\n' "$hashcolor$obj$reseutcolor" \
508 "$timecolor$newtime$resetcolor" \
509 "$extra" "$showref" "$stashnum" "$msg"
510 if [ -n "$maxcount" ]; then
511 maxcount=$(( $maxcount - 1 ))
512 [ $maxcount -gt 0 ] || break
514 done
517 page output
518 exit 0
520 [ -z "$signed" ] || [ "$reftype" = "tag" ] || die "signed tags must be under refs/tags"
521 [ $# -gt 0 ] || set -- $defbranch
522 all=
523 if [ $# -eq 1 ] && [ "$1" = "--all" ]; then
524 eval set -- $(git for-each-ref --shell --format="%(refname)" ${anyrefok:+"refs/heads"} "refs/$topbases")
525 outofdateok=1
526 all=1
527 if [ $# -eq 0 ]; then
528 if [ "$quiet" -gt 0 ] && [ -n "$noneok" ]; then
529 exit 0
530 else
531 onlytg=
532 [ -n "$anyrefok" ] || onlytg=" TopGit"
533 die "no$onlytg branches found"
537 [ -n "$refsonly" ] || ensure_ident_available
538 branches=
539 allrefs=
540 extrarefs=
541 tgbranches=
542 tgcount=0
543 othercount=0
544 ignore=
545 newlist=
546 firstprnt=
547 for arg in "$@"; do
548 case "$arg" in "~"?*)
549 [ -z "$firstprnt" ] || die "only one first parent may be specified with ~"
550 firstprnt="$(git rev-parse --verify --quiet "${arg#?}^0" -- 2>/dev/null)" && [ -n "$firstprnt" ] ||
551 die "not a commit-ish: ${arg#?}"
552 esac
553 done
554 while read -r obj typ ref && [ -n "$obj" ] && [ -n "$typ" ]; do
555 [ -n "$ref" ] || [ "$typ" != "missing" ] || die "no such ref: ${obj%???}"
556 case " $ignore " in *" $ref "*) continue; esac
557 if [ "$typ" != "commit" ] && [ "$typ" != "tag" ]; then
558 [ -n "$anyrefok" ] || die "not a committish (is a '$typ') ref: $ref"
559 [ "$quiet" -ge 2 ] || warn "ignoring non-committish (is a '$typ') ref: $ref"
560 ignore="${ignore:+$ignore }$ref"
561 continue
563 case " $newlist " in *" $ref "*);;*)
564 newlist="${newlist:+$newlist }$ref"
565 esac
566 if [ "$typ" = "tag" ]; then
567 [ "$quiet" -ge 2 ] || warn "storing as lightweight tag instead of 'tag' object: $ref"
568 ignore="${ignore:+$ignore }$ref"
570 done <<-EOT
572 printf '%s\n' "$@" | sed 's/^~//; s/^\(.*\)$/\1^{} \1/'
573 printf '%s\n' "$@" | sed 's/^~//; s/^\(.*\)$/\1 \1/'
575 git cat-file --batch-check='%(objectname) %(objecttype) %(rest)' 2>/dev/null ||
578 set -- $newlist
579 errtemp="$(get_temp errs)"
580 for b; do
581 sfn="$b"
582 if [ -z "$all" ]; then
583 [ "${b#-}" = "$b" ] || die "branch names starting with '-' must be fully qualified: $b"
584 sfn="$(git rev-parse --revs-only --symbolic-full-name "$b" -- 2>"$errtemp")" || :
585 [ -n "$sfn" ] || ! [ -s "$errtemp" ] || sfn="$(git rev-parse --revs-only --symbolic-full-name "refs/heads/$b" -- 2>/dev/null)" || :
587 [ -n "$sfn" ] || {
588 if [ -z "$anyrefok" ] || [ -s "$errtemp" ]; then
589 ! [ -s "$errtemp" ] || tail -n 1 <"$errtemp" >&2
590 die "invalid symbolic ref name: $b"
592 fullhash="$(git rev-parse --verify --quiet "$b" --)" || die "no such ref: $b"
593 case " $extrarefs " in *" $b "*);;*)
594 [ "$quiet" -ge 2 ] || warn "including non-symbolic ref only in parents calculation: $b"
595 extrarefs="${extrarefs:+$extrarefs }$fullhash"
596 esac
597 continue
599 case "$sfn" in
600 refs/"$topbases"/*)
601 added=
602 tgish=1
603 ref_exists "refs/heads/${sfn#refs/$topbases/}" || tgish=
604 [ -n "$anyrefok" ] || [ -n "$tgish" ] || [ "$quiet" -ge 2 ] ||
605 warn "including TopGit base that's missing its head: $sfn"
606 case " $allrefs " in *" $sfn "*);;*)
607 allrefs="${allrefs:+$allrefs }$sfn"
608 esac
609 case " $branches " in *" ${sfn#refs/$topbases/} "*);;*)
610 branches="${branches:+$branches }${sfn#refs/$topbases/}"
611 added=1
612 esac
613 if [ -n "$tgish" ]; then
614 case " $allrefs " in *" refs/heads/${sfn#refs/$topbases/} "*);;*)
615 allrefs="${allrefs:+$allrefs }refs/heads/${sfn#refs/$topbases/}"
616 esac
617 case " $tgbranches " in *" ${sfn#refs/$topbases/} "*);;*)
618 tgbranches="${tgbranches:+$tgbranches }${sfn#refs/$topbases/}"
619 added=1
620 esac
621 [ -z "$added" ] || tgcount=$(( $tgcount + 1 ))
622 else
623 [ -z "$added" ] || othercount=$(( $othercount + 1 ))
626 refs/heads/*)
627 added=
628 tgish=1
629 ref_exists "refs/$topbases/${sfn#refs/heads/}" || tgish=
630 [ -n "$anyrefok" ] || [ -n "$tgish" ] ||
631 die "not a TopGit branch: ${sfn#refs/heads/} (use --allow-any option)"
632 case " $allrefs " in *" $b "*);;*)
633 allrefs="${allrefs:+$allrefs }$sfn"
634 esac
635 case " $branches " in *" ${sfn#refs/heads/} "*);;*)
636 branches="${branches:+$branches }${sfn#refs/heads/}"
637 added=1
638 esac
639 if [ -n "$tgish" ]; then
640 case " $allrefs " in *" refs/$topbases/${sfn#refs/heads/} "*);;*)
641 allrefs="${allrefs:+$allrefs }refs/$topbases/${sfn#refs/heads/}"
642 esac
643 case " $tgbranches " in *" ${sfn#refs/heads/} "*);;*)
644 tgbranches="${tgbranches:+$tgbranches }${sfn#refs/heads/}"
645 added=1
646 esac
647 [ -z "$added" ] || tgcount=$(( $tgcount + 1 ))
648 else
649 [ -z "$added" ] || othercount=$(( $othercount + 1 ))
653 [ -n "$anyrefok" ] || die "refusing to include without --allow-any: $sfn"
654 case " $allrefs " in *" $sfn "*);;*)
655 allrefs="${allrefs:+$allrefs }$sfn"
656 esac
657 case " $branches " in *" ${sfn#refs/} "*);;*)
658 branches="${branches:+$branches }${sfn#refs/}"
659 othercount=$(( $othercount + 1 ))
660 esac
662 esac
663 done
665 [ -n "$force" ] ||
666 ! git rev-parse --verify --quiet "$refname" -- >/dev/null ||
667 die "$reftype '$tagname' already exists"
669 desc="tg branch"
670 descpl="tg branches"
671 if [ $othercount -gt 0 ]; then
672 if [ $tgcount -eq 0 ]; then
673 desc="ref"
674 descpl="refs"
675 else
676 descpl="$descpl and refs"
680 get_dep() {
681 case " $seen_deps " in *" $_dep "*) return 0; esac
682 seen_deps="${seen_deps:+$seen_deps }$_dep"
683 printf 'refs/heads/%s\n' "$_dep"
684 [ -z "$_dep_is_tgish" ] || printf 'refs/%s/%s\n' "$topbases" "$_dep"
687 get_deps_internal()
689 no_remotes=1
690 recurse_deps_exclude=
691 for _b; do
692 case " $recurse_deps_exclude " in *" $_b "*) continue; esac
693 seen_deps=
694 _dep="$_b"; _dep_is_tgish=1; get_dep
695 recurse_deps get_dep "$_b"
696 recurse_deps_exclude="$recurse_deps_exclude $seen_deps"
697 done
700 get_deps()
702 get_deps_internal "$@" | sort -u
705 out_of_date=
706 if [ -n "$outofdateok" ]; then
707 if [ -n "$tgbranches" ]; then
708 while read -r dep && [ -n "$dep" ]; do
709 case " $allrefs " in *" $dep "*);;*)
710 ! ref_exists "$dep" ||
711 allrefs="${allrefs:+$allrefs }$dep"
712 esac
713 done <<-EOT
714 $(get_deps $tgbranches)
717 else
718 for b in $tgbranches; do
719 if ! needs_update "$b" >/dev/null; then
720 out_of_date=1
721 echo "branch not up-to-date: $b" >&2
723 done
725 [ -z "$out_of_date" ] || die "all branches to be tagged must be up-to-date"
727 get_refs()
729 printf '%s\n' '-----BEGIN TOPGIT REFS-----'
731 printf '%s\n' $allrefs
732 [ -n "$outofdateok" ] || get_deps $tgbranches
733 } | sort -u | sed 's/^\(.*\)$/\1^0 \1/' |
734 git cat-file --batch-check='%(objectname) %(rest)' 2>/dev/null |
735 grep -v ' missing$' || :
736 printf '%s\n' '-----END TOPGIT REFS-----'
739 if [ -n "$refsonly" ]; then
740 get_refs
741 exit 0
744 stripcomments=
745 if [ -n "$msgfile" ]; then
746 if [ "$msgfile" = "-" ]; then
747 git stripspace >"$git_dir/TAG_EDITMSG"
748 else
749 git stripspace <"$msgfile" >"$git_dir/TAG_EDITMSG"
751 elif [ -n "$msg" ]; then
752 printf '%s\n' "$msg" | git stripspace >"$git_dir/TAG_EDITMSG"
753 else
754 case "$branches" in
755 *" "*)
756 if [ ${#branches} -le 60 ]; then
757 printf '%s\n' "tag $descpl $branches"
758 printf '%s\n' "$updmsg"
759 else
760 printf '%s\n' "tag $(( $(printf '%s' "$branches" | wc -w) )) $descpl" ""
761 for b in $branches; do
762 printf '%s\n' "$b"
763 done
767 printf '%s\n' "tag $desc $branches"
769 esac | git stripspace >"$git_dir/TAG_EDITMSG"
770 if [ -z "$noedit" ]; then
772 cat <<EOT
774 # Please enter a message for tg tag:
775 # $tagname
776 # Lines starting with '#' will be ignored.
778 # $descpl to be tagged:
781 for b in $branches; do
782 printf '%s\n' "# $b"
783 done
784 } >>"$git_dir/TAG_EDITMSG"
785 stripcomments=1
786 run_editor "$git_dir/TAG_EDITMSG" ||
787 die "there was a problem with the editor '$tg_editor'"
790 git -c core.commentchar='#' stripspace ${stripcomments:+--strip-comments} \
791 <"$git_dir/TAG_EDITMSG" >"$git_dir/TGTAG_FINALMSG"
792 [ -s "$git_dir/TGTAG_FINALMSG" ] || die "no tag message?"
793 echo "" >>"$git_dir/TGTAG_FINALMSG"
794 get_refs >>"$git_dir/TGTAG_FINALMSG"
796 v_count_args() { eval "$1="'$(( $# - 1 ))'; }
798 tagtarget=
799 case "$allrefs${extrarefs:+ $extrarefs}" in
800 *" "*)
801 parents="$(git merge-base --independent \
802 $(printf '%s^0 ' $allrefs $extrarefs))" ||
803 die "failed: git merge-base --independent"
806 if [ -n "$firstprnt" ]; then
807 parents="$(git rev-parse --quiet --verify "$allrefs^0" --)" ||
808 die "failed: git rev-parse $allrefs^0"
809 else
810 parents="$allrefs^0"
813 esac
814 if [ -n "$firstprnt" ]; then
815 oldparents="$parents"
816 parents="$firstprnt"
817 for acmt in $oldparents; do
818 [ "$acmt" = "$firstprnt" ] || parents="$parents $acmt"
819 done
820 unset oldparents
822 v_count_args pcnt $parents
823 if [ $pcnt -eq 1 ]; then
824 tagtarget="$parents"
825 [ -z "$treeish" ] ||
826 [ "$(git rev-parse --quiet --verify "$tagtarget^{tree}" --)" = "$treeish" ] ||
827 tagtarget=
829 if [ -z "$tagtarget" ]; then
830 tagtree="${treeish:-$firstprnt}"
831 [ -n "$tagtree" ] || tagtree="$(git mktree </dev/null)"
832 tagtarget="$(printf '%s\n' "tg tag branch consolidation" "" $branches |
833 git commit-tree $tagtree^{tree} $(printf -- '-p %s ' $parents))"
836 init_reflog "$refname"
837 if [ "$reftype" = "tag" ] && [ -n "$signed" ]; then
838 [ "$quiet" -eq 0 ] || exec >/dev/null
839 git tag -F "$git_dir/TGTAG_FINALMSG" ${signed:+-s} ${force:+-f} \
840 ${keyid:+-u} ${keyid} "$tagname" "$tagtarget"
841 else
842 obj="$(git rev-parse --verify --quiet "$tagtarget" --)" ||
843 die "invalid object name: $tagtarget"
844 typ="$(git cat-file -t "$tagtarget" 2>/dev/null)" ||
845 die "invalid object name: $tagtarget"
846 id="$(git var GIT_COMMITTER_IDENT 2>/dev/null)" ||
847 die "could not get GIT_COMMITTER_IDENT"
848 newtag="$({
849 printf '%s\n' "object $obj" "type $typ" "tag $tagname" \
850 "tagger $id" ""
851 cat "$git_dir/TGTAG_FINALMSG"
852 } | git mktag)" || die "git mktag failed"
853 old="$(git rev-parse --verify --short --quiet "$refname" --)" || :
854 updmsg=
855 case "$branches" in
856 *" "*)
857 if [ ${#branches} -le 100 ]; then
858 updmsg="$(printf '%s\n' "tgtag: $branches")"
859 else
860 updmsg="$(printf '%s\n' "tgtag: $(( $(printf '%s' "$branches" | wc -w) )) ${descpl#tg }")"
864 updmsg="$(printf '%s\n' "tgtag: $branches")"
866 esac
867 [ "$refname" != "TG_STASH" ] || ! [ -s "$git_dir/TG_STASH" ] || mv -f "$git_dir/TG_STASH" "$git_dir/ORIG_TG_STASH" >/dev/null 2>&1 || :
868 git update-ref -m "$updmsg" "$refname" "$newtag"
869 [ -z "$old" ] || [ "$quiet" -gt 0 ] || printf "Updated $reftype '%s' (was %s)\n" "$tagname" "$old"
871 rm -f "$git_dir/TAG_EDITMSG" "$git_dir/TGTAG_FINALMSG"