misc.python.materialize.git
Git utilities.
1# Copyright Materialize, Inc. and contributors. All rights reserved. 2# 3# Use of this software is governed by the Business Source License 4# included in the LICENSE file at the root of this repository. 5# 6# As of the Change Date specified in that file, in accordance with 7# the Business Source License, use of this software will be governed 8# by the Apache License, Version 2.0. 9 10"""Git utilities.""" 11 12import functools 13import os 14import subprocess 15import sys 16from pathlib import Path 17from typing import TypeVar 18 19import requests 20 21from materialize import spawn 22from materialize.mz_version import MzVersion, TypedVersionBase 23from materialize.util import YesNoOnce 24 25VERSION_TYPE = TypeVar("VERSION_TYPE", bound=TypedVersionBase) 26 27MATERIALIZE_REMOTE_URL = "https://github.com/MaterializeInc/materialize" 28 29fetched_tags_in_remotes: set[str | None] = set() 30 31 32def get_config(key: str) -> str | None: 33 """Read a git config value, returning None if unset.""" 34 try: 35 return spawn.capture(["git", "config", key]).strip() 36 except subprocess.CalledProcessError: 37 return None 38 39 40def get_user_name() -> str | None: 41 """Get the configured git user.name.""" 42 return get_config("user.name") 43 44 45def get_user_email() -> str | None: 46 """Get the configured git user.email.""" 47 return get_config("user.email") 48 49 50def rev_count(rev: str) -> int: 51 """Count the commits up to a revision. 52 53 Args: 54 rev: A Git revision in any format know to the Git CLI. 55 56 Returns: 57 count: The number of commits in the Git repository starting from the 58 initial commit and ending with the specified commit, inclusive. 59 """ 60 return int(spawn.capture(["git", "rev-list", "--count", rev, "--"]).strip()) 61 62 63def get_first_parent_commits(rev: str, limit: int) -> list[str]: 64 """Get commit hashes along the first-parent chain starting from rev. 65 66 Returns up to `limit` commit hashes (including rev itself), following 67 only first parents (i.e., staying on the main branch). 68 """ 69 return ( 70 spawn.capture(["git", "rev-list", "--first-parent", f"-{limit}", rev]) 71 .strip() 72 .splitlines() 73 ) 74 75 76def rev_parse(rev: str, *, abbrev: bool = False) -> str: 77 """Compute the hash for a revision. 78 79 Args: 80 rev: A Git revision in any format known to the Git CLI. 81 abbrev: Return a branch or tag name instead of a git sha 82 83 Returns: 84 ref: A 40 character hex-encoded SHA-1 hash representing the ID of the 85 named revision in Git's object database. 86 87 With "abbrev=True" this will return an abbreviated ref, or throw an 88 error if there is no abbrev. 89 """ 90 a = ["--abbrev-ref"] if abbrev else [] 91 out = spawn.capture(["git", "rev-parse", *a, "--verify", rev]).strip() 92 if not out: 93 raise RuntimeError(f"No parsed rev for {rev}") 94 return out 95 96 97@functools.cache 98def expand_globs(root: Path, *specs: Path | str) -> set[str]: 99 """Find unignored files within the specified paths.""" 100 # The goal here is to find all files in the working tree that are not 101 # ignored by .gitignore. Naively using `git ls-files` doesn't work, because 102 # it reports files that have been deleted in the working tree if they are 103 # still present in the index. Using `os.walkdir` doesn't work because there 104 # is no good way to evaluate .gitignore rules from Python. So we use a 105 # combination of `git diff` and `git ls-files`. 106 107 # `git diff` against the empty tree surfaces all tracked files that have 108 # not been deleted. 109 empty_tree = ( 110 "4b825dc642cb6eb9a060e54bf8d69288fbee4904" # git hash-object -t tree /dev/null 111 ) 112 diff_files = spawn.capture( 113 ["git", "diff", "--name-only", "-z", "--relative", empty_tree, "--", *specs], 114 cwd=root, 115 ) 116 117 # `git ls-files --others --exclude-standard` surfaces any non-ignored, 118 # untracked files, which are not included in the `git diff` output above. 119 ls_files = spawn.capture( 120 ["git", "ls-files", "--others", "--exclude-standard", "-z", "--", *specs], 121 cwd=root, 122 ) 123 124 return set(f for f in (diff_files + ls_files).split("\0") if f.strip() != "") 125 126 127def get_version_tags( 128 *, 129 version_type: type[VERSION_TYPE], 130 newest_first: bool = True, 131 fetch: bool = True, 132 remote_url: str = MATERIALIZE_REMOTE_URL, 133) -> list[VERSION_TYPE]: 134 """List all the version-like tags in the repo 135 136 Args: 137 fetch: If false, don't automatically run `git fetch --tags`. 138 prefix: A prefix to strip from each tag before attempting to parse the 139 tag as a version. 140 """ 141 if fetch: 142 _fetch( 143 remote=get_remote(remote_url), 144 include_tags=YesNoOnce.ONCE, 145 force=True, 146 only_tags=True, 147 ) 148 tags = [] 149 for t in spawn.capture(["git", "tag"]).splitlines(): 150 if not t.startswith(version_type.get_prefix()): 151 continue 152 try: 153 tags.append(version_type.parse(t)) 154 except ValueError as e: 155 print(f"WARN: {e}", file=sys.stderr) 156 157 return sorted(tags, reverse=newest_first) 158 159 160def get_latest_version( 161 version_type: type[VERSION_TYPE], 162 excluded_versions: set[VERSION_TYPE] | None = None, 163 current_version: VERSION_TYPE | None = None, 164) -> VERSION_TYPE: 165 all_version_tags: list[VERSION_TYPE] = get_version_tags( 166 version_type=version_type, fetch=True 167 ) 168 169 if excluded_versions is not None: 170 all_version_tags = [ 171 v 172 for v in all_version_tags 173 if v not in excluded_versions 174 and (not current_version or v < current_version) 175 ] 176 177 return max(all_version_tags) 178 179 180def get_tags_of_current_commit(include_tags: YesNoOnce = YesNoOnce.ONCE) -> list[str]: 181 if include_tags: 182 fetch(get_remote(), include_tags=include_tags, only_tags=True) 183 184 result = spawn.capture(["git", "tag", "--points-at", "HEAD"]) 185 186 if len(result) == 0: 187 return [] 188 189 return result.splitlines() 190 191 192def is_ancestor(earlier: str, later: str) -> bool: 193 """True if earlier is in an ancestor of later""" 194 try: 195 headers = {"Accept": "application/vnd.github+json"} 196 if token := os.getenv("GITHUB_TOKEN"): 197 headers["Authorization"] = f"Bearer {token}" 198 199 resp = requests.get( 200 f"https://api.github.com/repos/materializeinc/materialize/compare/{earlier}...{later}", 201 headers=headers, 202 ) 203 resp.raise_for_status() 204 data = resp.json() 205 return data.get("status") in ("ahead", "identical") 206 except Exception as e: 207 # Try locally if Github is down or the change has not been pushed yet when running locally 208 print(f"Failed to get ancestor status from Github, running locally: {e}") 209 210 # Make sure we have an up to date view of main. 211 command = ["git", "fetch"] 212 if spawn.capture(["git", "rev-parse", "--is-shallow-repository"]) == "true": 213 command.append("--unshallow") 214 spawn.runv(command + [get_remote(), earlier, later]) 215 216 return ( 217 spawn.run_and_get_return_code( 218 ["git", "merge-base", "--is-ancestor", earlier, later] 219 ) 220 == 0 221 ) 222 223 224def is_dirty() -> bool: 225 """Check if the working directory has modifications to tracked files""" 226 proc = subprocess.run("git diff --no-ext-diff --quiet --exit-code".split()) 227 idx = subprocess.run("git diff --cached --no-ext-diff --quiet --exit-code".split()) 228 return proc.returncode != 0 or idx.returncode != 0 229 230 231def describe() -> str: 232 """Describe the relationship between the current commit and the most recent tag""" 233 return spawn.capture(["git", "describe"]).strip() 234 235 236def fetch( 237 remote: str | None = None, 238 all_remotes: bool = False, 239 include_tags: YesNoOnce = YesNoOnce.NO, 240 force: bool = False, 241 branch: str | None = None, 242 only_tags: bool = False, 243) -> str: 244 """Fetch from remotes""" 245 246 if remote is not None and all_remotes: 247 raise RuntimeError("all_remotes must be false when a remote is specified") 248 249 if branch is not None and remote is None: 250 raise RuntimeError("remote must be specified when a branch is specified") 251 252 if branch is not None and only_tags: 253 raise RuntimeError("branch must not be specified if only_tags is set") 254 255 command = ["git", "fetch"] 256 if spawn.capture(["git", "rev-parse", "--is-shallow-repository"]) == "true": 257 command.append("--unshallow") 258 259 if remote: 260 command.append(remote) 261 262 if branch: 263 command.append(branch) 264 265 if all_remotes: 266 command.append("--all") 267 268 fetch_tags = ( 269 include_tags == YesNoOnce.YES 270 # fetch tags again if used with force (tags might have changed) 271 or (include_tags == YesNoOnce.ONCE and force) 272 or ( 273 include_tags == YesNoOnce.ONCE 274 and remote not in fetched_tags_in_remotes 275 and "*" not in fetched_tags_in_remotes 276 ) 277 ) 278 279 if fetch_tags: 280 command.append("--tags") 281 282 if force: 283 command.append("--force") 284 285 if not fetch_tags and only_tags: 286 return "" 287 288 output = spawn.capture(command).strip() 289 290 if fetch_tags: 291 fetched_tags_in_remotes.add(remote) 292 293 if all_remotes: 294 fetched_tags_in_remotes.add("*") 295 296 return output 297 298 299_fetch = fetch # renamed because an argument shadows the fetch name in get_tags 300 301 302def try_get_remote_name_by_url(url: str) -> str | None: 303 result = spawn.capture(["git", "remote", "--verbose"]) 304 for line in result.splitlines(): 305 remote, desc = line.split("\t") 306 if desc.lower() in (f"{url} (fetch)".lower(), f"{url}.git (fetch)".lower()): 307 return remote 308 return None 309 310 311def get_remote( 312 url: str = MATERIALIZE_REMOTE_URL, 313 default_remote_name: str = "origin", 314) -> str: 315 # Alternative syntax 316 remote = try_get_remote_name_by_url(url) or try_get_remote_name_by_url( 317 url.replace("https://github.com/", "git@github.com:") 318 ) 319 if not remote: 320 remote = default_remote_name 321 print(f"Remote for URL {url} not found, using {remote}") 322 323 return remote 324 325 326def get_common_ancestor_commit(remote: str, branch: str) -> str: 327 try: 328 head = spawn.capture(["git", "rev-parse", "HEAD"]).strip() 329 headers = {"Accept": "application/vnd.github+json"} 330 if token := os.getenv("GITHUB_TOKEN"): 331 headers["Authorization"] = f"Bearer {token}" 332 333 resp = requests.get( 334 f"https://api.github.com/repos/materializeinc/materialize/compare/{head}...{branch}", 335 headers=headers, 336 ) 337 resp.raise_for_status() 338 data = resp.json() 339 return data["merge_base_commit"]["sha"] 340 except Exception as e: 341 # Try locally if Github is down or the change has not been pushed yet when running locally 342 print(f"Failed to get ancestor commit from Github, running locally: {e}") 343 344 # Make sure we have an up to date view 345 command = ["git", "fetch"] 346 if spawn.capture(["git", "rev-parse", "--is-shallow-repository"]) == "true": 347 command.append("--unshallow") 348 spawn.runv(command + [remote, branch]) 349 350 return spawn.capture( 351 ["git", "merge-base", "HEAD", f"{remote}/{branch}"] 352 ).strip() 353 354 355def is_on_release_version() -> bool: 356 git_tags = get_tags_of_current_commit() 357 return any(MzVersion.is_valid_version_string(git_tag) for git_tag in git_tags) 358 359 360def contains_commit( 361 commit_sha: str, 362 target: str = "HEAD", 363 remote_url: str = MATERIALIZE_REMOTE_URL, 364) -> bool: 365 return is_ancestor(commit_sha, target) 366 367 368def get_tagged_release_version(version_type: type[VERSION_TYPE]) -> VERSION_TYPE | None: 369 """ 370 This returns the release version if exactly this commit is tagged. 371 If multiple release versions are present, the highest one will be returned. 372 None will be returned if the commit is not tagged. 373 """ 374 git_tags = get_tags_of_current_commit() 375 376 versions: list[VERSION_TYPE] = [] 377 378 for git_tag in git_tags: 379 if version_type.is_valid_version_string(git_tag): 380 versions.append(version_type.parse(git_tag)) 381 382 if len(versions) == 0: 383 return None 384 385 if len(versions) > 1: 386 print( 387 "Warning! Commit is tagged with multiple release versions! Returning the highest." 388 ) 389 390 return max(versions) 391 392 393def get_commit_message(commit_sha: str) -> str | None: 394 try: 395 command = ["git", "log", "-1", "--pretty=format:%s", commit_sha] 396 return spawn.capture(command, stderr=subprocess.DEVNULL).strip() 397 except subprocess.CalledProcessError: 398 # Sometimes mz_version() will report a Git SHA that is not available 399 # in the current repository 400 return None 401 402 403def get_branch_name() -> str: 404 """This may not work on Buildkite; consider using the same function from build_context.""" 405 command = ["git", "branch", "--show-current"] 406 return spawn.capture(command).strip() 407 408 409# Work tree mutation 410 411 412def create_branch(name: str) -> None: 413 spawn.runv(["git", "checkout", "-b", name]) 414 415 416def checkout(rev: str, path: str | None = None) -> None: 417 """Git checkout the rev""" 418 cmd = ["git", "checkout", rev] 419 if path: 420 cmd.extend(["--", path]) 421 spawn.runv(cmd) 422 423 424def add_file(file: str) -> None: 425 """Git add a file""" 426 spawn.runv(["git", "add", file]) 427 428 429def commit_all_changed(message: str) -> None: 430 """Commit all changed files with the given message""" 431 spawn.runv(["git", "commit", "-a", "-m", message]) 432 433 434def tag_annotated(tag: str) -> None: 435 """Create an annotated tag on HEAD""" 436 spawn.runv(["git", "tag", "-a", "-m", tag, tag])
33def get_config(key: str) -> str | None: 34 """Read a git config value, returning None if unset.""" 35 try: 36 return spawn.capture(["git", "config", key]).strip() 37 except subprocess.CalledProcessError: 38 return None
Read a git config value, returning None if unset.
41def get_user_name() -> str | None: 42 """Get the configured git user.name.""" 43 return get_config("user.name")
Get the configured git user.name.
46def get_user_email() -> str | None: 47 """Get the configured git user.email.""" 48 return get_config("user.email")
Get the configured git user.email.
51def rev_count(rev: str) -> int: 52 """Count the commits up to a revision. 53 54 Args: 55 rev: A Git revision in any format know to the Git CLI. 56 57 Returns: 58 count: The number of commits in the Git repository starting from the 59 initial commit and ending with the specified commit, inclusive. 60 """ 61 return int(spawn.capture(["git", "rev-list", "--count", rev, "--"]).strip())
Count the commits up to a revision.
Args: rev: A Git revision in any format know to the Git CLI.
Returns: count: The number of commits in the Git repository starting from the initial commit and ending with the specified commit, inclusive.
64def get_first_parent_commits(rev: str, limit: int) -> list[str]: 65 """Get commit hashes along the first-parent chain starting from rev. 66 67 Returns up to `limit` commit hashes (including rev itself), following 68 only first parents (i.e., staying on the main branch). 69 """ 70 return ( 71 spawn.capture(["git", "rev-list", "--first-parent", f"-{limit}", rev]) 72 .strip() 73 .splitlines() 74 )
Get commit hashes along the first-parent chain starting from rev.
Returns up to limit commit hashes (including rev itself), following
only first parents (i.e., staying on the main branch).
77def rev_parse(rev: str, *, abbrev: bool = False) -> str: 78 """Compute the hash for a revision. 79 80 Args: 81 rev: A Git revision in any format known to the Git CLI. 82 abbrev: Return a branch or tag name instead of a git sha 83 84 Returns: 85 ref: A 40 character hex-encoded SHA-1 hash representing the ID of the 86 named revision in Git's object database. 87 88 With "abbrev=True" this will return an abbreviated ref, or throw an 89 error if there is no abbrev. 90 """ 91 a = ["--abbrev-ref"] if abbrev else [] 92 out = spawn.capture(["git", "rev-parse", *a, "--verify", rev]).strip() 93 if not out: 94 raise RuntimeError(f"No parsed rev for {rev}") 95 return out
Compute the hash for a revision.
Args: rev: A Git revision in any format known to the Git CLI. abbrev: Return a branch or tag name instead of a git sha
Returns: ref: A 40 character hex-encoded SHA-1 hash representing the ID of the named revision in Git's object database.
With "abbrev=True" this will return an abbreviated ref, or throw an
error if there is no abbrev.
98@functools.cache 99def expand_globs(root: Path, *specs: Path | str) -> set[str]: 100 """Find unignored files within the specified paths.""" 101 # The goal here is to find all files in the working tree that are not 102 # ignored by .gitignore. Naively using `git ls-files` doesn't work, because 103 # it reports files that have been deleted in the working tree if they are 104 # still present in the index. Using `os.walkdir` doesn't work because there 105 # is no good way to evaluate .gitignore rules from Python. So we use a 106 # combination of `git diff` and `git ls-files`. 107 108 # `git diff` against the empty tree surfaces all tracked files that have 109 # not been deleted. 110 empty_tree = ( 111 "4b825dc642cb6eb9a060e54bf8d69288fbee4904" # git hash-object -t tree /dev/null 112 ) 113 diff_files = spawn.capture( 114 ["git", "diff", "--name-only", "-z", "--relative", empty_tree, "--", *specs], 115 cwd=root, 116 ) 117 118 # `git ls-files --others --exclude-standard` surfaces any non-ignored, 119 # untracked files, which are not included in the `git diff` output above. 120 ls_files = spawn.capture( 121 ["git", "ls-files", "--others", "--exclude-standard", "-z", "--", *specs], 122 cwd=root, 123 ) 124 125 return set(f for f in (diff_files + ls_files).split("\0") if f.strip() != "")
Find unignored files within the specified paths.
161def get_latest_version( 162 version_type: type[VERSION_TYPE], 163 excluded_versions: set[VERSION_TYPE] | None = None, 164 current_version: VERSION_TYPE | None = None, 165) -> VERSION_TYPE: 166 all_version_tags: list[VERSION_TYPE] = get_version_tags( 167 version_type=version_type, fetch=True 168 ) 169 170 if excluded_versions is not None: 171 all_version_tags = [ 172 v 173 for v in all_version_tags 174 if v not in excluded_versions 175 and (not current_version or v < current_version) 176 ] 177 178 return max(all_version_tags)
193def is_ancestor(earlier: str, later: str) -> bool: 194 """True if earlier is in an ancestor of later""" 195 try: 196 headers = {"Accept": "application/vnd.github+json"} 197 if token := os.getenv("GITHUB_TOKEN"): 198 headers["Authorization"] = f"Bearer {token}" 199 200 resp = requests.get( 201 f"https://api.github.com/repos/materializeinc/materialize/compare/{earlier}...{later}", 202 headers=headers, 203 ) 204 resp.raise_for_status() 205 data = resp.json() 206 return data.get("status") in ("ahead", "identical") 207 except Exception as e: 208 # Try locally if Github is down or the change has not been pushed yet when running locally 209 print(f"Failed to get ancestor status from Github, running locally: {e}") 210 211 # Make sure we have an up to date view of main. 212 command = ["git", "fetch"] 213 if spawn.capture(["git", "rev-parse", "--is-shallow-repository"]) == "true": 214 command.append("--unshallow") 215 spawn.runv(command + [get_remote(), earlier, later]) 216 217 return ( 218 spawn.run_and_get_return_code( 219 ["git", "merge-base", "--is-ancestor", earlier, later] 220 ) 221 == 0 222 )
True if earlier is in an ancestor of later
225def is_dirty() -> bool: 226 """Check if the working directory has modifications to tracked files""" 227 proc = subprocess.run("git diff --no-ext-diff --quiet --exit-code".split()) 228 idx = subprocess.run("git diff --cached --no-ext-diff --quiet --exit-code".split()) 229 return proc.returncode != 0 or idx.returncode != 0
Check if the working directory has modifications to tracked files
232def describe() -> str: 233 """Describe the relationship between the current commit and the most recent tag""" 234 return spawn.capture(["git", "describe"]).strip()
Describe the relationship between the current commit and the most recent tag
237def fetch( 238 remote: str | None = None, 239 all_remotes: bool = False, 240 include_tags: YesNoOnce = YesNoOnce.NO, 241 force: bool = False, 242 branch: str | None = None, 243 only_tags: bool = False, 244) -> str: 245 """Fetch from remotes""" 246 247 if remote is not None and all_remotes: 248 raise RuntimeError("all_remotes must be false when a remote is specified") 249 250 if branch is not None and remote is None: 251 raise RuntimeError("remote must be specified when a branch is specified") 252 253 if branch is not None and only_tags: 254 raise RuntimeError("branch must not be specified if only_tags is set") 255 256 command = ["git", "fetch"] 257 if spawn.capture(["git", "rev-parse", "--is-shallow-repository"]) == "true": 258 command.append("--unshallow") 259 260 if remote: 261 command.append(remote) 262 263 if branch: 264 command.append(branch) 265 266 if all_remotes: 267 command.append("--all") 268 269 fetch_tags = ( 270 include_tags == YesNoOnce.YES 271 # fetch tags again if used with force (tags might have changed) 272 or (include_tags == YesNoOnce.ONCE and force) 273 or ( 274 include_tags == YesNoOnce.ONCE 275 and remote not in fetched_tags_in_remotes 276 and "*" not in fetched_tags_in_remotes 277 ) 278 ) 279 280 if fetch_tags: 281 command.append("--tags") 282 283 if force: 284 command.append("--force") 285 286 if not fetch_tags and only_tags: 287 return "" 288 289 output = spawn.capture(command).strip() 290 291 if fetch_tags: 292 fetched_tags_in_remotes.add(remote) 293 294 if all_remotes: 295 fetched_tags_in_remotes.add("*") 296 297 return output
Fetch from remotes
303def try_get_remote_name_by_url(url: str) -> str | None: 304 result = spawn.capture(["git", "remote", "--verbose"]) 305 for line in result.splitlines(): 306 remote, desc = line.split("\t") 307 if desc.lower() in (f"{url} (fetch)".lower(), f"{url}.git (fetch)".lower()): 308 return remote 309 return None
312def get_remote( 313 url: str = MATERIALIZE_REMOTE_URL, 314 default_remote_name: str = "origin", 315) -> str: 316 # Alternative syntax 317 remote = try_get_remote_name_by_url(url) or try_get_remote_name_by_url( 318 url.replace("https://github.com/", "git@github.com:") 319 ) 320 if not remote: 321 remote = default_remote_name 322 print(f"Remote for URL {url} not found, using {remote}") 323 324 return remote
327def get_common_ancestor_commit(remote: str, branch: str) -> str: 328 try: 329 head = spawn.capture(["git", "rev-parse", "HEAD"]).strip() 330 headers = {"Accept": "application/vnd.github+json"} 331 if token := os.getenv("GITHUB_TOKEN"): 332 headers["Authorization"] = f"Bearer {token}" 333 334 resp = requests.get( 335 f"https://api.github.com/repos/materializeinc/materialize/compare/{head}...{branch}", 336 headers=headers, 337 ) 338 resp.raise_for_status() 339 data = resp.json() 340 return data["merge_base_commit"]["sha"] 341 except Exception as e: 342 # Try locally if Github is down or the change has not been pushed yet when running locally 343 print(f"Failed to get ancestor commit from Github, running locally: {e}") 344 345 # Make sure we have an up to date view 346 command = ["git", "fetch"] 347 if spawn.capture(["git", "rev-parse", "--is-shallow-repository"]) == "true": 348 command.append("--unshallow") 349 spawn.runv(command + [remote, branch]) 350 351 return spawn.capture( 352 ["git", "merge-base", "HEAD", f"{remote}/{branch}"] 353 ).strip()
369def get_tagged_release_version(version_type: type[VERSION_TYPE]) -> VERSION_TYPE | None: 370 """ 371 This returns the release version if exactly this commit is tagged. 372 If multiple release versions are present, the highest one will be returned. 373 None will be returned if the commit is not tagged. 374 """ 375 git_tags = get_tags_of_current_commit() 376 377 versions: list[VERSION_TYPE] = [] 378 379 for git_tag in git_tags: 380 if version_type.is_valid_version_string(git_tag): 381 versions.append(version_type.parse(git_tag)) 382 383 if len(versions) == 0: 384 return None 385 386 if len(versions) > 1: 387 print( 388 "Warning! Commit is tagged with multiple release versions! Returning the highest." 389 ) 390 391 return max(versions)
This returns the release version if exactly this commit is tagged. If multiple release versions are present, the highest one will be returned. None will be returned if the commit is not tagged.
394def get_commit_message(commit_sha: str) -> str | None: 395 try: 396 command = ["git", "log", "-1", "--pretty=format:%s", commit_sha] 397 return spawn.capture(command, stderr=subprocess.DEVNULL).strip() 398 except subprocess.CalledProcessError: 399 # Sometimes mz_version() will report a Git SHA that is not available 400 # in the current repository 401 return None
404def get_branch_name() -> str: 405 """This may not work on Buildkite; consider using the same function from build_context.""" 406 command = ["git", "branch", "--show-current"] 407 return spawn.capture(command).strip()
This may not work on Buildkite; consider using the same function from build_context.
417def checkout(rev: str, path: str | None = None) -> None: 418 """Git checkout the rev""" 419 cmd = ["git", "checkout", rev] 420 if path: 421 cmd.extend(["--", path]) 422 spawn.runv(cmd)
Git checkout the rev
Git add a file
430def commit_all_changed(message: str) -> None: 431 """Commit all changed files with the given message""" 432 spawn.runv(["git", "commit", "-a", "-m", message])
Commit all changed files with the given message
435def tag_annotated(tag: str) -> None: 436 """Create an annotated tag on HEAD""" 437 spawn.runv(["git", "tag", "-a", "-m", tag, tag])
Create an annotated tag on HEAD