1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
|
#!/usr/bin/env python3
"""Update VERSION file to release and development versions"""
import sys
import datetime
from types import SimpleNamespace
import argparse
def read_version_file():
"""Return version file content as object instance with attributes"""
with open("include/VERSION", encoding="utf-8") as file:
lines = file.read().splitlines()
return SimpleNamespace(
major=lines[0], minor=lines[1], micro=lines[2], year=lines[3]
)
def write_version_file(major, minor, micro, year):
"""Write version file content from object instance with attributes"""
with open("include/VERSION", "w", encoding="utf-8") as file:
file.write(f"{major}\n")
file.write(f"{minor}\n")
file.write(f"{micro}\n")
file.write(f"{year}\n")
def is_int(value):
"""Return True if the *value* represents an integer, False otherwise"""
try:
int(value)
return True
except ValueError:
return False
def this_year():
"""Return current year"""
return datetime.date.today().year
def construct_version(version_info):
"""Construct version string from version info"""
return f"{version_info.major}.{version_info.minor}.{version_info.micro}"
def suggest_commit_from_version_file(action, tag):
"""Using information in the version file, suggest a commit message"""
version_file = read_version_file()
suggest_commit(action, construct_version(version_file), tag=tag)
def suggest_commit(action, version, tag):
"""Suggest a commit message for action and version"""
print("read:")
if tag:
print(" user_message: Use the provided messages for the commit and the tag.")
print(" note: Once all checks pass, you are expected to create a tag.")
else:
print(" user_message: Use the provided message for the commit")
print("use:")
print(f" commit_message: 'version: {action} {version}'")
if tag:
print(f" tag_message: 'GRASS GIS {version}'")
def release_candidate(args):
"""Switch to RC"""
version_file = read_version_file()
micro = version_file.micro
if micro.endswith("dev"):
micro = micro[:-3]
if not micro:
sys.exit("Creating RC from a dev micro without number is not possible")
micro = f"{micro}RC{args.number}"
else:
sys.exit(
"Creating RC from a non-dev VERSION file "
f"with micro '{micro}' is not possible"
)
write_version_file(
major=version_file.major,
minor=version_file.minor,
micro=micro,
year=this_year(),
)
suggest_commit_from_version_file("GRASS GIS", tag=True)
def release(_unused):
"""Switch to release version"""
version_file = read_version_file()
micro = version_file.micro
if micro.endswith("dev"):
micro = micro[:-3]
if not micro:
micro = 0
micro = f"{micro}"
else:
sys.exit("Creating a release from a non-dev VERSION file is not possible")
write_version_file(
major=version_file.major,
minor=version_file.minor,
micro=micro,
year=this_year(),
)
suggest_commit_from_version_file("GRASS GIS", tag=True)
def update_micro(_unused):
"""Update to next micro version"""
version_file = read_version_file()
micro = version_file.micro
if micro == "dev":
sys.exit("The micro version does not increase with development-only versions.")
# We could also add micro version when not present, but requested with:
# micro = "0dev"
elif micro.endswith("dev"):
sys.exit(f"Already dev with micro '{micro}'. Release first before update.")
elif is_int(micro):
micro = int(version_file.micro) + 1
micro = f"{micro}dev"
else:
if "RC" in micro:
sys.exit(
f"Updating micro for RC '{micro}' is not possible. "
"Release first before update."
)
sys.exit(f"Unknown micro version in VERSION file: '{micro}'")
write_version_file(
major=version_file.major,
minor=version_file.minor,
micro=micro,
year=this_year(),
)
suggest_commit_from_version_file("Start", tag=False)
def update_minor(args):
"""Update to next minor version"""
version_file = read_version_file()
micro = version_file.micro
minor = int(version_file.minor)
minor += 1
if micro.endswith("dev"):
micro = "0dev"
else:
sys.exit("Updating version from a non-dev VERSION file is not possible")
write_version_file(
major=version_file.major, minor=minor, micro=micro, year=this_year()
)
suggest_commit_from_version_file("Start", tag=False)
def update_major(_unused):
"""Update to next major version"""
version_file = read_version_file()
micro = version_file.micro
if micro.endswith("dev"):
micro = "0dev"
else:
sys.exit("Updating version from a non-dev VERSION file is not possible")
minor = 0
major = int(version_file.major) + 1
write_version_file(major=major, minor=minor, micro=micro, year=this_year())
suggest_commit_from_version_file("Start", tag=False)
def back_to_dev(_unused):
"""Switch version to development state"""
version_file = read_version_file()
micro = version_file.micro
if "RC" in micro:
micro = micro.split("RC")[0]
micro = f"{micro}dev"
action = "Back to"
elif is_int(micro):
micro = int(micro) + 1
micro = f"{micro}dev"
action = "Start"
else:
if micro.endswith("dev"):
sys.exit(f"Already dev with micro '{micro}'")
sys.exit(
"Can switch to dev only from release or RC VERSION file, "
f"not from micro '{micro}'"
)
write_version_file(
major=version_file.major,
minor=version_file.minor,
micro=micro,
year=this_year(),
)
suggest_commit_from_version_file(action, tag=False)
def status_as_yaml(version_info, today, version, tag):
"""Print VERSION file and today's date as YAML"""
print(f"today: {today}")
print(f"year: {version_info.year}")
print(f"major: {version_info.major}")
print(f"minor: {version_info.minor}")
print(f"micro: {version_info.micro}")
print(f"version: {version}")
if tag:
print(f"tag: {version}")
def status_as_bash(version_info, today, version, tag):
"""Print VERSION file and today's date as Bash eval variables"""
print(f"TODAY={today}")
print(f"YEAR={version_info.year}")
print(f"MAJOR={version_info.major}")
print(f"MINOR={version_info.minor}")
print(f"MICRO={version_info.micro}")
print(f"VERSION={version}")
if tag:
print(f"TAG={version}")
def status(args):
"""Print VERSION file and today's date"""
version_info = read_version_file()
today = datetime.date.today().isoformat()
version = construct_version(version_info)
if not version_info.micro.endswith("dev"):
tag = version
else:
tag = None
if args.bash:
status_as_bash(version_info=version_info, today=today, version=version, tag=tag)
else:
status_as_yaml(version_info=version_info, today=today, version=version, tag=tag)
def suggest_message(args):
"""Print suggestion for a commit message
Assumes that the version file was changed, but not commited yet,
but it does not check that assumption.
This shows a wrong commit message if going back from RCs,
but it is not likely this is needed because the suggestion
will be part of the message for the switch and there is
no other work to do afterwards except for the commit
(unlike updating the version number).
"""
version_info = read_version_file()
if not version_info.micro.endswith("dev"):
tag = construct_version(version_info)
action = "GRASS GIS"
else:
tag = None
action = "Start"
suggest_commit_from_version_file(action, tag=tag)
def main():
"""Translate sub-commands to function calls"""
parser = argparse.ArgumentParser(
description="Update VERSION file using the specified action.",
epilog="Run in the root directory to access the VERSION file.",
)
subparsers = parser.add_subparsers(dest="command", required=True)
subparser = subparsers.add_parser(
"rc", help="switch to release candidate (no dev suffix)"
)
subparser.add_argument(
"number", type=int, help="RC number (number sequence not checked)"
)
subparser.set_defaults(func=release_candidate)
subparser = subparsers.add_parser(
"dev", help="switch to development state (attaches dev suffix)"
)
subparser.set_defaults(func=back_to_dev)
subparser = subparsers.add_parser(
"release", help="switch to release version (no dev suffix)"
)
subparser.set_defaults(func=release)
subparser = subparsers.add_parser(
"major", help="increase major (X.y.z) version (attaches dev suffix)"
)
subparser.set_defaults(func=update_major)
subparser = subparsers.add_parser(
"minor", help="increase minor (x.Y.z) version (uses dev in micro)"
)
subparser.set_defaults(func=update_minor)
subparser = subparsers.add_parser(
"micro", help="increase micro (x.y.Z, aka patch) version (attaches dev suffix)"
)
subparser.set_defaults(func=update_micro)
subparser = subparsers.add_parser(
"status", help="show status of VERSION file (as YAML by default)"
)
subparser.add_argument(
"--bash", action="store_true", help="format as Bash variables for eval"
)
subparser.set_defaults(func=status)
subparser = subparsers.add_parser(
"suggest", help="suggest a commit message for new version"
)
subparser.set_defaults(func=suggest_message)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()
|