tg.sh: set LESS to -FRX by default instead of -FRSX
[topgit/pro.git] / tg-tag.sh
blob0ff92d0f3b1b9370a3176baf2fb492f992389d97
1 #!/bin/sh
2 # TopGit tag command
3 # (C) 2015 Kyle J. McKay <mackyle@gmail.com>
4 # GPLv2
6 lf="$(printf '\n.')" && lf="${lf%?}"
7 tab="$(printf '\t.')" && tab="${tab%?}"
8 USAGE="Usage: ${tgname:-tg} [...] tag [-s | -u <key-id>] [-f] [--no-edit] [-m <msg> | -F <file>] (<tagname> | --refs) [<branch>...]"
9 USAGE="$USAGE$lf Or: ${tgname:-tg} [...] tag (-g | --reflog) [--reflog-message] [--no-type] [-n <number> | -number] [<tagname>]"
11 usage()
13 if [ "${1:-0}" != 0 ]; then
14 printf '%s\n' "$USAGE" >&2
15 else
16 printf '%s\n' "$USAGE"
18 exit ${1:-0}
21 ## Parse options
23 signed=
24 keyid=
25 force=
26 msg=
27 msgfile=
28 noedit=
29 defnoedit=
30 refsonly=
31 maxcount=
32 reflog=
33 outofdateok=
34 defbranch=HEAD
35 stash=
36 reflogmsg=
37 notype=
38 setreflogmsg=
40 is_numeric()
42 [ -n "$1" ] || return 1
43 while [ -n "$1" ]; do
44 case "$1" in
45 [0-9]*)
46 set -- "${1#?}";;
48 break;;
49 esac
50 done
51 [ -z "$1" ]
54 while [ $# -gt 0 ]; do case "$1" in
55 -h|--help)
56 usage
58 -g|--reflog)
59 reflog=1
61 --reflog-message)
62 reflogmsg=1
63 setreflogmsg=1
65 --no-reflog-message)
66 reflogmsg=
67 setreflogmsg=1
69 --no-type)
70 notype=1
72 -s|--sign)
73 signed=1
75 -u|--local-user|--local-user=*)
76 case "$1" in --local-user=*)
77 x="$1"
78 shift
79 set -- --local-user "${x#--local-user=}" "$@"
80 esac
81 if [ $# -lt 2 ]; then
82 echo "The $1 option requires an argument" >&2
83 usage 1
85 shift
86 keyid="$1"
88 -f|--force)
89 force=1
91 --no-edit)
92 noedit=1
94 --edit)
95 noedit=0
97 --allow-outdated)
98 outofdateok=1
100 --refs|--refs-only)
101 refsonly=1
103 -m|--message|--message=*)
104 case "$1" in --message=*)
105 x="$1"
106 shift
107 set -- --message "${x#--message=}" "$@"
108 esac
109 if [ $# -lt 2 ]; then
110 echo "The $1 option requires an argument" >&2
111 usage 1
113 shift
114 msg="$1"
116 -F|--file|--file=*)
117 case "$1" in --file=*)
118 x="$1"
119 shift
120 set -- --file "${x#--file=}" "$@"
121 esac
122 if [ $# -lt 2 ]; then
123 echo "The $1 option requires an argument" >&2
124 usage 1
126 shift
127 msgfile="$1"
129 -n|--max-count|--max-count=*|-[1-9]*)
130 case "$1" in --max-count=*)
131 x="$1"
132 shift
133 set -- --max-count "${x#--max-count=}" "$@"
134 esac
135 case "$1" in -[1-9]*)
136 x="${1#-}"
137 shift
138 set -- -n "$x" "$@"
139 esac
140 if [ $# -lt 2 ]; then
141 echo "The $1 option requires an argument" >&2
142 usage 1
144 shift
145 maxcount="$1"
148 shift
149 break
151 --all)
152 break
154 --stash)
155 if [ -n "$reflog" ]; then
156 case "$2" in -[1-9]*)
157 x1="$1"
158 x2="$2"
159 shift
160 shift
161 set -- "$x2" "$x1" "$@"
162 continue
163 esac
165 stash=1
166 defbranch=--all
167 break
169 -?*)
170 echo "Unknown option: $1" >&2
171 usage 1
174 if [ -n "$reflog" ]; then
175 case "$2" in -[1-9]*)
176 x1="$1"
177 x2="$2"
178 shift
179 shift
180 set -- "$x2" "$x1" "$@"
181 continue
182 esac
184 break
186 esac; shift; done
188 [ -z "$stash" -o -n "$reflog" ] || { outofdateok=1; force=1; defnoedit=1; }
189 [ -n "$noedit" ] || noedit="$defnoedit"
190 [ "$noedit" != "0" ] || noedit=
191 [ -z "$reflog" -o -z "$signed$keyid$force$msg$msgfile$noedit$refsonly$outofdateok" ] || usage 1
192 [ -n "$reflog" -o -z "$setreflogmsg$notype$maxcount" ] || usage 1
193 [ -z "$maxcount" ] || is_numeric "$maxcount" || die "invalid count: $maxcount"
194 [ -z "$maxcount" ] || [ $maxcount -gt 0 ] || die "invalid count: $maxcount"
195 [ -z "$msg" -o -z "$msgfile" ] || die "only one -F or -m option is allowed."
196 [ -z "$refsonly" ] || set -- refs..only "$@"
197 [ $# -gt 0 -o -z "$reflog" ] || set -- --stash
198 [ -n "$1" ] || { echo "Tag name required" >&2; usage 1; }
199 tagname="$1"
200 shift
201 [ "$tagname" != "--stash" ] || tagname=refs/tgstash
202 refname="$tagname"
203 case "$refname" in HEAD|refs/*) :;; *) refname="refs/tags/$refname"; esac
204 reftype=tag
205 case "$refname" in refs/tags/*) tagname="${refname#refs/tags/}";; *) reftype=ref; esac
206 [ -z "$reflog" -o $# -eq 0 ] || usage 1
207 if [ -n "$reflog" ]; then
208 [ "$tagname" != "refs/tgstash" -o -n "$setreflogmsg" ] || reflogmsg=1
209 git rev-parse --verify --quiet "$refname" -- >/dev/null || \
210 die "no such ref: $refname"
211 showref="$(git rev-parse --abbrev-ref=strict "$refname")"
212 [ -s "$git_dir/logs/$refname" ] || \
213 die "no reflog present for ref: $refname"
214 hashcolor=
215 resetcolor=
216 if git config --get-colorbool color.tgtag; then
217 metacolor="$(git config --get-color color.tgtag.meta)"
218 [ -n "$metacolor" ] || metacolor="$(git config --get-color color.diff.meta "bold")"
219 hashcolor="$(git config --get-color color.tgtag.commit)"
220 [ -n "$hashcolor" ] || hashcolor="$(git config --get-color color.diff.commit "yellow")"
221 datecolor="$(git config --get-color color.tgtag.date "bold blue")"
222 timecolor="$(git config --get-color color.tgtag.time "green")"
223 resetcolor="$(git config --get-color "" reset)"
225 setup_pager
226 sed 's/[^ ][^ ]* //' <"$git_dir/logs/$refname" | \
227 awk '{a[i++]=$0} END {for (j=i-1; j>=0;) print a[j--]}' | \
228 git cat-file --batch-check='%(objectname) %(objecttype) %(rest)' | \
230 stashnum=-1
231 lastdate=
232 while read -r newrev type rest; do
233 stashnum=$(( $stashnum + 1 ))
234 [ "$type" != "missing" ] || continue
235 IFS="$tab" read -r cmmttr msg <<-~EOT~
236 $rest
237 ~EOT~
238 ne="${cmmttr% *}"
239 ne="${ne% *}"
240 es="${cmmttr#$ne}"
241 es="${es% *}"
242 es="${es# }"
243 obj="${newrev#????????}"
244 obj="${newrev%$obj}"
245 extra=
246 [ "$type" = "tag" -o -n "$notype" ] || \
247 extra="$hashcolor($metacolor$type$resetcolor$hashcolor)$resetcolor "
248 if [ -z "$reflogmsg" -o -z "$msg" ]; then
249 objmsg=
250 if [ "$type" = "tag" ]; then
251 objmsg="$(git cat-file tag "$obj" | \
252 sed '1,/^$/d' | sed '/^$/,$d')"
253 elif [ "$type" = "commit" ]; then
254 objmsg="$(git log -n 1 --format='format:%s' "$obj" --)"
256 [ -z "$objmsg" ] || msg="$objmsg"
258 read newdate newtime <<-EOT
259 $(strftime "%Y-%m-%d %H:%M:%S" "$es")
261 if [ "$lastdate" != "$newdate" ]; then
262 printf '%s=== %s ===%s\n' "$datecolor" "$newdate" "$resetcolor"
263 lastdate="$newdate"
265 printf '%s %s %s%s@{%s}: %s\n' "$hashcolor$obj$reseutcolor" \
266 "$timecolor$newtime$resetcolor" \
267 "$extra" "$showref" "$stashnum" "$msg"
268 if [ -n "$maxcount" ]; then
269 maxcount=$(( $maxcount - 1 ))
270 [ $maxcount -gt 0 ] || break
272 done
273 } | eval "$TG_PAGER"
274 exit 0
276 [ -z "$signed" -o "$reftype" = "tag" ] || die "signed tags must be under refs/tags"
277 [ $# -gt 0 ] || set -- $defbranch
278 if [ $# -eq 1 ] && [ "$1" = "--all" ]; then
279 set -- $(git for-each-ref --format="%(refname)" refs/top-bases |
280 sed -e 's,^refs/top-bases/,,')
281 outofdateok=1
282 [ $# -ge 1 ] || die "no TopGit branches found"
284 branches=
285 for b; do
286 bname="$(verify_topgit_branch "$b")"
287 case " ${branches:-..} " in *" $bname "*) :;; *)
288 branches="${branches:+$branches }$bname"
289 esac
290 done
291 [ -n "$force" ] || \
292 ! git rev-parse --verify --quiet "$refname" -- >/dev/null ||
293 die "$reftype '$tagname' already exists"
295 out_of_date=
296 if [ -z "$outofdateok" ]; then
297 for b in $branches; do
298 if ! needs_update "$b" >/dev/null; then
299 out_of_date=1
300 echo "branch not up-to-date: $b"
302 done
304 [ -z "$out_of_date" ] || die "all branches to be tagged must be up-to-date"
306 run_editor()
308 _editor="$GIT_EDITOR"
309 [ -n "$_editor" ] || _editor="$(git var GIT_EDITOR)" || return $?
310 eval "$_editor" '"$@"'
313 show_dep()
315 case " $seen_deps " in *" $_dep "*) return 0; esac
316 seen_deps="${seen_deps:+$seen_deps }$_dep"
317 printf 'refs/heads/%s refs/heads/%s\n' "$_dep" "$_dep"
318 [ -z "$_dep_is_tgish" ] || \
319 printf 'refs/top-bases/%s refs/top-bases/%s\n' "$_dep" "$_dep"
322 show_deps()
324 no_remotes=1
325 recurse_deps_exclude=
326 for _b; do
327 seen_deps=
328 _dep="$_b"; _dep_is_tgish=1; show_dep
329 recurse_deps show_dep "$_b"
330 recurse_deps_exclude="$recurse_deps_exclude $seen_deps"
331 done
334 get_refs()
336 printf '%s\n' '-----BEGIN TOPGIT REFS-----'
337 show_deps $branches | LC_ALL=C sort -u | \
338 git cat-file --batch-check='%(objectname) %(rest)' | grep -v ' missing$'
339 printf '%s\n' '-----END TOPGIT REFS-----'
342 if [ -n "$refsonly" ]; then
343 get_refs
344 exit 0
347 stripcomments=
348 if [ -n "$msgfile" ]; then
349 if [ "$msgfile" = "-" ]; then
350 git stripspace >"$git_dir/TAG_EDITMSG"
351 else
352 git stripspace <"$msgfile" >"$git_dir/TAG_EDITMSG"
354 elif [ -n "$msg" ]; then
355 printf '%s\n' "$msg" | git stripspace >"$git_dir/TAG_EDITMSG"
356 else
357 case "$branches" in
358 *" "*)
359 if [ ${#branches} -le 60 ]; then
360 printf '%s\n' "tag tg branches $branches"
361 printf '%s\n' "$updmsg"
362 else
363 printf '%s\n' "tag $(( $(printf '%s' "$branches" | wc -w) )) tg branches" ""
364 for b in $branches; do
365 printf '%s\n' "$b"
366 done
370 printf '%s\n' "tag tg branch $branches"
372 esac | git stripspace >"$git_dir/TAG_EDITMSG"
373 if [ -z "$noedit" ]; then
375 cat <<EOT
377 # Please enter a message for tg tag:
378 # $tagname
379 # Lines starting with '#' will be ignored.
381 # tg branches to be tagged:
384 for b in $branches; do
385 printf '%s\n' "# $b"
386 done
387 } >>"$git_dir/TAG_EDITMSG"
388 stripcomments=1
389 run_editor "$git_dir/TAG_EDITMSG" || \
390 die "there was a problem with the editor '$_editor'"
393 git stripspace ${stripcomments:+ --strip-comments} \
394 <"$git_dir/TAG_EDITMSG" >"$git_dir/TGTAG_FINALMSG"
395 [ -s "$git_dir/TGTAG_FINALMSG" ] || die "no tag message?"
396 echo "" >>"$git_dir/TGTAG_FINALMSG"
397 get_refs >>"$git_dir/TGTAG_FINALMSG"
399 tagtarget=
400 case "$branches" in
401 *" "*)
402 parents="$(git merge-base --independent \
403 $(printf 'refs/heads/%s^0 ' $branches))" || \
404 die "failed: git merge-base --independent"
405 mttree="$(git hash-object -t tree -w --stdin </dev/null)"
406 tagtarget="$(printf '%s\n' "tg tag branch consolidation" "" $branches | \
407 git commit-tree $mttree $(printf -- '-p %s ' $parents))"
410 tagtarget="refs/heads/$branches^0"
412 esac
414 if [ -n "$logrefupdates" ]; then
415 mkdir -p "$git_dir/logs/$(dirname "$refname")" 2>/dev/null || :
416 { >>"$git_dir/logs/$refname" || :; } 2>/dev/null
418 if [ "$reftype" = "tag" -a -n "$signed" ]; then
419 git tag -F "$git_dir/TGTAG_FINALMSG" ${signed:+-s} ${force:+-f} \
420 ${keyid:+-u} ${keyid} "$tagname" "$tagtarget"
421 else
422 obj="$(git rev-parse --verify --quiet "$tagtarget" --)" || \
423 die "invalid object name: $tagtarget"
424 typ="$(git cat-file -t "$tagtarget" 2>/dev/null)" || \
425 die "invalid object name: $tagtarget"
426 id="$(git var GIT_COMMITTER_IDENT 2>/dev/null)" || \
427 die "could not get GIT_COMMITTER_IDENT"
428 newtag="$({
429 printf '%s\n' "object $obj" "type $typ" "tag $tagname" \
430 "tagger $id" ""
431 cat "$git_dir/TGTAG_FINALMSG"
432 } | git mktag)" || die "git mktag failed"
433 old="$(git rev-parse --verify --short --quiet "$refname" || :)"
434 updmsg=
435 case "$branches" in
436 *" "*)
437 if [ ${#branches} -le 100 ]; then
438 updmsg="$(printf '%s\n' "tgtag: $branches")"
439 else
440 updmsg="$(printf '%s\n' "tgtag: $(( $(printf '%s' "$branches" | wc -w) )) branches")"
444 updmsg="$(printf '%s\n' "tgtag: $branches")"
446 esac
447 git update-ref -m "$updmsg" "$refname" "$newtag"
448 [ -z "$old" ] || printf "Updated $reftype '%s' (was %s)\n" "$tagname" "$old"
450 rm -f "$git_dir/TAG_EDITMSG" "$git_dir/TGTAG_FINALMSG"