Skip to content

Commit e15fb35

Browse files
authored
feat(tag-expiry): Add tag expiry command (#18)
1 parent 0d0ef8b commit e15fb35

File tree

5 files changed

+235
-0
lines changed

5 files changed

+235
-0
lines changed

Makefile

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ lint: bin
3434
shellcheck bin/git-gau-autoclean
3535
shellcheck bin/git-gau-automerge
3636
shellcheck bin/git-gau-exec
37+
shellcheck bin/git-gau-tag-expiry
3738
shellcheck bin/git-gau-xargs
3839
shellcheck lib/docker-entry
3940
shellcheck lib/docker-entry.d/50-git-credentials
@@ -50,6 +51,7 @@ doc: \
5051
doc/git-gau-automerge.1 \
5152
doc/git-gau-docker-entry.1 \
5253
doc/git-gau-exec.1 \
54+
doc/git-gau-tag-expiry.1 \
5355
doc/git-gau-xargs.1
5456

5557
clean:
@@ -59,6 +61,7 @@ clean:
5961
-rm -f doc/git-gau-automerge.1
6062
-rm -f doc/git-gau-docker-entry.1
6163
-rm -f doc/git-gau-exec.1
64+
-rm -f doc/git-gau-tag-expiry.1
6265
-rm -f doc/git-gau-xargs.1
6366
-rm -rf dist
6467
-rm -rf build
@@ -70,6 +73,7 @@ install-doc: doc
7073
install -m 0644 -D doc/git-gau-automerge.1 $(DESTDIR)$(mandir)/man1/git-gau-automerge.1
7174
install -m 0644 -D doc/git-gau-docker-entry.1 $(DESTDIR)$(mandir)/man1/git-gau-docker-entry.1
7275
install -m 0644 -D doc/git-gau-exec.1 $(DESTDIR)$(mandir)/man1/git-gau-exec.1
76+
install -m 0644 -D doc/git-gau-tag-expiry.1 $(DESTDIR)$(mandir)/man1/git-gau-tag-expiry.1
7377
install -m 0644 -D doc/git-gau-xargs.1 $(DESTDIR)$(mandir)/man1/git-gau-xargs.1
7478

7579
install-bin: bin
@@ -78,6 +82,7 @@ install-bin: bin
7882
install -m 0755 -D bin/git-gau-autoclean $(DESTDIR)$(bindir)/git-gau-autoclean
7983
install -m 0755 -D bin/git-gau-automerge $(DESTDIR)$(bindir)/git-gau-automerge
8084
install -m 0755 -D bin/git-gau-exec $(DESTDIR)$(bindir)/git-gau-exec
85+
install -m 0755 -D bin/git-gau-tag-expiry $(DESTDIR)$(bindir)/git-gau-tag-expiry
8186
install -m 0755 -D bin/git-gau-xargs $(DESTDIR)$(bindir)/git-gau-xargs
8287
install -m 0755 -D lib/docker-entry $(DESTDIR)$(libdir)/git-gau/docker-entry
8388
install -m 0755 -D lib/docker-entry.d/50-git-credentials $(DESTDIR)$(libdir)/git-gau/docker-entry.d/50-git-credentials
@@ -92,6 +97,7 @@ uninstall:
9297
-rm -f $(DESTDIR)$(bindir)/git-gau-autoclean
9398
-rm -f $(DESTDIR)$(bindir)/git-gau-automerge
9499
-rm -f $(DESTDIR)$(bindir)/git-gau-exec
100+
-rm -f $(DESTDIR)$(bindir)/git-gau-tag-expiry
95101
-rm -f $(DESTDIR)$(bindir)/git-gau-xargs
96102
-rm -f $(DESTDIR)$(libdir)/git-gau/docker-entry
97103
-rm -f $(DESTDIR)$(libdir)/git-gau/docker-entry.d/50-git-credentials
@@ -103,6 +109,7 @@ uninstall:
103109
-rm -f $(DESTDIR)$(mandir)/man1/git-gau-automerge.1
104110
-rm -f $(DESTDIR)$(mandir)/man1/git-gau-docker-entry.1
105111
-rm -f $(DESTDIR)$(mandir)/man1/git-gau-exec.1
112+
-rm -f $(DESTDIR)$(mandir)/man1/git-gau-tag-expiry.1
106113
-rm -f $(DESTDIR)$(mandir)/man1/git-gau-xargs.1
107114

108115
dist-bin:

bin/git-gau-tag-expiry

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/bin/sh
2+
#
3+
# git-gau-tag-expiry - Run a command if the most recent tag is older than n
4+
# seconds.
5+
6+
set -e
7+
set -u
8+
9+
# Required binaries
10+
ECHO=/bin/echo
11+
GIT=/usr/bin/git
12+
DATE=/bin/date
13+
14+
# Print usage message and exit.
15+
usage() {
16+
${ECHO} "${0}: tag-pattern seconds command [args...]" >&2
17+
exit 1
18+
}
19+
20+
# Run a command if mast recent tag is older than n seconds.
21+
#
22+
# $1: Tag pattern. May contain branch placeholder (%b)
23+
# $2: Maximum time to last tag.
24+
# $@: Command [args...]
25+
gau_tag_expiry() {
26+
PATTERN="${1}"
27+
THRESHOLD="${2}"
28+
shift 2
29+
30+
# Substitute branch placeholder (%b) in PATTERN
31+
case "${PATTERN}" in
32+
*%b*)
33+
HEAD="${PATTERN%%%b*}"
34+
TAIL="${PATTERN#*%b}"
35+
PATTERN="${HEAD}$(${GIT} rev-parse --abbrev-ref HEAD)${TAIL}"
36+
;;
37+
esac
38+
39+
# Select the most recent annotated tag matching the given PATTERN and store
40+
# its unix timestamp into the TAG_DATE var.
41+
TAG_DATE=$(${GIT} for-each-ref --count=1 --sort=-taggerdate \
42+
--format="%(taggerdate:unix)" "refs/tags/${PATTERN}")
43+
TAG_EXPIRY=$(($(${DATE} +%s) - THRESHOLD))
44+
45+
# Run command if no tag exists or if it is too old.
46+
if [ -z "${TAG_DATE}" ] || [ "${TAG_DATE}" -gt ${TAG_EXPIRY} ]; then
47+
"$@"
48+
fi
49+
}
50+
51+
if [ "${#}" -gt 3 ] && [ "${1:-}" != "-h" ] && [ "${1:-}" != "--help" ]; then
52+
gau_tag_expiry "${@}"
53+
else
54+
usage
55+
fi

