tg: avoid unnecessary setup for strftime function
[topgit/pro.git] / tg-tag.sh
blob105e40298f6da5af5b3afb9f5fb06e035fa44bb4
1 #!/bin/sh
2 # TopGit tag command
3 # Copyright (C) 2015 Kyle J. McKay <mackyle@gmail.com>
4 # All rights reserved.
5 # GPLv2
7 lf="$(printf '\n.')" && lf="${lf%?}"
8 tab="$(printf '\t.')" && tab="${tab%?}"
9 USAGE="Usage: ${tgname:-tg} [...] tag [-s | -u <key-id>] [-f] [-q] [--no-edit] [-m <msg> | -F <file>] (<tagname> | --refs) [<branch>...]"
10 USAGE="$USAGE$lf Or: ${tgname:-tg} [...] tag (-g | --reflog) [--reflog-message] [--no-type] [-n <number> | -number] [<tagname>]"
12 usage()
14 if [ "${1:-0}" != 0 ]; then
15 printf '%s\n' "$USAGE" >&2
16 else
17 printf '%s\n' "$USAGE"
19 exit ${1:-0}
22 ## Parse options
24 signed=
25 keyid=
26 force=
27 msg=
28 msgfile=
29 noedit=
30 defnoedit=
31 refsonly=
32 maxcount=
33 reflog=
34 outofdateok=
35 defbranch=HEAD
36 stash=
37 reflogmsg=
38 notype=
39 setreflogmsg=
40 quiet=
42 is_numeric()
44 [ -n "$1" ] || return 1
45 while [ -n "$1" ]; do
46 case "$1" in
47 [0-9]*)
48 set -- "${1#?}";;
50 break;;
51 esac
52 done
53 [ -z "$1" ]
56 while [ $# -gt 0 ]; do case "$1" in
57 -h|--help)
58 usage
60 -q|--quiet)
61 quiet=1
63 -g|--reflog)
64 reflog=1
66 --reflog-message)
67 reflogmsg=1
68 setreflogmsg=1
70 --no-reflog-message)
71 reflogmsg=
72 setreflogmsg=1
74 --no-type)
75 notype=1
77 -s|--sign)
78 signed=1
80 -u|--local-user|--local-user=*)
81 case "$1" in --local-user=*)
82 x="$1"
83 shift
84 set -- --local-user "${x#--local-user=}" "$@"
85 esac
86 if [ $# -lt 2 ]; then
87 echo "The $1 option requires an argument" >&2
88 usage 1
90 shift
91 keyid="$1"
93 -f|--force)
94 force=1
96 --no-edit)
97 noedit=1
99 --edit)
100 noedit=0
102 --allow-outdated)
103 outofdateok=1
105 --refs|--refs-only)
106 refsonly=1
108 -m|--message|--message=*)
109 case "$1" in --message=*)
110 x="$1"
111 shift
112 set -- --message "${x#--message=}" "$@"
113 esac
114 if [ $# -lt 2 ]; then
115 echo "The $1 option requires an argument" >&2
116 usage 1
118 shift
119 msg="$1"
121 -F|--file|--file=*)
122 case "$1" in --file=*)
123 x="$1"
124 shift
125 set -- --file "${x#--file=}" "$@"
126 esac
127 if [ $# -lt 2 ]; then
128 echo "The $1 option requires an argument" >&2
129 usage 1
131 shift
132 msgfile="$1"
134 -n|--max-count|--max-count=*|-[1-9]*)
135 case "$1" in --max-count=*)
136 x="$1"
137 shift
138 set -- --max-count "${x#--max-count=}" "$@"
139 esac
140 case "$1" in -[1-9]*)
141 x="${1#-}"
142 shift
143 set -- -n "$x" "$@"
144 esac
145 if [ $# -lt 2 ]; then
146 echo "The $1 option requires an argument" >&2
147 usage 1
149 shift
150 maxcount="$1"
153 shift
154 break
156 --all)
157 break
159 --stash)
160 if [ -n "$reflog" ]; then
161 case "$2" in -[1-9]*)
162 x1="$1"
163 x2="$2"
164 shift
165 shift
166 set -- "$x2" "$x1" "$@"
167 continue
168 esac
170 stash=1
171 defbranch=--all
172 break
174 -?*)
175 echo "Unknown option: $1" >&2
176 usage 1
179 if [ -n "$reflog" ]; then
180 case "$2" in -[1-9]*)
181 x1="$1"
182 x2="$2"
183 shift
184 shift
185 set -- "$x2" "$x1" "$@"
186 continue
187 esac
189 break
191 esac; shift; done
193 [ -z "$stash" -o -n "$reflog" ] || { outofdateok=1; force=1; defnoedit=1; }
194 [ -n "$noedit" ] || noedit="$defnoedit"
195 [ "$noedit" != "0" ] || noedit=
196 [ -z "$reflog" -o -z "$signed$keyid$force$msg$msgfile$noedit$refsonly$outofdateok" ] || usage 1
197 [ -n "$reflog" -o -z "$setreflogmsg$notype$maxcount" ] || usage 1
198 [ -z "$maxcount" ] || is_numeric "$maxcount" || die "invalid count: $maxcount"
199 [ -z "$maxcount" ] || [ $maxcount -gt 0 ] || die "invalid count: $maxcount"
200 [ -z "$msg" -o -z "$msgfile" ] || die "only one -F or -m option is allowed."
201 [ -z "$refsonly" ] || set -- refs..only "$@"
202 [ $# -gt 0 -o -z "$reflog" ] || set -- --stash
203 [ -n "$1" ] || { echo "Tag name required" >&2; usage 1; }
204 tagname="$1"
205 shift
206 [ "$tagname" != "--stash" ] || tagname=refs/tgstash
207 refname="$tagname"
208 case "$refname" in HEAD|refs/*) :;; *)
209 if reftest="$(git rev-parse --revs-only --symbolic-full-name "$refname" -- 2>/dev/null)" && \
210 [ -n "$reftest" ]; then
211 if [ -n "$reflog" ]; then
212 refname="$reftest"
213 else
214 case "$reftest" in
215 refs/tags/*|refs/tgstash)
216 refname="$reftest"
219 refname="refs/tags/$refname"
220 esac
222 else
223 refname="refs/tags/$refname"
225 esac
226 reftype=tag
227 case "$refname" in refs/tags/*) tagname="${refname#refs/tags/}";; *) reftype=ref; tagname="$refname"; esac
228 [ -z "$reflog" -o $# -eq 0 ] || usage 1
229 if [ -n "$reflog" ]; then
230 [ "$tagname" != "refs/tgstash" -o -n "$setreflogmsg" ] || reflogmsg=1
231 git rev-parse --verify --quiet "$refname" -- >/dev/null || \
232 die "no such ref: $refname"
233 [ -s "$git_dir/logs/$refname" ] || \
234 die "no reflog present for $reftype: $tagname"
235 showref="$(git rev-parse --revs-only --abbrev-ref=strict "$refname" --)"
236 hashcolor=
237 resetcolor=
238 if git config --get-colorbool color.tgtag; then
239 metacolor="$(git config --get-color color.tgtag.meta)"
240 [ -n "$metacolor" ] || metacolor="$(git config --get-color color.diff.meta "bold")"
241 hashcolor="$(git config --get-color color.tgtag.commit)"
242 [ -n "$hashcolor" ] || hashcolor="$(git config --get-color color.diff.commit "yellow")"
243 datecolor="$(git config --get-color color.tgtag.date "bold blue")"
244 timecolor="$(git config --get-color color.tgtag.time "green")"
245 resetcolor="$(git config --get-color "" reset)"
247 setup_strftime
248 output()
250 sed 's/[^ ][^ ]* //' <"$git_dir/logs/$refname" | \
251 awk '{a[i++]=$0} END {for (j=i-1; j>=0;) print a[j--]}' | \
252 git cat-file --batch-check='%(objectname) %(objecttype) %(rest)' | \
254 stashnum=-1
255 lastdate=
256 while read -r newrev type rest; do
257 stashnum=$(( $stashnum + 1 ))
258 [ "$type" != "missing" ] || continue
259 IFS="$tab" read -r cmmttr msg <<-~EOT~
260 $rest
261 ~EOT~
262 ne="${cmmttr% *}"
263 ne="${ne% *}"
264 es="${cmmttr#$ne}"
265 es="${es% *}"
266 es="${es# }"
267 obj="$(git rev-parse --verify --quiet --short "$newrev" --)"
268 extra=
269 [ "$type" = "tag" -o -n "$notype" ] || \
270 extra="$hashcolor($metacolor$type$resetcolor$hashcolor)$resetcolor "
271 if [ -z "$reflogmsg" -o -z "$msg" ]; then
272 objmsg=
273 if [ "$type" = "tag" ]; then
274 objmsg="$(git cat-file tag "$obj" | \
275 sed '1,/^$/d' | sed '/^$/,$d')"
276 elif [ "$type" = "commit" ]; then
277 objmsg="$(git log -n 1 --format='format:%s' "$obj" --)"
279 [ -z "$objmsg" ] || msg="$objmsg"
281 read newdate newtime <<-EOT
282 $(strftime "%Y-%m-%d %H:%M:%S" "$es")
284 if [ "$lastdate" != "$newdate" ]; then
285 printf '%s=== %s ===%s\n' "$datecolor" "$newdate" "$resetcolor"
286 lastdate="$newdate"
288 printf '%s %s %s%s@{%s}: %s\n' "$hashcolor$obj$reseutcolor" \
289 "$timecolor$newtime$resetcolor" \
290 "$extra" "$showref" "$stashnum" "$msg"
291 if [ -n "$maxcount" ]; then
292 maxcount=$(( $maxcount - 1 ))
293 [ $maxcount -gt 0 ] || break
295 done
298 page output
299 exit 0
301 [ -z "$signed" -o "$reftype" = "tag" ] || die "signed tags must be under refs/tags"
302 [ $# -gt 0 ] || set -- $defbranch
303 if [ $# -eq 1 ] && [ "$1" = "--all" ]; then
304 set -- $(git for-each-ref --format="%(refname)" refs/top-bases |
305 sed -e 's,^refs/top-bases/,,')
306 outofdateok=1
307 [ $# -ge 1 ] || die "no TopGit branches found"
309 branches=
310 for b; do
311 bname="$(verify_topgit_branch "$b")"
312 case " ${branches:-..} " in *" $bname "*) :;; *)
313 branches="${branches:+$branches }$bname"
314 esac
315 done
316 [ -n "$force" ] || \
317 ! git rev-parse --verify --quiet "$refname" -- >/dev/null ||
318 die "$reftype '$tagname' already exists"
320 out_of_date=
321 if [ -z "$outofdateok" ]; then
322 for b in $branches; do
323 if ! needs_update "$b" >/dev/null; then
324 out_of_date=1
325 echo "branch not up-to-date: $b"
327 done
329 [ -z "$out_of_date" ] || die "all branches to be tagged must be up-to-date"
331 show_dep()
333 case " $seen_deps " in *" $_dep "*) return 0; esac
334 seen_deps="${seen_deps:+$seen_deps }$_dep"
335 printf 'refs/heads/%s refs/heads/%s\n' "$_dep" "$_dep"
336 [ -z "$_dep_is_tgish" ] || \
337 printf 'refs/top-bases/%s refs/top-bases/%s\n' "$_dep" "$_dep"
340 show_deps()
342 no_remotes=1
343 recurse_deps_exclude=
344 for _b; do
345 seen_deps=
346 _dep="$_b"; _dep_is_tgish=1; show_dep
347 recurse_deps show_dep "$_b"
348 recurse_deps_exclude="$recurse_deps_exclude $seen_deps"
349 done
352 get_refs()
354 printf '%s\n' '-----BEGIN TOPGIT REFS-----'
355 show_deps $branches | LC_ALL=C sort -u | \
356 git cat-file --batch-check='%(objectname) %(rest)' | grep -v ' missing$'
357 printf '%s\n' '-----END TOPGIT REFS-----'
360 if [ -n "$refsonly" ]; then
361 get_refs
362 exit 0
365 stripcomments=
366 if [ -n "$msgfile" ]; then
367 if [ "$msgfile" = "-" ]; then
368 git stripspace >"$git_dir/TAG_EDITMSG"
369 else
370 git stripspace <"$msgfile" >"$git_dir/TAG_EDITMSG"
372 elif [ -n "$msg" ]; then
373 printf '%s\n' "$msg" | git stripspace >"$git_dir/TAG_EDITMSG"
374 else
375 case "$branches" in
376 *" "*)
377 if [ ${#branches} -le 60 ]; then
378 printf '%s\n' "tag tg branches $branches"
379 printf '%s\n' "$updmsg"
380 else
381 printf '%s\n' "tag $(( $(printf '%s' "$branches" | wc -w) )) tg branches" ""
382 for b in $branches; do
383 printf '%s\n' "$b"
384 done
388 printf '%s\n' "tag tg branch $branches"
390 esac | git stripspace >"$git_dir/TAG_EDITMSG"
391 if [ -z "$noedit" ]; then
393 cat <<EOT
395 # Please enter a message for tg tag:
396 # $tagname
397 # Lines starting with '#' will be ignored.
399 # tg branches to be tagged:
402 for b in $branches; do
403 printf '%s\n' "# $b"
404 done
405 } >>"$git_dir/TAG_EDITMSG"
406 stripcomments=1
407 run_editor "$git_dir/TAG_EDITMSG" || \
408 die "there was a problem with the editor '$tg_editor'"
411 git stripspace ${stripcomments:+ --strip-comments} \
412 <"$git_dir/TAG_EDITMSG" >"$git_dir/TGTAG_FINALMSG"
413 [ -s "$git_dir/TGTAG_FINALMSG" ] || die "no tag message?"
414 echo "" >>"$git_dir/TGTAG_FINALMSG"
415 get_refs >>"$git_dir/TGTAG_FINALMSG"
417 tagtarget=
418 case "$branches" in
419 *" "*)
420 parents="$(git merge-base --independent \
421 $(printf 'refs/heads/%s^0 ' $branches))" || \
422 die "failed: git merge-base --independent"
423 mttree="$(git hash-object -t tree -w --stdin </dev/null)"
424 tagtarget="$(printf '%s\n' "tg tag branch consolidation" "" $branches | \
425 git commit-tree $mttree $(printf -- '-p %s ' $parents))"
428 tagtarget="refs/heads/$branches^0"
430 esac
432 if [ -n "$logrefupdates" ]; then
433 mkdir -p "$git_dir/logs/$(dirname "$refname")" 2>/dev/null || :
434 { >>"$git_dir/logs/$refname" || :; } 2>/dev/null
436 if [ "$reftype" = "tag" -a -n "$signed" ]; then
437 [ -z "$quiet" ] || exec >/dev/null
438 git tag -F "$git_dir/TGTAG_FINALMSG" ${signed:+-s} ${force:+-f} \
439 ${keyid:+-u} ${keyid} "$tagname" "$tagtarget"
440 else
441 obj="$(git rev-parse --verify --quiet "$tagtarget" --)" || \
442 die "invalid object name: $tagtarget"
443 typ="$(git cat-file -t "$tagtarget" 2>/dev/null)" || \
444 die "invalid object name: $tagtarget"
445 id="$(git var GIT_COMMITTER_IDENT 2>/dev/null)" || \
446 die "could not get GIT_COMMITTER_IDENT"
447 newtag="$({
448 printf '%s\n' "object $obj" "type $typ" "tag $tagname" \
449 "tagger $id" ""
450 cat "$git_dir/TGTAG_FINALMSG"
451 } | git mktag)" || die "git mktag failed"
452 old="$(git rev-parse --verify --short --quiet "$refname" -- || :)"
453 updmsg=
454 case "$branches" in
455 *" "*)
456 if [ ${#branches} -le 100 ]; then
457 updmsg="$(printf '%s\n' "tgtag: $branches")"
458 else
459 updmsg="$(printf '%s\n' "tgtag: $(( $(printf '%s' "$branches" | wc -w) )) branches")"
463 updmsg="$(printf '%s\n' "tgtag: $branches")"
465 esac
466 git update-ref -m "$updmsg" "$refname" "$newtag"
467 [ -z "$old" -o -n "$quiet" ] || printf "Updated $reftype '%s' (was %s)\n" "$tagname" "$old"
469 rm -f "$git_dir/TAG_EDITMSG" "$git_dir/TGTAG_FINALMSG"