blob: 4b12a8c64e0e40e4207617e733090f2afaaa3e44 [file]
# Copyright 2026 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import subprocess
from pathlib import Path
def get_git_path(repo_dir: Path, name: str) -> Path:
"""Return the absolute, resolved path to a git metadata file.
Args:
repo_dir: Path to the repository root directory.
name: Name of the git path to resolve (e.g. 'HEAD', 'index').
Returns:
The absolute path as a Path object.
"""
ret = subprocess.run(
[
"git",
"--no-optional-locks",
"-C",
str(repo_dir),
"rev-parse",
"--path-format=absolute",
"--git-path",
name,
],
capture_output=True,
text=True,
check=True,
)
return Path(ret.stdout.strip()).resolve()
def get_git_ref(repo_dir: Path) -> Path:
"""Return the absolute path to the current branch ref file or packed-refs.
If the repository is in detached HEAD state, returns the path to the HEAD file.
If the current branch ref is loose, returns the path to the loose ref file.
If the current branch ref is packed, returns the path to the packed-refs file.
Args:
repo_dir: Path to the repository root directory.
Returns:
The absolute path as a Path object.
"""
# Find the current ref if on a branch
cmd = [
"git",
"--no-optional-locks",
"-C",
str(repo_dir),
"symbolic-ref",
"HEAD",
]
ret = subprocess.run(cmd, capture_output=True, text=True)
if ret.returncode != 0:
# Detached HEAD. We track HEAD.
return get_git_path(repo_dir, "HEAD")
ref_name = ret.stdout.strip()
ref_path = get_git_path(repo_dir, ref_name)
if ref_path.exists():
return ref_path
# If the ref is packed, it won't exist as a loose file.
# We track packed-refs instead.
packed_refs = get_git_path(repo_dir, "packed-refs")
if packed_refs.exists():
return packed_refs
# Fallback to HEAD if ref/packed-refs don't exist for some reason
return get_git_path(repo_dir, "HEAD")
def find_git_head_inputs(
repo_dir: Path, track_index: bool = False
) -> set[Path]:
"""Find all input files that affect the git HEAD of a given repository.
Args:
repo_dir: Path to a non-bare git repository directory.
track_index: If True, also track the index file (useful for dirty checks).
Returns:
A set of Path values that can be used as implicit inputs in a depfile.
"""
result: set[Path] = set()
git_file = repo_dir / ".git"
if git_file.is_file():
result.add(git_file)
git_head = get_git_path(repo_dir, "HEAD")
result.add(git_head)
git_config = get_git_path(repo_dir, "config")
if git_config.exists():
result.add(git_config)
git_packed_refs = get_git_path(repo_dir, "packed-refs")
if git_packed_refs.exists():
result.add(git_packed_refs)
# Track the ref file (or packed-refs/HEAD fallback)
ref_path = get_git_ref(repo_dir)
result.add(ref_path)
if track_index:
git_index = get_git_path(repo_dir, "index")
if git_index.exists():
result.add(git_index)
return result
def _get_best_ref(project_path: Path) -> str:
"""Determine the best git ref to use for resolving the revision.
Checks candidates like 'HEAD', 'jiri/head', 'origin/main' in order
and returns the first one that exists.
Args:
project_path: Path to the git repository.
Returns:
The ref name to use (e.g., 'HEAD').
"""
candidates = ["HEAD", "jiri/head", "origin/main"]
for c in candidates:
ret = subprocess.run(
[
"git",
"--no-optional-locks",
"-C",
str(project_path),
"rev-parse",
"--verify",
c,
],
capture_output=True,
text=True,
)
if ret.returncode == 0:
return c
return "HEAD"
def get_git_revision(project_path: Path) -> str:
"""Return the git revision hash for the project.
Automatically resolves the best ref to use (e.g. HEAD).
Args:
project_path: Path to the git repository.
Returns:
The 40-character hexadecimal revision hash.
"""
ref = _get_best_ref(project_path)
ret = subprocess.run(
[
"git",
"--no-optional-locks",
"-C",
str(project_path),
"rev-parse",
ref,
],
text=True,
capture_output=True,
check=True,
)
return ret.stdout.strip()