topgit: version 0.19.11
[topgit/pro.git] / tg-tag.sh
blob0cf214c7e381be4938dcf82c2f856788428edd08
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
403 old="$(git rev-parse --verify --short "$refname" --)" || exit 1
404 [ -z "$sfxis0" ] || ! git symbolic-ref -q "${refname%$sfx}" -- >/dev/null 2>&1 || sfxis0=
405 if [ -n "$sfxis0" ]; then
406 [ -f "$logbase/logs/${refname%$sfx}" ] || die "no reflog found for: ${refname%$sfx}"
407 [ -s "$logbase/logs/${refname%$sfx}" ] || die "empty reflog found for: ${refname%$sfx}"
408 # make sure @{1} is valid via pseudo stale-fix before using --updateref
409 cnt="$(( $(wc -l <"$logbase/logs/${refname%$sfx}") ))"
410 lastcnt=
411 at1=
412 while
413 # avoid using --updateref if @{0} is the only entry (i.e. less than 2 lines in log)
414 [ $cnt -ge 2 ] && [ "$cnt" != "$lastcnt" ] &&
415 at1="$(git rev-parse --verify --quiet "${refname%$sfx}@{1}" -- 2>/dev/null)" &&
416 [ -n "$at1" ] &&
417 ! git rev-list --no-walk --objects "$at1" -- >/dev/null 2>&1
419 # poor man's --stale-fix that's faster and actually works reliably
420 git reflog delete --rewrite "${refname%$sfx}@{1}" >/dev/null 2>&1 ||
421 die "pseudo stale-fix failed for broken ${refname%$sfx}@{1}"
422 lastcnt="$cnt"
423 at1=
424 cnt="$(( $(wc -l <"$logbase/logs/${refname%$sfx}") ))"
425 done
426 # avoid using --updateref if @{0} is the only entry (i.e. less than 2 lines in log)
427 [ -n "$at1" ] && [ $cnt -ge 2 ] || sfxis0=
429 git reflog delete --rewrite ${sfxis0:+--updateref} "$refname" || die "reflog drop failed"
430 if [ -n "$sfxis0" ]; then
431 # check if we need to clean up
432 check="$(git rev-parse --verify --quiet "${refname%$sfx}" --)" || :
433 [ "${check#*[!0]}" != "$check" ] || check= # all 0's or empty is bad
434 # Git versions prior to 2.4.0 might need some clean up
435 [ -n "$check" ] || git update-ref -d "${refname%$sfx}" >/dev/null 2>&1 || :
437 printf "Dropped $reftype '%s' reflog entry (was %s)\n" "$tagname" "$old"
438 exit 0
441 if [ -n "$reflog" ]; then
442 [ "$refname" = "refs/tgstash" ] || [ -n "$setreflogmsg" ] || reflogmsg=1
443 git rev-parse --verify --quiet "$refname" -- >/dev/null ||
444 die "no such ref: $refname"
445 [ -s "$logbase/logs/$refname" ] ||
446 die "no reflog present for $reftype: $tagname"
447 showref="$refname"
448 [ "$refname" = "HEAD" ] || showref="$(git rev-parse --revs-only --abbrev-ref=strict "$refname" --)"
449 hashcolor=
450 resetcolor=
451 if git config --get-colorbool color.tgtag; then
452 metacolor="$(git config --get-color color.tgtag.meta)"
453 [ -n "$metacolor" ] || metacolor="$(git config --get-color color.diff.meta "bold")"
454 hashcolor="$(git config --get-color color.tgtag.commit)"
455 [ -n "$hashcolor" ] || hashcolor="$(git config --get-color color.diff.commit "yellow")"
456 datecolor="$(git config --get-color color.tgtag.date "bold blue")"
457 timecolor="$(git config --get-color color.tgtag.time "green")"
458 resetcolor="$(git config --get-color "" reset)"
460 setup_strftime
461 output()
463 sed 's/[^ ][^ ]* //' <"$logbase/logs/$refname" |
464 awk '{a[i++]=$0} END {for (j=i-1; j>=0;) print a[j--]}' |
465 git cat-file --batch-check='%(objectname) %(objecttype) %(rest)' |
467 stashnum=-1
468 lastdate=
469 while read -r newrev type rest; do
470 stashnum=$(( $stashnum + 1 ))
471 [ "$type" != "missing" ] || continue
472 IFS="$tab" read -r cmmttr msg <<-~EOT~
473 $rest
474 ~EOT~
475 ne="${cmmttr% *}"
476 ne="${ne% *}"
477 es="${cmmttr#$ne}"
478 es="${es% *}"
479 es="${es# }"
480 obj="$(git rev-parse --verify --quiet --short "$newrev" --)"
481 extra=
482 [ "$type" = "tag" ] || [ -n "$notype" ] ||
483 extra="$hashcolor($metacolor$type$resetcolor$hashcolor)$resetcolor "
484 if [ -z "$reflogmsg" ] || [ -z "$msg" ]; then
485 objmsg=
486 if [ "$type" = "tag" ]; then
487 objmsg="$(git cat-file tag "$obj" |
488 sed '1,/^$/d' | sed '/^$/,$d')"
489 elif [ "$type" = "commit" ]; then
490 objmsg="$(git --no-pager log -n 1 --format='format:%s' "$obj" --)"
492 [ -z "$objmsg" ] || msg="$objmsg"
494 read newdate newtime <<-EOT
495 $(strftime "%Y-%m-%d %H:%M:%S" "$es" 2>/dev/null)
497 if [ "$lastdate" != "$newdate" ]; then
498 printf '%s=== %s ===%s\n' "$datecolor" "$newdate" "$resetcolor"
499 lastdate="$newdate"
501 printf '%s %s %s%s@{%s}: %s\n' "$hashcolor$obj$reseutcolor" \
502 "$timecolor$newtime$resetcolor" \
503 "$extra" "$showref" "$stashnum" "$msg"
504 if [ -n "$maxcount" ]; then
505 maxcount=$(( $maxcount - 1 ))
506 [ $maxcount -gt 0 ] || break
508 done
511 page output
512 exit 0
514 [ -z "$signed" ] || [ "$reftype" = "tag" ] || die "signed tags must be under refs/tags"
515 [ $# -gt 0 ] || set -- $defbranch
516 all=
517 if [ $# -eq 1 ] && [ "$1" = "--all" ]; then
518 eval set -- $(git for-each-ref --shell --format="%(refname)" ${anyrefok:+"refs/heads"} "refs/$topbases")
519 outofdateok=1
520 all=1
521 if [ $# -eq 0 ]; then
522 if [ "$quiet" -gt 0 ] && [ -n "$noneok" ]; then
523 exit 0
524 else
525 onlytg=
526 [ -n "$anyrefok" ] || onlytg=" TopGit"
527 die "no$onlytg branches found"
531 [ -n "$refsonly" ] || ensure_ident_available
532 branches=
533 allrefs=
534 extrarefs=
535 tgbranches=
536 tgcount=0
537 othercount=0
538 ignore=
539 newlist=
540 firstprnt=
541 for arg in "$@"; do
542 case "$arg" in "~"?*)
543 [ -z "$firstprnt" ] || die "only one first parent may be specified with ~"
544 firstprnt="$(git rev-parse --verify --quiet "${arg#?}^0" -- 2>/dev/null)" && [ -n "$firstprnt" ] ||
545 die "not a commit-ish: ${arg#?}"
546 esac
547 done
548 while read -r obj typ ref && [ -n "$obj" ] && [ -n "$typ" ]; do
549 [ -n "$ref" ] || [ "$typ" != "missing" ] || die "no such ref: ${obj%???}"
550 case " $ignore " in *" $ref "*) continue; esac
551 if [ "$typ" != "commit" ] && [ "$typ" != "tag" ]; then
552 [ -n "$anyrefok" ] || die "not a committish (is a '$typ') ref: $ref"
553 [ "$quiet" -ge 2 ] || warn "ignoring non-committish (is a '$typ') ref: $ref"
554 ignore="${ignore:+$ignore }$ref"
555 continue
557 case " $newlist " in *" $ref "*);;*)
558 newlist="${newlist:+$newlist }$ref"
559 esac
560 if [ "$typ" = "tag" ]; then
561 [ "$quiet" -ge 2 ] || warn "storing as lightweight tag instead of 'tag' object: $ref"
562 ignore="${ignore:+$ignore }$ref"
564 done <<-EOT
566 printf '%s\n' "$@" | sed 's/^~//; s/^\(.*\)$/\1^{} \1/'
567 printf '%s\n' "$@" | sed 's/^~//; s/^\(.*\)$/\1 \1/'
569 git cat-file --batch-check='%(objectname) %(objecttype) %(rest)' 2>/dev/null ||
572 set -- $newlist
573 errtemp="$(get_temp errs)"
574 for b; do
575 sfn="$b"
576 if [ -z "$all" ]; then
577 [ "${b#-}" = "$b" ] || die "branch names starting with '-' must be fully qualified: $b"
578 sfn="$(git rev-parse --revs-only --symbolic-full-name "$b" -- 2>"$errtemp")" || :
579 [ -n "$sfn" ] || ! [ -s "$errtemp" ] || sfn="$(git rev-parse --revs-only --symbolic-full-name "refs/heads/$b" -- 2>/dev/null)" || :
581 [ -n "$sfn" ] || {
582 if [ -z "$anyrefok" ] || [ -s "$errtemp" ]; then
583 ! [ -s "$errtemp" ] || tail -n 1 <"$errtemp" >&2
584 die "invalid symbolic ref name: $b"
586 fullhash="$(git rev-parse --verify --quiet "$b" --)" || die "no such ref: $b"
587 case " $extrarefs " in *" $b "*);;*)
588 [ "$quiet" -ge 2 ] || warn "including non-symbolic ref only in parents calculation: $b"
589 extrarefs="${extrarefs:+$extrarefs }$fullhash"
590 esac
591 continue
593 case "$sfn" in
594 refs/"$topbases"/*)
595 added=
596 tgish=1
597 ref_exists "refs/heads/${sfn#refs/$topbases/}" || tgish=
598 [ -n "$anyrefok" ] || [ -n "$tgish" ] || [ "$quiet" -ge 2 ] ||
599 warn "including TopGit base that's missing its head: $sfn"
600 case " $allrefs " in *" $sfn "*);;*)
601 allrefs="${allrefs:+$allrefs }$sfn"
602 esac
603 case " $branches " in *" ${sfn#refs/$topbases/} "*);;*)
604 branches="${branches:+$branches }${sfn#refs/$topbases/}"
605 added=1
606 esac
607 if [ -n "$tgish" ]; then
608 case " $allrefs " in *" refs/heads/${sfn#refs/$topbases/} "*);;*)
609 allrefs="${allrefs:+$allrefs }refs/heads/${sfn#refs/$topbases/}"
610 esac
611 case " $tgbranches " in *" ${sfn#refs/$topbases/} "*);;*)
612 tgbranches="${tgbranches:+$tgbranches }${sfn#refs/$topbases/}"
613 added=1
614 esac
615 [ -z "$added" ] || tgcount=$(( $tgcount + 1 ))
616 else
617 [ -z "$added" ] || othercount=$(( $othercount + 1 ))
620 refs/heads/*)
621 added=
622 tgish=1
623 ref_exists "refs/$topbases/${sfn#refs/heads/}" || tgish=
624 [ -n "$anyrefok" ] || [ -n "$tgish" ] ||
625 die "not a TopGit branch: ${sfn#refs/heads/} (use --allow-any option)"
626 case " $allrefs " in *" $b "*);;*)
627 allrefs="${allrefs:+$allrefs }$sfn"
628 esac
629 case " $branches " in *" ${sfn#refs/heads/} "*);;*)
630 branches="${branches:+$branches }${sfn#refs/heads/}"
631 added=1
632 esac
633 if [ -n "$tgish" ]; then
634 case " $allrefs " in *" refs/$topbases/${sfn#refs/heads/} "*);;*)
635 allrefs="${allrefs:+$allrefs }refs/$topbases/${sfn#refs/heads/}"
636 esac
637 case " $tgbranches " in *" ${sfn#refs/heads/} "*);;*)
638 tgbranches="${tgbranches:+$tgbranches }${sfn#refs/heads/}"
639 added=1
640 esac
641 [ -z "$added" ] || tgcount=$(( $tgcount + 1 ))
642 else
643 [ -z "$added" ] || othercount=$(( $othercount + 1 ))
647 [ -n "$anyrefok" ] || die "refusing to include without --allow-any: $sfn"
648 case " $allrefs " in *" $sfn "*);;*)
649 allrefs="${allrefs:+$allrefs }$sfn"
650 esac
651 case " $branches " in *" ${sfn#refs/} "*);;*)
652 branches="${branches:+$branches }${sfn#refs/}"
653 othercount=$(( $othercount + 1 ))
654 esac
656 esac
657 done
659 [ -n "$force" ] ||
660 ! git rev-parse --verify --quiet "$refname" -- >/dev/null ||
661 die "$reftype '$tagname' already exists"
663 desc="tg branch"
664 descpl="tg branches"
665 if [ $othercount -gt 0 ]; then
666 if [ $tgcount -eq 0 ]; then
667 desc="ref"
668 descpl="refs"
669 else
670 descpl="$descpl and refs"
674 get_dep() {
675 case " $seen_deps " in *" $_dep "*) return 0; esac
676 seen_deps="${seen_deps:+$seen_deps }$_dep"
677 printf 'refs/heads/%s\n' "$_dep"
678 [ -z "$_dep_is_tgish" ] || printf 'refs/%s/%s\n' "$topbases" "$_dep"
681 get_deps_internal()
683 no_remotes=1
684 recurse_deps_exclude=
685 for _b; do
686 case " $recurse_deps_exclude " in *" $_b "*) continue; esac
687 seen_deps=
688 _dep="$_b"; _dep_is_tgish=1; get_dep
689 recurse_deps get_dep "$_b"
690 recurse_deps_exclude="$recurse_deps_exclude $seen_deps"
691 done
694 get_deps()
696 get_deps_internal "$@" | sort -u
699 out_of_date=
700 if [ -n "$outofdateok" ]; then
701 if [ -n "$tgbranches" ]; then
702 while read -r dep && [ -n "$dep" ]; do
703 case " $allrefs " in *" $dep "*);;*)
704 ! ref_exists "$dep" ||
705 allrefs="${allrefs:+$allrefs }$dep"
706 esac
707 done <<-EOT
708 $(get_deps $tgbranches)
711 else
712 for b in $tgbranches; do
713 if ! needs_update "$b" >/dev/null; then
714 out_of_date=1
715 echo "branch not up-to-date: $b" >&2
717 done
719 [ -z "$out_of_date" ] || die "all branches to be tagged must be up-to-date"
721 get_refs()
723 printf '%s\n' '-----BEGIN TOPGIT REFS-----'
725 printf '%s\n' $allrefs
726 [ -n "$outofdateok" ] || get_deps $tgbranches
727 } | sort -u | sed 's/^\(.*\)$/\1^0 \1/' |
728 git cat-file --batch-check='%(objectname) %(rest)' 2>/dev/null |
729 grep -v ' missing$' || :
730 printf '%s\n' '-----END TOPGIT REFS-----'
733 if [ -n "$refsonly" ]; then
734 get_refs
735 exit 0
738 stripcomments=
739 if [ -n "$msgfile" ]; then
740 if [ "$msgfile" = "-" ]; then
741 git stripspace >"$git_dir/TAG_EDITMSG"
742 else
743 git stripspace <"$msgfile" >"$git_dir/TAG_EDITMSG"
745 elif [ -n "$msg" ]; then
746 printf '%s\n' "$msg" | git stripspace >"$git_dir/TAG_EDITMSG"
747 else
748 case "$branches" in
749 *" "*)
750 if [ ${#branches} -le 60 ]; then
751 printf '%s\n' "tag $descpl $branches"
752 printf '%s\n' "$updmsg"
753 else
754 printf '%s\n' "tag $(( $(printf '%s' "$branches" | wc -w) )) $descpl" ""
755 for b in $branches; do
756 printf '%s\n' "$b"
757 done
761 printf '%s\n' "tag $desc $branches"
763 esac | git stripspace >"$git_dir/TAG_EDITMSG"
764 if [ -z "$noedit" ]; then
766 cat <<EOT
768 # Please enter a message for tg tag:
769 # $tagname
770 # Lines starting with '#' will be ignored.
772 # $descpl to be tagged:
775 for b in $branches; do
776 printf '%s\n' "# $b"
777 done
778 } >>"$git_dir/TAG_EDITMSG"
779 stripcomments=1
780 run_editor "$git_dir/TAG_EDITMSG" ||
781 die "there was a problem with the editor '$tg_editor'"
784 git stripspace ${stripcomments:+--strip-comments} \
785 <"$git_dir/TAG_EDITMSG" >"$git_dir/TGTAG_FINALMSG"
786 [ -s "$git_dir/TGTAG_FINALMSG" ] || die "no tag message?"
787 echo "" >>"$git_dir/TGTAG_FINALMSG"
788 get_refs >>"$git_dir/TGTAG_FINALMSG"
790 v_count_args() { eval "$1="'$(( $# - 1 ))'; }
792 tagtarget=
793 case "$allrefs${extrarefs:+ $extrarefs}" in
794 *" "*)
795 parents="$(git merge-base --independent \
796 $(printf '%s^0 ' $allrefs $extrarefs))" ||
797 die "failed: git merge-base --independent"
800 if [ -n "$firstprnt" ]; then
801 parents="$(git rev-parse --quiet --verify "$allrefs^0" --)" ||
802 die "failed: git rev-parse $allrefs^0"
803 else
804 parents="$allrefs^0"
807 esac
808 if [ -n "$firstprnt" ]; then
809 oldparents="$parents"
810 parents="$firstprnt"
811 for acmt in $oldparents; do
812 [ "$acmt" = "$firstprnt" ] || parents="$parents $acmt"
813 done
814 unset oldparents
816 v_count_args pcnt $parents
817 if [ $pcnt -eq 1 ]; then
818 tagtarget="$parents"
819 [ -z "$treeish" ] ||
820 [ "$(git rev-parse --quiet --verify "$tagtarget^{tree}" --)" = "$treeish" ] ||
821 tagtarget=
823 if [ -z "$tagtarget" ]; then
824 tagtree="${treeish:-$firstprnt}"
825 [ -n "$tagtree" ] || tagtree="$(git hash-object -t tree -w --stdin </dev/null)"
826 tagtarget="$(printf '%s\n' "tg tag branch consolidation" "" $branches |
827 git commit-tree $tagtree^{tree} $(printf -- '-p %s ' $parents))"
830 init_reflog "$refname"
831 if [ "$reftype" = "tag" ] && [ -n "$signed" ]; then
832 [ "$quiet" -eq 0 ] || exec >/dev/null
833 git tag -F "$git_dir/TGTAG_FINALMSG" ${signed:+-s} ${force:+-f} \
834 ${keyid:+-u} ${keyid} "$tagname" "$tagtarget"
835 else
836 obj="$(git rev-parse --verify --quiet "$tagtarget" --)" ||
837 die "invalid object name: $tagtarget"
838 typ="$(git cat-file -t "$tagtarget" 2>/dev/null)" ||
839 die "invalid object name: $tagtarget"
840 id="$(git var GIT_COMMITTER_IDENT 2>/dev/null)" ||
841 die "could not get GIT_COMMITTER_IDENT"
842 newtag="$({
843 printf '%s\n' "object $obj" "type $typ" "tag $tagname" \
844 "tagger $id" ""
845 cat "$git_dir/TGTAG_FINALMSG"
846 } | git mktag)" || die "git mktag failed"
847 old="$(git rev-parse --verify --short --quiet "$refname" --)" || :
848 updmsg=
849 case "$branches" in
850 *" "*)
851 if [ ${#branches} -le 100 ]; then
852 updmsg="$(printf '%s\n' "tgtag: $branches")"
853 else
854 updmsg="$(printf '%s\n' "tgtag: $(( $(printf '%s' "$branches" | wc -w) )) ${descpl#tg }")"
858 updmsg="$(printf '%s\n' "tgtag: $branches")"
860 esac
861 [ "$refname" != "TG_STASH" ] || ! [ -s "$git_dir/TG_STASH" ] || mv -f "$git_dir/TG_STASH" "$git_dir/ORIG_TG_STASH" >/dev/null 2>&1 || :
862 git update-ref -m "$updmsg" "$refname" "$newtag"
863 [ -z "$old" ] || [ "$quiet" -gt 0 ] || printf "Updated $reftype '%s' (was %s)\n" "$tagname" "$old"
865 rm -f "$git_dir/TAG_EDITMSG" "$git_dir/TGTAG_FINALMSG"