Skip to content

Commit f983698

Browse files
committed
tools: add repo-version to print a package version from git references
1 parent 6bf8baa commit f983698

3 files changed

Lines changed: 177 additions & 0 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ build
2020
*.dylib
2121
*.so
2222
*.a
23+
__pycache__
2324

2425
#ignore editor temporary files
2526
.*.swp
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
#! /usr/bin/env python3
2+
3+
# Daemon BSD Source Code
4+
# Copyright (c) 2024-2026, Daemon Developers
5+
# All rights reserved.
6+
#
7+
# Redistribution and use in source and binary forms, with or without
8+
# modification, are permitted provided that the following conditions are met:
9+
# * Redistributions of source code must retain the above copyright
10+
# notice, this list of conditions and the following disclaimer.
11+
# * Redistributions in binary form must reproduce the above copyright
12+
# notice, this list of conditions and the following disclaimer in the
13+
# documentation and/or other materials provided with the distribution.
14+
# * Neither the name of the <organization> nor the
15+
# names of its contributors may be used to endorse or promote products
16+
# derived from this software without specific prior written permission.
17+
#
18+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19+
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20+
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21+
# DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
22+
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23+
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24+
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25+
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26+
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27+
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28+
29+
import datetime
30+
import os.path
31+
import subprocess
32+
import sys
33+
import time
34+
35+
class _RepositoryVersionString():
36+
git_short_ref_length = 7
37+
is_permissive = False
38+
39+
def __init__(self, source_dir, is_permissive, is_quiet, is_local):
40+
if not os.path.isdir(source_dir):
41+
raise(ValueError, "not a directory")
42+
43+
self.process_stderr = None
44+
45+
if is_quiet:
46+
self.process_stderr = subprocess.DEVNULL
47+
48+
self.is_local = is_local
49+
50+
self.source_dir_realpath = os.path.realpath(source_dir)
51+
52+
self.git_command_list = ["git", "-C", self.source_dir_realpath]
53+
54+
# Test that Git is available and working.
55+
self.runGitCommand(["-v"])
56+
57+
self.is_permissive = is_permissive
58+
59+
def runGitCommand(self, command_list, is_permissive=False):
60+
command_list = self.git_command_list + command_list
61+
62+
process_check = not (self.is_permissive or is_permissive)
63+
64+
process = subprocess.run(command_list,
65+
stdout=subprocess.PIPE, stderr=self.process_stderr, check=process_check, text=True)
66+
67+
return process.stdout.rstrip(), process.returncode
68+
69+
def getDateString(self, timestamp):
70+
return datetime.datetime.fromtimestamp(timestamp, datetime.UTC).strftime('%Y%m%d-%H%M%S')
71+
72+
def isDirtyGit(self):
73+
if self.is_local:
74+
lookup_dir = self.source_dir_realpath
75+
else:
76+
# Git prints the Git repository root directory.
77+
git_show_toplevel_string, git_show_toplevel_returncode = \
78+
self.runGitCommand(["rev-parse", "--show-toplevel"])
79+
80+
lookup_dir = git_show_toplevel_string.splitlines()[0]
81+
82+
# Git returns 1 if there is at least one modified file in the given directory.
83+
git_diff_quiet_string, git_diff_quiet_returncode \
84+
= self.runGitCommand(["diff", "--quiet", lookup_dir], is_permissive=True)
85+
86+
if git_diff_quiet_returncode != 0:
87+
return True
88+
89+
# Git prints the list of untracked files in the given directory.
90+
git_ls_untracked_string, git_ls_untracked_returncode \
91+
= self.runGitCommand(["ls-files", "-z", "--others", "--exclude-standard", lookup_dir])
92+
93+
untracked_file_list = git_ls_untracked_string.split('\0')[:-1]
94+
95+
return len(untracked_file_list) > 0
96+
97+
def getVersionString(self):
98+
# Fallback version string.
99+
tag_string="0"
100+
date_string="-" + self.getDateString(time.time())
101+
ref_string=""
102+
dirt_string="+dirty"
103+
104+
# Git returns 1 if the directory is not a Git repository.
105+
git_last_commit_string, git_last_commit_returncode \
106+
= self.runGitCommand(["rev-parse", "HEAD", "--"])
107+
108+
# Git-based version string.
109+
if git_last_commit_returncode == 0:
110+
# Git prints the current commit reference.
111+
git_last_commit_short_string = git_last_commit_string[:self.git_short_ref_length]
112+
ref_string = "-" + git_last_commit_short_string
113+
114+
# Git prints the current commit date.
115+
git_last_commit_timestamp_string, git_last_commit_timestamp_returncode \
116+
= self.runGitCommand(["log", "-1", "--pretty=format:%ct"])
117+
118+
if git_last_commit_timestamp_returncode == 0:
119+
date_string = "-" + self.getDateString(int(git_last_commit_timestamp_string))
120+
121+
# Git prints the most recent tag or returns 1 if there is not tag at all.
122+
git_closest_tag_string, git_closest_tag_returncode \
123+
= self.runGitCommand(["describe", "--tags", "--abbrev=0", "--match", "v[0-9].*"])
124+
125+
if git_closest_tag_returncode == 0:
126+
git_closest_tag_version_string = git_closest_tag_string[1:]
127+
tag_string = git_closest_tag_version_string
128+
129+
# Git prints a version string that is equal to the most recent tag
130+
# if the most recent tag is on the current commit or returns 1 if
131+
# there is no tag at all.
132+
git_describe_tag_string, git_describe_tag_returncode \
133+
= self.runGitCommand(["describe", "--tags", "--match", "v[0-9].*"])
134+
git_describe_version_string = git_describe_tag_string[1:]
135+
136+
if git_describe_tag_returncode == 0:
137+
if git_closest_tag_version_string == git_describe_version_string:
138+
# Do not write current commit reference and date in version
139+
# string if the tag is on the current commit.
140+
date_string = ""
141+
ref_string = ""
142+
143+
if not self.isDirtyGit():
144+
# Do not write the dirty flag in version string if everything in
145+
# the Git repository is properly committed.
146+
dirt_string = ""
147+
148+
return tag_string + date_string + ref_string + dirt_string
149+
150+
def getVersionString(source_dir, is_permissive=False, is_quiet=False, is_local=False):
151+
return _RepositoryVersionString(source_dir, is_permissive, is_quiet, is_local).getVersionString()
152+
153+
def main():
154+
import argparse
155+
156+
def existing_dir(path):
157+
if not os.path.isdir(path):
158+
raise argparse.ArgumentTypeError(f"{path} is not an existing directory")
159+
return path
160+
161+
parser = argparse.ArgumentParser(description="Print repository version string")
162+
163+
parser.add_argument("-p", "--permissive", dest="is_permissive", help="ignore Git errors", action="store_true")
164+
parser.add_argument("-q", "--quiet", dest="is_quiet", help="silence Git errors", action="store_true")
165+
parser.add_argument("-w", "--local", dest="is_local", help="look for dirt in given directory only, not in whole repository", action="store_true")
166+
parser.add_argument(dest="source_dir", nargs="?", metavar="DIRNAME", type=existing_dir, default=".", help="repository path")
167+
168+
args = parser.parse_args()
169+
170+
print(getVersionString(args.source_dir, is_permissive=args.is_permissive, is_quiet=args.is_quiet, is_local=args.is_local))
171+
172+
if __name__ == "__main__":
173+
main()

tools/repo-version/repo-version

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#! /usr/bin/env python3
2+
from RepositoryVersionString import main
3+
main()

0 commit comments

Comments
 (0)