Skip to content

Commit 3402691

Browse files
illwieckzslipher
andcommitted
tools: add repo-version to print a package version from git references
Co-authored-by: slipher <slipher@protonmail.com>
1 parent c121f2f commit 3402691

3 files changed

Lines changed: 165 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: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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 GitComputeVersion():
36+
git_short_ref_length = 7
37+
38+
def __init__(self, source_dir, allows_stray, is_quiet, is_local):
39+
if not os.path.isdir(source_dir):
40+
raise(ValueError, "not a directory")
41+
42+
self.process_stderr = None
43+
44+
if is_quiet:
45+
self.process_stderr = subprocess.DEVNULL
46+
47+
self.is_local = is_local
48+
49+
self.source_dir_realpath = os.path.realpath(source_dir)
50+
51+
self.git_command_list = ["git", "-C", self.source_dir_realpath]
52+
53+
# Test that Git is available and working.
54+
self.runGitCommand(["-v"])
55+
56+
self.allows_stray = allows_stray
57+
58+
def runGitCommand(self, command_list):
59+
command_list = self.git_command_list + command_list
60+
61+
process = subprocess.run(command_list,
62+
stdout=subprocess.PIPE, stderr=self.process_stderr, text=True, check=True)
63+
64+
return process.stdout.rstrip()
65+
66+
def getDateString(self, timestamp):
67+
return datetime.datetime.fromtimestamp(timestamp, datetime.UTC).strftime('%Y%m%d-%H%M%S')
68+
69+
def isGitDirty(self):
70+
if self.is_local:
71+
lookup_dir_arg = [self.source_dir_realpath]
72+
else:
73+
lookup_dir_arg = []
74+
75+
git_status_porcelain_string = self.runGitCommand(
76+
["status", "--porcelain", "--"] + lookup_dir_arg)
77+
78+
return git_status_porcelain_string != ""
79+
80+
def getVersionString(self):
81+
# Fallback version string for stray repositories (not tracked by git yet).
82+
tag_string="0"
83+
date_string="-" + self.getDateString(time.time())
84+
ref_string=""
85+
dirt_string="+stray"
86+
87+
# Git returns an error if the directory is not a Git repository or it has no
88+
# commits. It also may return an error if the repo is broken somehow. We fail
89+
# to distinguish these cases and assume it is not a Git repository.
90+
try:
91+
git_last_commit_string = self.runGitCommand(
92+
["rev-parse", "HEAD", "--"])
93+
except subprocess.CalledProcessError:
94+
if not self.allows_stray:
95+
raise
96+
return tag_string + date_string + ref_string + dirt_string
97+
98+
# Now we know we must use a Git-based version string.
99+
dirt_string = ""
100+
101+
# Git prints the current commit reference.
102+
git_last_commit_short_string = git_last_commit_string[:self.git_short_ref_length]
103+
ref_string = "-" + git_last_commit_short_string
104+
105+
# Git prints the current commit date.
106+
git_last_commit_timestamp_string = self.runGitCommand(
107+
["log", "-1", "--pretty=format:%ct"])
108+
date_string = "-" + self.getDateString(int(git_last_commit_timestamp_string))
109+
110+
# Git prints the most recent tag, or a commit hash if there is no tag at all.
111+
git_closest_tag_string = self.runGitCommand(
112+
["describe", "--always", "--tags", "--abbrev=0", "--match", "v[0-9]*"])
113+
114+
# If a tag is found:
115+
if git_closest_tag_string[0] == "v":
116+
git_closest_tag_version_string = git_closest_tag_string[1:]
117+
tag_string = git_closest_tag_version_string
118+
119+
# Git prints a version string that is equal to the most recent tag
120+
# if the most recent tag is on the current commit.
121+
git_describe_tag_string = self.runGitCommand(
122+
["describe", "--tags", "--match", "v[0-9]*"])
123+
git_describe_version_string = git_describe_tag_string[1:]
124+
125+
if git_closest_tag_version_string == git_describe_version_string:
126+
# Do not write current commit reference and date in version
127+
# string if the tag is on the current commit.
128+
date_string = ""
129+
ref_string = ""
130+
131+
if self.isGitDirty():
132+
# Write the dirty flag in version string if not everything in
133+
# the Git repository is properly committed.
134+
dirt_string = "+dirty"
135+
136+
return tag_string + date_string + ref_string + dirt_string
137+
138+
def getVersionString(source_dir, allows_stray=False, is_quiet=False, is_local=False):
139+
return GitComputeVersion(source_dir, allows_stray, is_quiet, is_local).getVersionString()
140+
141+
def main():
142+
import argparse
143+
144+
def existing_dir(path):
145+
if not os.path.isdir(path):
146+
raise argparse.ArgumentTypeError(f"{path} is not an existing directory")
147+
return path
148+
149+
parser = argparse.ArgumentParser(description="Print repository version string")
150+
151+
parser.add_argument("-s", "--stray", dest="allows_stray", help="allows stray repositories not tracked by Git", action="store_true")
152+
parser.add_argument("-q", "--quiet", dest="is_quiet", help="silence Git errors", action="store_true")
153+
parser.add_argument("-l", "--local", dest="is_local", help="look for dirtiness in given directory only, not in whole repository", action="store_true")
154+
parser.add_argument(dest="source_dir", nargs="?", metavar="DIRNAME", type=existing_dir, default=".", help="repository path")
155+
156+
args = parser.parse_args()
157+
158+
print(getVersionString(args.source_dir, allows_stray=args.allows_stray, is_quiet=args.is_quiet, is_local=args.is_local))
159+
160+
if __name__ == "__main__":
161+
main()
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 GitComputeVersion import main
3+
main()

0 commit comments

Comments
 (0)