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 # GitHub resolves the ref "HEAD" server-side to the repo's default 200 # branch, not the local checkout's HEAD. Resolve it to a concrete SHA 201 # first, otherwise e.g. compare/HEAD...main becomes main...main. 202 api_earlier = rev_parse(earlier) if earlier == "HEAD" else earlier 203 api_later = rev_parse(later) if later == "HEAD" else later 204 205 resp = requests.get( 206 f"https://api.github.com/repos/materializeinc/materialize/compare/{api_earlier}...{api_later}", 207 headers=headers, 208 ) 209 resp.raise_for_status() 210 data = resp.json() 211 return data.get("status") in ("ahead", "identical") 212 except Exception as e: 213 # Try locally if Github is down or the change has not been pushed yet when running locally 214 print(f"Failed to get ancestor status from Github, running locally: {e}") 215 216 # Make sure we have an up to date view of main. 217 command = ["git", "fetch"] 218 if ( 219 spawn.capture(["git", "rev-parse", "--is-shallow-repository"]).strip() 220 == "true" 221 ): 222 command.append("--unshallow") 223 spawn.runv(command + [get_remote(), earlier, later]) 224 225 return ( 226 spawn.run_and_get_return_code( 227 ["git", "merge-base", "--is-ancestor", earlier, later] 228 ) 229 == 0 230 ) 231 232 233def is_dirty() -> bool: 234 """Check if the working directory has modifications to tracked files""" 235 proc = subprocess.run("git diff --no-ext-diff --quiet --exit-code".split()) 236 idx = subprocess.run("git diff --cached --no-ext-diff --quiet --exit-code".split()) 237 return proc.returncode != 0 or idx.returncode != 0 238 239 240def describe() -> str: 241 """Describe the relationship between the current commit and the most recent tag""" 242 return spawn.capture(["git", "describe"]).strip() 243 244 245def fetch( 246 remote: str | None = None, 247 all_remotes: bool = False, 248 include_tags: YesNoOnce = YesNoOnce.NO, 249 force: bool = False, 250 branch: str | None = None, 251 only_tags: bool = False, 252) -> str: 253 """Fetch from remotes""" 254 255 if remote is not None and all_remotes: 256 raise RuntimeError("all_remotes must be false when a remote is specified") 257 258 if branch is not None and remote is None: 259 raise RuntimeError("remote must be specified when a branch is specified") 260 261 if branch is not None and only_tags: 262 raise RuntimeError("branch must not be specified if only_tags is set") 263 264 command = ["git", "fetch"] 265 if spawn.capture(["git", "rev-parse", "--is-shallow-repository"]).strip() == "true": 266 command.append("--unshallow") 267 268 if remote: 269 command.append(remote) 270 271 if branch: 272 command.append(branch) 273 274 if all_remotes: 275 command.append("--all") 276 277 fetch_tags = ( 278 include_tags == YesNoOnce.YES 279 # fetch tags again if used with force (tags might have changed) 280 or (include_tags == YesNoOnce.ONCE and force) 281 or ( 282 include_tags == YesNoOnce.ONCE 283 and remote not in fetched_tags_in_remotes 284 and "*" not in fetched_tags_in_remotes 285 ) 286 ) 287 288 if fetch_tags: 289 command.append("--tags") 290 291 if force: 292 command.append("--force") 293 294 if not fetch_tags and only_tags: 295 return "" 296 297 output = spawn.capture(command).strip() 298 299 if fetch_tags: 300 fetched_tags_in_remotes.add(remote) 301 302 if all_remotes: 303 fetched_tags_in_remotes.add("*") 304 305 return output 306 307 308_fetch = fetch # renamed because an argument shadows the fetch name in get_tags 309 310 311def try_get_remote_name_by_url(url: str) -> str | None: 312 result = spawn.capture(["git", "remote", "--verbose"]) 313 for line in result.splitlines(): 314 remote, desc = line.split("\t") 315 if desc.lower() in (f"{url} (fetch)".lower(), f"{url}.git (fetch)".lower()): 316 return remote 317 return None 318 319 320def get_remote( 321 url: str = MATERIALIZE_REMOTE_URL, 322 default_remote_name: str = "origin", 323) -> str: 324 # Alternative syntax 325 remote = try_get_remote_name_by_url(url) or try_get_remote_name_by_url( 326 url.replace("https://github.com/", "git@github.com:") 327 ) 328 if not remote: 329 remote = default_remote_name 330 print(f"Remote for URL {url} not found, using {remote}") 331 332 return remote 333 334 335def get_common_ancestor_commit(remote: str, branch: str) -> str: 336 try: 337 head = spawn.capture(["git", "rev-parse", "HEAD"]).strip() 338 headers = {"Accept": "application/vnd.github+json"} 339 if token := os.getenv("GITHUB_TOKEN"): 340 headers["Authorization"] = f"Bearer {token}" 341 342 resp = requests.get( 343 f"https://api.github.com/repos/materializeinc/materialize/compare/{head}...{branch}", 344 headers=headers, 345 ) 346 resp.raise_for_status() 347 data = resp.json() 348 return data["merge_base_commit"]["sha"] 349 except Exception as e: 350 # Try locally if Github is down or the change has not been pushed yet when running locally 351 print(f"Failed to get ancestor commit from Github, running locally: {e}") 352 353 # Make sure we have an up to date view 354 command = ["git", "fetch"] 355 if ( 356 spawn.capture(["git", "rev-parse", "--is-shallow-repository"]).strip() 357 == "true" 358 ): 359 command.append("--unshallow") 360 spawn.runv(command + [remote, branch]) 361 362 return spawn.capture( 363 ["git", "merge-base", "HEAD", f"{remote}/{branch}"] 364 ).strip() 365 366 367def is_on_release_version() -> bool: 368 git_tags = get_tags_of_current_commit() 369 return any(MzVersion.is_valid_version_string(git_tag) for git_tag in git_tags) 370 371 372def contains_commit( 373 commit_sha: str, 374 target: str = "HEAD", 375 remote_url: str = MATERIALIZE_REMOTE_URL, 376) -> bool: 377 return is_ancestor(commit_sha, target) 378 379 380def get_tagged_release_version(version_type: type[VERSION_TYPE]) -> VERSION_TYPE | None: 381 """ 382 This returns the release version if exactly this commit is tagged. 383 If multiple release versions are present, the highest one will be returned. 384 None will be returned if the commit is not tagged. 385 """ 386 git_tags = get_tags_of_current_commit() 387 388 versions: list[VERSION_TYPE] = [] 389 390 for git_tag in git_tags: 391 if version_type.is_valid_version_string(git_tag): 392 versions.append(version_type.parse(git_tag)) 393 394 if len(versions) == 0: 395 return None 396 397 if len(versions) > 1: 398 print( 399 "Warning! Commit is tagged with multiple release versions! Returning the highest." 400 ) 401 402 return max(versions) 403 404 405def get_commit_message(commit_sha: str) -> str | None: 406 try: 407 command = ["git", "log", "-1", "--pretty=format:%s", commit_sha] 408 return spawn.capture(command, stderr=subprocess.DEVNULL).strip() 409 except subprocess.CalledProcessError: 410 # Sometimes mz_version() will report a Git SHA that is not available 411 # in the current repository 412 return None 413 414 415def get_branch_name() -> str: 416 """This may not work on Buildkite; consider using the same function from build_context.""" 417 command = ["git", "branch", "--show-current"] 418 return spawn.capture(command).strip() 419 420 421# Work tree mutation 422 423 424def create_branch(name: str) -> None: 425 spawn.runv(["git", "checkout", "-b", name]) 426 427 428def checkout(rev: str, path: str | None = None) -> None: 429 """Git checkout the rev""" 430 cmd = ["git", "checkout", rev] 431 if path: 432 cmd.extend(["--", path]) 433 spawn.runv(cmd) 434 435 436def add_file(file: str) -> None: 437 """Git add a file""" 438 spawn.runv(["git", "add", file]) 439 440 441def commit_all_changed(message: str) -> None: 442 """Commit all changed files with the given message""" 443 spawn.runv(["git", "commit", "-a", "-m", message]) 444 445 446def tag_annotated(tag: str) -> None: 447 """Create an annotated tag on HEAD""" 448 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 # GitHub resolves the ref "HEAD" server-side to the repo's default 201 # branch, not the local checkout's HEAD. Resolve it to a concrete SHA 202 # first, otherwise e.g. compare/HEAD...main becomes main...main. 203 api_earlier = rev_parse(earlier) if earlier == "HEAD" else earlier 204 api_later = rev_parse(later) if later == "HEAD" else later 205 206 resp = requests.get( 207 f"https://api.github.com/repos/materializeinc/materialize/compare/{api_earlier}...{api_later}", 208 headers=headers, 209 ) 210 resp.raise_for_status() 211 data = resp.json() 212 return data.get("status") in ("ahead", "identical") 213 except Exception as e: 214 # Try locally if Github is down or the change has not been pushed yet when running locally 215 print(f"Failed to get ancestor status from Github, running locally: {e}") 216 217 # Make sure we have an up to date view of main. 218 command = ["git", "fetch"] 219 if ( 220 spawn.capture(["git", "rev-parse", "--is-shallow-repository"]).strip() 221 == "true" 222 ): 223 command.append("--unshallow") 224 spawn.runv(command + [get_remote(), earlier, later]) 225 226 return ( 227 spawn.run_and_get_return_code( 228 ["git", "merge-base", "--is-ancestor", earlier, later] 229 ) 230 == 0 231 )
True if earlier is in an ancestor of later
234def is_dirty() -> bool: 235 """Check if the working directory has modifications to tracked files""" 236 proc = subprocess.run("git diff --no-ext-diff --quiet --exit-code".split()) 237 idx = subprocess.run("git diff --cached --no-ext-diff --quiet --exit-code".split()) 238 return proc.returncode != 0 or idx.returncode != 0
Check if the working directory has modifications to tracked files
241def describe() -> str: 242 """Describe the relationship between the current commit and the most recent tag""" 243 return spawn.capture(["git", "describe"]).strip()
Describe the relationship between the current commit and the most recent tag
246def fetch( 247 remote: str | None = None, 248 all_remotes: bool = False, 249 include_tags: YesNoOnce = YesNoOnce.NO, 250 force: bool = False, 251 branch: str | None = None, 252 only_tags: bool = False, 253) -> str: 254 """Fetch from remotes""" 255 256 if remote is not None and all_remotes: 257 raise RuntimeError("all_remotes must be false when a remote is specified") 258 259 if branch is not None and remote is None: 260 raise RuntimeError("remote must be specified when a branch is specified") 261 262 if branch is not None and only_tags: 263 raise RuntimeError("branch must not be specified if only_tags is set") 264 265 command = ["git", "fetch"] 266 if spawn.capture(["git", "rev-parse", "--is-shallow-repository"]).strip() == "true": 267 command.append("--unshallow") 268 269 if remote: 270 command.append(remote) 271 272 if branch: 273 command.append(branch) 274 275 if all_remotes: 276 command.append("--all") 277 278 fetch_tags = ( 279 include_tags == YesNoOnce.YES 280 # fetch tags again if used with force (tags might have changed) 281 or (include_tags == YesNoOnce.ONCE and force) 282 or ( 283 include_tags == YesNoOnce.ONCE 284 and remote not in fetched_tags_in_remotes 285 and "*" not in fetched_tags_in_remotes 286 ) 287 ) 288 289 if fetch_tags: 290 command.append("--tags") 291 292 if force: 293 command.append("--force") 294 295 if not fetch_tags and only_tags: 296 return "" 297 298 output = spawn.capture(command).strip() 299 300 if fetch_tags: 301 fetched_tags_in_remotes.add(remote) 302 303 if all_remotes: 304 fetched_tags_in_remotes.add("*") 305 306 return output
Fetch from remotes
312def try_get_remote_name_by_url(url: str) -> str | None: 313 result = spawn.capture(["git", "remote", "--verbose"]) 314 for line in result.splitlines(): 315 remote, desc = line.split("\t") 316 if desc.lower() in (f"{url} (fetch)".lower(), f"{url}.git (fetch)".lower()): 317 return remote 318 return None
321def get_remote( 322 url: str = MATERIALIZE_REMOTE_URL, 323 default_remote_name: str = "origin", 324) -> str: 325 # Alternative syntax 326 remote = try_get_remote_name_by_url(url) or try_get_remote_name_by_url( 327 url.replace("https://github.com/", "git@github.com:") 328 ) 329 if not remote: 330 remote = default_remote_name 331 print(f"Remote for URL {url} not found, using {remote}") 332 333 return remote
336def get_common_ancestor_commit(remote: str, branch: str) -> str: 337 try: 338 head = spawn.capture(["git", "rev-parse", "HEAD"]).strip() 339 headers = {"Accept": "application/vnd.github+json"} 340 if token := os.getenv("GITHUB_TOKEN"): 341 headers["Authorization"] = f"Bearer {token}" 342 343 resp = requests.get( 344 f"https://api.github.com/repos/materializeinc/materialize/compare/{head}...{branch}", 345 headers=headers, 346 ) 347 resp.raise_for_status() 348 data = resp.json() 349 return data["merge_base_commit"]["sha"] 350 except Exception as e: 351 # Try locally if Github is down or the change has not been pushed yet when running locally 352 print(f"Failed to get ancestor commit from Github, running locally: {e}") 353 354 # Make sure we have an up to date view 355 command = ["git", "fetch"] 356 if ( 357 spawn.capture(["git", "rev-parse", "--is-shallow-repository"]).strip() 358 == "true" 359 ): 360 command.append("--unshallow") 361 spawn.runv(command + [remote, branch]) 362 363 return spawn.capture( 364 ["git", "merge-base", "HEAD", f"{remote}/{branch}"] 365 ).strip()
381def get_tagged_release_version(version_type: type[VERSION_TYPE]) -> VERSION_TYPE | None: 382 """ 383 This returns the release version if exactly this commit is tagged. 384 If multiple release versions are present, the highest one will be returned. 385 None will be returned if the commit is not tagged. 386 """ 387 git_tags = get_tags_of_current_commit() 388 389 versions: list[VERSION_TYPE] = [] 390 391 for git_tag in git_tags: 392 if version_type.is_valid_version_string(git_tag): 393 versions.append(version_type.parse(git_tag)) 394 395 if len(versions) == 0: 396 return None 397 398 if len(versions) > 1: 399 print( 400 "Warning! Commit is tagged with multiple release versions! Returning the highest." 401 ) 402 403 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.
406def get_commit_message(commit_sha: str) -> str | None: 407 try: 408 command = ["git", "log", "-1", "--pretty=format:%s", commit_sha] 409 return spawn.capture(command, stderr=subprocess.DEVNULL).strip() 410 except subprocess.CalledProcessError: 411 # Sometimes mz_version() will report a Git SHA that is not available 412 # in the current repository 413 return None
416def get_branch_name() -> str: 417 """This may not work on Buildkite; consider using the same function from build_context.""" 418 command = ["git", "branch", "--show-current"] 419 return spawn.capture(command).strip()
This may not work on Buildkite; consider using the same function from build_context.
429def checkout(rev: str, path: str | None = None) -> None: 430 """Git checkout the rev""" 431 cmd = ["git", "checkout", rev] 432 if path: 433 cmd.extend(["--", path]) 434 spawn.runv(cmd)
Git checkout the rev
Git add a file
442def commit_all_changed(message: str) -> None: 443 """Commit all changed files with the given message""" 444 spawn.runv(["git", "commit", "-a", "-m", message])
Commit all changed files with the given message
447def tag_annotated(tag: str) -> None: 448 """Create an annotated tag on HEAD""" 449 spawn.runv(["git", "tag", "-a", "-m", tag, tag])
Create an annotated tag on HEAD