3 # Copyright (C) 2015 Kyle J. McKay <mackyle@gmail.com>
7 USAGE
="Usage: ${tgname:-tg} [...] tag [-s | -u <key-id>] [-f] [-q] [--no-edit] [-m <msg> | -F <file>] (<tagname> | --refs) [<branch>...]"
8 USAGE
="$USAGE$lf Or: ${tgname:-tg} [...] tag (-g | --reflog) [--reflog-message | --commit-message] [--no-type] [-n <number> | -number] [<tagname>]"
9 USAGE
="$USAGE$lf Or: ${tgname:-tg} [...] tag (--clear | --delete) <tagname>"
10 USAGE
="$USAGE$lf Or: ${tgname:-tg} [...] tag --drop <tagname>@{n}"
14 if [ "${1:-0}" != 0 ]; then
15 printf '%s\n' "$USAGE" >&2
17 printf '%s\n' "$USAGE"
49 [ -n "$1" ] ||
return 1
61 while [ $# -gt 0 ]; do case "$1" in
66 quiet
=$
(( $quiet + 1 ))
80 -g|
--reflog|
--walk-reflogs)
87 --no-reflog-message|
--commit-message)
97 -u|
--local-user|
--local-user=*)
98 case "$1" in --local-user=*)
101 set -- --local-user "${x#--local-user=}" "$@"
103 if [ $# -lt 2 ]; then
104 echo "The $1 option requires an argument" >&2
128 -m|
--message|
--message=*)
129 case "$1" in --message=*)
132 set -- --message "${x#--message=}" "$@"
134 if [ $# -lt 2 ]; then
135 echo "The $1 option requires an argument" >&2
142 case "$1" in --file=*)
145 set -- --file "${x#--file=}" "$@"
147 if [ $# -lt 2 ]; then
148 echo "The $1 option requires an argument" >&2
154 -n|
--max-count|
--max-count=*|
-[1-9]*)
155 case "$1" in --max-count=*)
158 set -- --max-count "${x#--max-count=}" "$@"
160 case "$1" in -[1-9]*)
165 if [ $# -lt 2 ]; then
166 echo "The $1 option requires an argument" >&2
179 --stash|
--stash"@{"*"}")
180 if [ -n "$reflog" ]; then
181 case "$2" in -[1-9]*)
186 set -- "$x2" "$x1" "$@"
195 echo "Unknown option: $1" >&2
199 if [ -n "$reflog" ]; then
200 case "$2" in -[1-9]*)
205 set -- "$x2" "$x1" "$@"
213 [ -z "$stash" -o -n "$reflog$drop$clear$delete" ] ||
{ outofdateok
=1; force
=1; defnoedit
=1; }
214 [ -n "$noedit" ] || noedit
="$defnoedit"
215 [ "$noedit" != "0" ] || noedit
=
216 [ -z "$reflog" -o -z "$drop$clear$delete$signed$keyid$force$msg$msgfile$noedit$refsonly$outofdateok" ] || usage
1
217 [ -n "$reflog" -o -z "$setreflogmsg$notype$maxcount" ] || usage
1
218 [ -z "$drop$clear$delete" -o -z "$setreflogmsg$notype$maxcount$signed$keyid$force$msg$msgfile$noedit$refsonly$outofdateok" ] || usage
1
219 [ -z "$reflog$drop$clear$delete" -o "$reflog$drop$clear$delete" = "1" ] || usage
1
220 [ -z "$maxcount" ] || is_numeric
"$maxcount" || die
"invalid count: $maxcount"
221 [ -z "$maxcount" ] ||
[ $maxcount -gt 0 ] || die
"invalid count: $maxcount"
222 [ -z "$msg" -o -z "$msgfile" ] || die
"only one -F or -m option is allowed."
223 [ -z "$refsonly" ] ||
set -- refs..only
"$@"
224 [ $# -gt 0 -o -z "$reflog" ] ||
set -- --stash
225 [ -n "$1" ] ||
{ echo "Tag name required" >&2; usage
1; }
228 [ "$tagname" != "--stash" ] || tagname
=refs
/tgstash
229 case "$tagname" in --stash"@{"*"}")
230 strip
="${tagname#--stash??}"
232 tagname
="refs/tgstash@{$strip}"
236 case "$refname" in [!@
]*"@{"*"}")
238 refname
="${refname%@*}"
239 sfx
="${sfx#$refname}"
241 case "$refname" in HEAD|refs
/*);;*)
242 if reftest
="$(git rev-parse --revs-only --symbolic-full-name "$refname" -- 2>/dev/null)" &&
243 [ -n "$reftest" ]; then
244 if [ -n "$reflog$drop$clear$delete" ]; then
248 refs
/tags
/*|refs
/tgstash
)
252 refname
="refs/tags/$refname"
256 refname
="refs/tags/$refname"
259 refname
="$refname$sfx"
261 case "$refname" in refs
/tags
/*) tagname
="${refname#refs/tags/}";; *) reftype
=ref
; tagname
="$refname"; esac
262 [ -z "$reflog$drop$clear$delete" -o $# -eq 0 ] || usage
1
263 if [ -n "$drop$clear$delete" ]; then
264 if [ -n "$sfx" ]; then
265 [ -z "$clear$delete" ] || die
"invalid ref name ($sfx suffix not allowed): $refname"
267 [ -z "$drop" ] || die
"invalid reflog name (@{n} suffix required): $refname"
269 old
="$(git rev-parse --verify --quiet --short "${refname%$sfx}" --)" || die
"no such ref: ${refname%$sfx}"
270 if [ -n "$delete" ]; then
271 git update-ref
-d "$refname" || die
"git update-ref -d failed"
272 printf "Deleted $reftype '%s' (was %s)\n" "$tagname" "$old"
274 elif [ -n "$clear" ]; then
275 [ -f "$git_dir/logs/$refname" ] || die
"no reflog found for: $refname"
276 [ -s "$git_dir/logs/$refname" ] || die
"empty reflog found for: $refname"
277 cp -p "$git_dir/logs/$refname" "$git_dir/logs/$refname^-+" || die
"cp failed"
278 sed -n '$p' <"$git_dir/logs/$refname^-+" >"$git_dir/logs/$refname" || die
"reflog clear failed"
279 rm -f "$git_dir/logs/$refname^-+"
280 printf "Cleared $reftype '%s' reflog to single @{0} entry\n" "$tagname"
283 old
="$(git rev-parse --verify --short "$refname" --)" ||
exit 1
284 git reflog delete
--rewrite --updateref "$refname" || die
"reflog drop failed"
285 printf "Dropped $reftype '%s' reflog entry (was %s)\n" "$tagname" "$old"
289 if [ -n "$reflog" ]; then
290 [ "$refname" = "refs/tgstash" -o -n "$setreflogmsg" ] || reflogmsg
=1
291 git rev-parse
--verify --quiet "$refname" -- >/dev
/null ||
292 die
"no such ref: $refname"
293 [ -s "$git_dir/logs/$refname" ] ||
294 die
"no reflog present for $reftype: $tagname"
295 showref
="$(git rev-parse --revs-only --abbrev-ref=strict "$refname" --)"
298 if git config
--get-colorbool color.tgtag
; then
299 metacolor
="$(git config --get-color color.tgtag.meta)"
300 [ -n "$metacolor" ] || metacolor
="$(git config --get-color color.diff.meta "bold
")"
301 hashcolor
="$(git config --get-color color.tgtag.commit)"
302 [ -n "$hashcolor" ] || hashcolor
="$(git config --get-color color.diff.commit "yellow
")"
303 datecolor
="$(git config --get-color color.tgtag.date "bold blue
")"
304 timecolor
="$(git config --get-color color.tgtag.time "green
")"
305 resetcolor
="$(git config --get-color "" reset)"
310 sed 's/[^ ][^ ]* //' <"$git_dir/logs/$refname" |
311 awk '{a[i++]=$0} END {for (j=i-1; j>=0;) print a[j--]}' |
312 git cat-file
--batch-check='%(objectname) %(objecttype) %(rest)' |
316 while read -r newrev
type rest
; do
317 stashnum
=$
(( $stashnum + 1 ))
318 [ "$type" != "missing" ] ||
continue
319 IFS
="$tab" read -r cmmttr msg
<<-~EOT~
327 obj="$(git rev-parse --verify --quiet --short "$newrev" --)"
329 [ "$type" = "tag" -o -n "$notype" ] ||
330 extra="$hashcolor($metacolor$type$resetcolor$hashcolor)$resetcolor "
331 if [ -z "$reflogmsg" -o -z "$msg" ]; then
333 if [ "$type" = "tag" ]; then
334 objmsg="$(git cat-file tag "$obj" |
335 sed '1,/^$/d' | sed '/^$/,$d')"
336 elif [ "$type" = "commit" ]; then
337 objmsg="$(git log -n 1 --format='format:%s' "$obj" --)"
339 [ -z "$objmsg" ] || msg="$objmsg"
341 read newdate newtime <<-EOT
342 $(strftime "%Y-%m-%d %H:%M:%S" "$es")
344 if [ "$lastdate" != "$newdate" ]; then
345 printf '%s=== %s ===%s\n' "$datecolor" "$newdate" "$resetcolor"
348 printf '%s %s %s%s@{%s}: %s\n' "$hashcolor$obj$reseutcolor" \
349 "$timecolor$newtime$resetcolor" \
350 "$extra" "$showref" "$stashnum" "$msg"
351 if [ -n "$maxcount" ]; then
352 maxcount=$(( $maxcount - 1 ))
353 [ $maxcount -gt 0 ] ||
break
361 [ -z "$signed" -o "$reftype" = "tag" ] || die
"signed tags must be under refs/tags"
362 [ $# -gt 0 ] ||
set -- $defbranch
364 if [ $# -eq 1 ] && [ "$1" = "--all" ]; then
365 eval set -- $
(git for-each-ref
--shell --format="%(refname)" "refs/$topbases")
368 if [ $# -eq 0 ]; then
369 if [ "$quiet" -gt 0 -a -n "$noneok" ]; then
372 die
"no TopGit branches found"
376 ensure_ident_available
385 while read -r obj typ ref
&& [ -n "$obj" -a -n "$typ" ]; do
386 [ -n "$ref" -o "$typ" != "missing" ] || die
"no such ref: ${obj%???}"
387 case " $ignore " in *" $ref "*) continue; esac
388 if [ "$typ" != "commit" -a "$typ" != "tag" ]; then
389 [ -n "$anyrefok" ] || die
"not a committish (is a '$typ') ref: $ref"
390 [ "$quiet" -ge 2 ] || warn
"ignoring non-committish (is a '$typ') ref: $ref"
391 ignore
="${ignore:+$ignore }$ref"
394 case " $newlist " in *" $ref "*);;*)
395 newlist
="${newlist:+$newlist }$ref"
397 if [ "$typ" = "tag" ]; then
398 [ "$quiet" -ge 2 ] || warn
"storing as lightweight tag instead of 'tag' object: $ref"
399 ignore
="${ignore:+$ignore }$ref"
403 printf '%s\n' "$@" | sed 's/^\(.*\)$/\1^{} \1/'
404 printf '%s\n' "$@" | sed 's/^\(.*\)$/\1 \1/'
406 git cat-file --batch-check='%(objectname) %(objecttype) %(rest)' 2>/dev/null ||
413 sfn
="$(git rev-parse --revs-only --symbolic-full-name "$b" -- 2>/dev/null)" ||
:
415 [ -n "$anyrefok" ] || die
"no such symbolic ref name: $b"
416 fullhash
="$(git rev-parse --verify --quiet "$b" --)" || die
"no such ref: $b"
417 case " $extrarefs " in *" $b "*);;*)
418 [ "$quiet" -ge 2 ] || warn
"including non-symbolic ref only in parents calculation: $b"
419 extrarefs
="${extrarefs:+$extrarefs }$fullhash"
427 ref_exists
"refs/heads/${sfn#refs/$topbases/}" || tgish
=
428 [ -n "$anyrefok" ] ||
[ -n "$tgish" ] ||
[ "$quiet" -ge 2 ] ||
429 warn
"including TopGit base that's missing its head: $sfn"
430 case " $allrefs " in *" $sfn "*);;*)
431 allrefs
="${allrefs:+$allrefs }$sfn"
433 case " $branches " in *" ${sfn#refs/$topbases/} "*);;*)
434 branches
="${branches:+$branches }${sfn#refs/$topbases/}"
437 if [ -n "$tgish" ]; then
438 case " $allrefs " in *" refs/heads/${sfn#refs/$topbases/} "*);;*)
439 allrefs
="${allrefs:+$allrefs }refs/heads/${sfn#refs/$topbases/}"
441 case " $tgbranches " in *" ${sfn#refs/$topbases/} "*);;*)
442 tgbranches
="${tgbranches:+$tgbranches }${sfn#refs/$topbases/}"
445 [ -z "$added" ] || tgcount
=$
(( $tgcount + 1 ))
447 [ -z "$added" ] || othercount
=$
(( $othercount + 1 ))
453 ref_exists
"refs/$topbases/${sfn#refs/heads/}" || tgish
=
454 [ -n "$anyrefok" ] ||
[ -n "$tgish" ] ||
455 die
"not a TopGit branch: ${sfn#refs/heads/} (use --allow-any option)"
456 case " $allrefs " in *" $b "*);;*)
457 allrefs
="${allrefs:+$allrefs }$sfn"
459 case " $branches " in *" ${sfn#refs/heads/} "*);;*)
460 branches
="${branches:+$branches }${sfn#refs/heads/}"
463 if [ -n "$tgish" ]; then
464 case " $allrefs " in *" refs/$topbases/${sfn#refs/heads/} "*);;*)
465 allrefs
="${allrefs:+$allrefs }refs/$topbases/${sfn#refs/heads/}"
467 case " $tgbranches " in *" ${sfn#refs/heads/} "*);;*)
468 tgbranches
="${tgbranches:+$tgbranches }${sfn#refs/heads/}"
471 [ -z "$added" ] || tgcount
=$
(( $tgcount + 1 ))
473 [ -z "$added" ] || othercount
=$
(( $othercount + 1 ))
477 [ -n "$anyrefok" ] || die
"refusing to include without --allow-any: $sfn"
478 case " $allrefs " in *" $sfn "*);;*)
479 allrefs
="${allrefs:+$allrefs }$sfn"
481 case " $branches " in *" ${sfn#refs/} "*);;*)
482 branches
="${branches:+$branches }${sfn#refs/}"
483 othercount
=$
(( $othercount + 1 ))
490 ! git rev-parse
--verify --quiet "$refname" -- >/dev
/null ||
491 die
"$reftype '$tagname' already exists"
495 if [ $othercount -gt 0 ]; then
496 if [ $tgcount -eq 0 ]; then
500 descpl
="$descpl and refs"
505 case " $seen_deps " in *" $_dep "*) return 0; esac
506 seen_deps
="${seen_deps:+$seen_deps }$_dep"
507 printf 'refs/heads/%s\n' "$_dep"
508 [ -z "$_dep_is_tgish" ] ||
printf 'refs/%s/%s\n' "$topbases" "$_dep"
514 recurse_deps_exclude
=
516 case " $recurse_deps_exclude " in *" $_b "*) continue; esac
518 _dep
="$_b"; _dep_is_tgish
=1; get_dep
519 recurse_deps get_dep
"$_b"
520 recurse_deps_exclude
="$recurse_deps_exclude $seen_deps"
526 get_deps_internal
"$@" | LC_ALL
=C
sort -u
530 if [ -n "$outofdateok" ]; then
531 if [ -n "$tgbranches" ]; then
532 while read -r dep
&& [ -n "$dep" ]; do
533 case " $allrefs " in *" $dep "*);;*)
534 ! ref_exists
"$dep" ||
535 allrefs
="${allrefs:+$allrefs }$dep"
538 $(get_deps $tgbranches)
542 for b
in $tgbranches; do
543 if ! needs_update
"$b" >/dev
/null
; then
545 echo "branch not up-to-date: $b"
549 [ -z "$out_of_date" ] || die
"all branches to be tagged must be up-to-date"
553 printf '%s\n' '-----BEGIN TOPGIT REFS-----'
555 printf '%s\n' $allrefs
556 [ -n "$outofdateok" ] || get_deps
$tgbranches
557 } | LC_ALL
=C
sort -u |
sed 's/^\(.*\)$/\1^0 \1/' |
558 git cat-file
--batch-check='%(objectname) %(rest)' 2>/dev
/null |
559 grep -v ' missing$' ||
:
560 printf '%s\n' '-----END TOPGIT REFS-----'
563 if [ -n "$refsonly" ]; then
569 if [ -n "$msgfile" ]; then
570 if [ "$msgfile" = "-" ]; then
571 git stripspace
>"$git_dir/TAG_EDITMSG"
573 git stripspace
<"$msgfile" >"$git_dir/TAG_EDITMSG"
575 elif [ -n "$msg" ]; then
576 printf '%s\n' "$msg" | git stripspace
>"$git_dir/TAG_EDITMSG"
580 if [ ${#branches} -le 60 ]; then
581 printf '%s\n' "tag $descpl $branches"
582 printf '%s\n' "$updmsg"
584 printf '%s\n' "tag $(( $(printf '%s' "$branches" | wc -w) )) $descpl" ""
585 for b
in $branches; do
591 printf '%s\n' "tag $desc $branches"
593 esac | git stripspace
>"$git_dir/TAG_EDITMSG"
594 if [ -z "$noedit" ]; then
598 # Please enter a message for tg tag:
600 # Lines starting with '#' will be ignored.
602 # $descpl to be tagged:
605 for b
in $branches; do
608 } >>"$git_dir/TAG_EDITMSG"
610 run_editor
"$git_dir/TAG_EDITMSG" ||
611 die
"there was a problem with the editor '$tg_editor'"
614 git stripspace
${stripcomments:+ --strip-comments} \
615 <"$git_dir/TAG_EDITMSG" >"$git_dir/TGTAG_FINALMSG"
616 [ -s "$git_dir/TGTAG_FINALMSG" ] || die
"no tag message?"
617 echo "" >>"$git_dir/TGTAG_FINALMSG"
618 get_refs
>>"$git_dir/TGTAG_FINALMSG"
621 case "$allrefs${extrarefs:+ $extrarefs}" in
623 parents
="$(git merge-base --independent \
624 $(printf '%s^0 ' $allrefs $extrarefs))" ||
625 die
"failed: git merge-base --independent"
626 if [ $
(printf '%s\n' "$parents" |
wc -l) -eq 1 ]; then
629 mttree
="$(git hash-object -t tree -w --stdin </dev/null)"
630 tagtarget
="$(printf '%s\n' "tg tag branch consolidation
" "" $branches |
631 git commit-tree $mttree $(printf -- '-p %s ' $parents))"
635 tagtarget
="$allrefs^0"
639 init_reflog
"$refname"
640 if [ "$reftype" = "tag" -a -n "$signed" ]; then
641 [ "$quiet" -eq 0 ] ||
exec >/dev
/null
642 git tag
-F "$git_dir/TGTAG_FINALMSG" ${signed:+-s} ${force:+-f} \
643 ${keyid:+-u} ${keyid} "$tagname" "$tagtarget"
645 obj
="$(git rev-parse --verify --quiet "$tagtarget" --)" ||
646 die
"invalid object name: $tagtarget"
647 typ
="$(git cat-file -t "$tagtarget" 2>/dev/null)" ||
648 die
"invalid object name: $tagtarget"
649 id
="$(git var GIT_COMMITTER_IDENT 2>/dev/null)" ||
650 die
"could not get GIT_COMMITTER_IDENT"
652 printf '%s\n' "object
$obj" "type $typ" "tag
$tagname" \
654 cat "$git_dir/TGTAG_FINALMSG
"
655 } | git mktag)" || die
"git mktag failed"
656 old
="$(git rev-parse --verify --short --quiet "$refname" --)" ||
:
660 if [ ${#branches} -le 100 ]; then
661 updmsg
="$(printf '%s\n' "tgtag
: $branches")"
663 updmsg
="$(printf '%s\n' "tgtag
: $
(( $
(printf '%s' "$branches" |
wc -w) )) ${descpl#tg }")"
667 updmsg
="$(printf '%s\n' "tgtag
: $branches")"
670 git update-ref
-m "$updmsg" "$refname" "$newtag"
671 [ -z "$old" -o "$quiet" -gt 0 ] ||
printf "Updated $reftype '%s' (was %s)\n" "$tagname" "$old"
673 rm -f "$git_dir/TAG_EDITMSG" "$git_dir/TGTAG_FINALMSG"