doc/git-gau-tag-expiry.1.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
% git-gau-tag-expiry(1) Git Gau User Manuals
2+
% Lorenz Schori
3+
% March 15, 2021
4+
5+
# NAME
6+
7+
git-gau-tag-expiry - Run a command if the most recent tag is older than n seconds.
8+
9+
# SYNOPSIS
10+
11+
git-gau-tag-expiry tag-pattern seconds command [*args*]...
12+
13+
# DESCRIPTION
14+
15+
Runs the specified command unless a tag is found matching the given
16+
*tag-pattern* which was created no longer than specified *seconds* ago.
17+
18+
The syntax for *tag-pattern* is the same as for `git-for-each-ref`.
19+
Additionally the `%b` placeholder is replaced by the currently checked out
20+
branch.
21+
22+
Note that this command only considers annotated tags. Lightweight tags are
23+
ignored.
24+
25+
# EXAMPLES
26+
27+
Check whether there was a build of the current branch during the past week.
28+
29+
git gau-tag-expire builds/%b/* 604800 \
30+
echo "Last build is older than one week"
31+
32+
# SEE ALSO
33+
34+
`git-gau-at` (1).
35+
`git-tag` (1).
36+
`git-for-each-ref` (1).

test/__main__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from .gauautoclean import *
88
from .gauautomerge import *
99
from .gauexec import *
10+
from .gautagexpiry import *
1011
from .gauxargs import *
1112

1213
unittest.main()

test/gautagexpiry.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
from __future__ import absolute_import, division, unicode_literals
2+
3+
import os
4+
import datetime
5+
import shutil
6+
import subprocess
7+
import tempfile
8+
import unittest
9+
10+
class TagExpiryTestCase(unittest.TestCase):
11+
repodir = None
12+
t3 = datetime.datetime.now()-datetime.timedelta(days=3)
13+
t7 = datetime.datetime.now()-datetime.timedelta(days=7)
14+
t10 = datetime.datetime.now()-datetime.timedelta(days=10)
15+
16+
def _cmd(self, *args, **kwds):
17+
kwds.setdefault('cwd', self.repodir)
18+
return subprocess.check_output(args, **kwds)
19+
20+
def setUp(self):
21+
self.repodir = tempfile.mkdtemp()
22+
self._cmd('git', 'init')
23+
self._cmd('git', 'config', 'user.name', 'Test')
24+
self._cmd('git', 'config', 'user.email', 'test@localhost')
25+
self._cmd('git', 'commit', '--allow-empty', '-m', 'Initial commit')
26+
27+
# Create annotated tag seven days ago with defaults from git-gau-at.
28+
branch = self._cmd('git', 'rev-parse', '--abbrev-ref', '--symbolic', 'HEAD')
29+
datestr = self.t7.astimezone(datetime.timezone.utc).strftime("%Y%m%dT%H%M%SZ")
30+
tagname = "/".join(['build', branch.rstrip().decode(), datestr])
31+
32+
env = os.environ.copy()
33+
env.update({
34+
'GIT_COMMITTER_DATE': self.t7.isoformat()
35+
})
36+
self._cmd('git', 'tag', '--annotate', '--message', 'Most recent build', tagname, env=env)
37+
38+
# Create another tag three days ago with different pattern.
39+
env = os.environ.copy()
40+
env.update({
41+
'GIT_COMMITTER_DATE': self.t3.isoformat()
42+
})
43+
self._cmd('git', 'tag', '--annotate', '--message', 'Most recent release', 'release/v2.0.3', env=env)
44+
45+
# Create another tag ten days ago with different pattern.
46+
env = os.environ.copy()
47+
env.update({
48+
'GIT_COMMITTER_DATE': self.t10.isoformat()
49+
})
50+
self._cmd('git', 'tag', '--annotate', '--message', 'Most recent release', 'release/v2.0.2')
51+
52+
def tearDown(self):
53+
if self.repodir is not None:
54+
shutil.rmtree(self.repodir)
55+
56+
def testExpirySimple(self):
57+
"""
58+
Static tag with simple script resulting in a message.
59+
"""
60+
61+
script = 'echo Build expired!'
62+
63+
branch = self._cmd('git', 'rev-parse', '--abbrev-ref', '--symbolic', 'HEAD')
64+
pattern = '/'.join(['build', branch.rstrip().decode(), '*'])
65+
66+
# with ttl=two days, no expiry expected
67+
d2 = int(datetime.timedelta(days=2).total_seconds())
68+
output = self._cmd('git', 'gau-tag-expiry', pattern, str(d2), '/bin/sh', '-c', script)
69+
self.assertEqual(output, b'')
70+
71+
# with ttl=six days, no expiry expected
72+
d6 = int(datetime.timedelta(days=6).total_seconds())
73+
output = self._cmd('git', 'gau-tag-expiry', pattern, str(d6), '/bin/sh', '-c', script)
74+
self.assertEqual(output, b'')
75+
76+
# with ttl=eight days, expiry expected
77+
d8 = int(datetime.timedelta(days=8).total_seconds())
78+
output = self._cmd('git', 'gau-tag-expiry', pattern, str(d8), '/bin/sh', '-c', script)
79+
self.assertEqual(output, b'Build expired!\n')
80+
81+
def testExpiryBranchPlaceholder(self):
82+
"""
83+
Tag pattern with branch placeholder with simple script resulting in a message.
84+
"""
85+
86+
script = 'echo Build expired!'
87+
88+
pattern = 'build/%b/*'
89+
90+
# with ttl=two days, no expiry expected
91+
d2 = int(datetime.timedelta(days=2).total_seconds())
92+
output = self._cmd('git', 'gau-tag-expiry', pattern, str(d2), '/bin/sh', '-c', script)
93+
self.assertEqual(output, b'')
94+
95+
# with ttl=six days, no expiry expected
96+
d6 = int(datetime.timedelta(days=6).total_seconds())
97+
output = self._cmd('git', 'gau-tag-expiry', pattern, str(d6), '/bin/sh', '-c', script)
98+
self.assertEqual(output, b'')
99+
100+
# with ttl=eight days, expiry expected
101+
d8 = int(datetime.timedelta(days=8).total_seconds())
102+
output = self._cmd('git', 'gau-tag-expiry', pattern, str(d8), '/bin/sh', '-c', script)
103+
self.assertEqual(output, b'Build expired!\n')
104+
105+
def testExpiryNoMatchingTag(self):
106+
"""
107+
Static tag with simple script resulting in a message.
108+
"""
109+
110+
script = 'echo Build expired!'
111+
112+
pattern = 'no-matching-category/*'
113+
114+
# with ttl=two days, expiry expected
115+
d2 = int(datetime.timedelta(days=2).total_seconds())
116+
output = self._cmd('git', 'gau-tag-expiry', pattern, str(d2), '/bin/sh', '-c', script)
117+
self.assertEqual(output, b'Build expired!\n')
118+
119+
# with ttl=six days, expiry expected
120+
d6 = int(datetime.timedelta(days=6).total_seconds())
121+
output = self._cmd('git', 'gau-tag-expiry', pattern, str(d6), '/bin/sh', '-c', script)
122+
self.assertEqual(output, b'Build expired!\n')
123+
124+
# with ttl=eight days, expiry expected
125+
d8 = int(datetime.timedelta(days=8).total_seconds())
126+
output = self._cmd('git', 'gau-tag-expiry', pattern, str(d8), '/bin/sh', '-c', script)
127+
self.assertEqual(output, b'Build expired!\n')
128+
129+
def testFailOnExitStatusNonZero(self):
130+
"""
131+
Command with non-zero status will propagate.
132+
"""
133+
134+
script = 'echo Expiry check failed; /bin/false'
135+
self.assertRaises(subprocess.CalledProcessError, self._cmd,
136+
'git', 'gau-tag-expiry', '*', '-c', script)

0 commit comments

Comments
 (0)