blob: ab10ca13444ce770685b64d5f9cd67ebcfe5f40a [file] [log] [blame] [edit]
#!/usr/bin/env fuchsia-vendored-python
# Copyright 2024 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.
"""Extract the git HEAD commit value from a given git source directory.
This also supports creating a Ninja depfile to track the files that
participate in its computation.
"""
import argparse
import subprocess
import sys
from pathlib import Path
def find_git_head_inputs(repo_dir: Path) -> set[Path]:
"""Find all input files used to determine the git HEAD of a given repository.
Args:
repo_dir: Path to a non-bare git repository directory.
Returns:
A set of Path values that can be used as implicit inputs in a depfile.
"""
result: set[Path] = set()
git_dir = repo_dir / ".git"
if git_dir.is_dir():
# A regular .git sub-directory.
pass
elif git_dir.is_file():
# A sub-module redirection file, contains 'gitdir: <path_to_real_git_dir>'
result.add(git_dir)
content = git_dir.read_text().strip()
assert content.startswith("gitdir: ")
git_dir = repo_dir / content[8:]
# The .git/config file will be accessed if it exists.
git_config = git_dir / "config"
if git_config.exists():
result.add(git_config)
# The .git/packed-refs file will be accessed if it exists,
# even in detached state.
git_packed_refs = git_dir / "packed-refs"
if git_packed_refs.exists():
result.add(git_packed_refs)
git_head = git_dir / "HEAD"
result.add(git_head)
if git_head.exists():
# HEAD can be an hexadecimal commit value (detached state)
# or "ref: refs/heads/<branch>" (when on a branch).
content = git_head.read_text().strip()
if content.startswith("ref: "):
branch_ref = git_dir / content[5:]
# refs/heads/<branch> might not exist when packed references
# are enabled, in this case, the value is in .git/packed-refs
# instead.
if branch_ref.exists():
result.add(branch_ref)
return result
def get_git_head_commit(repo_dir: Path, git_binary: Path = Path("git")) -> str:
"""Return the git HEAD commit value of a given repository.
Args:
repo_dir: Path to a git repository directory.
git_binary: Optional path to the git binary to use, default to "git".
Returns:
An hexadecimal string value for the HEAD commit (even when on a branch).
This will be "GIT_ERROR" if an error happened when trying to get it.
"""
ret = subprocess.run(
[git_binary, "-C", repo_dir, "rev-parse", "HEAD"],
text=True,
capture_output=True,
)
if ret.returncode != 0:
return "GIT_ERROR"
return ret.stdout.strip()
def main() -> int:
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawTextHelpFormatter
)
parser.add_argument(
"--git",
type=Path,
help="Specify path to git binary (auto-detected from PATH)",
)
parser.add_argument(
"--output", type=Path, help="Optional output file path."
)
parser.add_argument(
"--depfile", type=Path, help="Optional Ninja depfile output file path."
)
parser.add_argument(
"repo_dir", type=Path, help="Path to git repository directory."
)
args = parser.parse_args()
if args.depfile and not args.output:
parser.error("--depfile option requires --output.")
result = get_git_head_commit(args.repo_dir, args.git)
if args.output:
args.output.write_text(result)
else:
print(result)
if args.depfile:
depfile_inputs = find_git_head_inputs(args.repo_dir)
args.depfile.write_text(
"%s: %s\n"
% (args.output, " ".join(str(f) for f in sorted(depfile_inputs)))
)
return 0
if __name__ == "__main__":
sys.exit(main())