Source code
Revision control
Copy as Markdown
Other Tools
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this,
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import Literal, Optional, Union, overload
from packaging.version import Version
from mozversioncontrol.errors import (
InvalidRepoPath,
MissingConfigureInfo,
MissingVCSInfo,
MissingVCSTool,
)
from mozversioncontrol.repo.git import GitRepository
from mozversioncontrol.repo.jj import (
MINIMUM_SUPPORTED_JJ_VERSION,
USING_JJ_DETECTED,
USING_JJ_WARNING,
JjVersionError,
JujutsuRepository,
)
from mozversioncontrol.repo.mercurial import HgRepository
from mozversioncontrol.repo.source import SrcRepository
VCS_CLASSES: dict[str, type] = {
"hg": HgRepository,
"git": GitRepository,
"jj": JujutsuRepository,
"src": SrcRepository,
}
@overload
def get_specific_repository_object(
data: str, output_format: Literal["git"]
) -> GitRepository: ...
@overload
def get_specific_repository_object(
data: str, output_format: Literal["hg"]
) -> HgRepository: ...
@overload
def get_specific_repository_object(
data: str, output_format: Literal["jj"]
) -> JujutsuRepository: ...
@overload
def get_specific_repository_object(
data: str, output_format: Literal["src"]
) -> SrcRepository: ...
def get_specific_repository_object(path: Optional[Union[str, Path]], vcs: str):
"""Return a repository object for the given VCS and path."""
resolved_path = Path(path).resolve()
try:
vcs_cls = VCS_CLASSES[vcs]
except KeyError:
raise ValueError(
f"Unsupported VCS: '{vcs}'; expected one of {tuple(VCS_CLASSES)}"
)
return vcs_cls(resolved_path)
def get_repository_object(path: Optional[Union[str, Path]]):
"""Get a repository object for the repository at `path`.
If `path` is not a known VCS repository, raise an exception.
"""
# If we provide a path to hg that does not match the on-disk casing (e.g.,
# because `path` was normcased), then the hg fsmonitor extension will call
# watchman with that path and watchman will spew errors.
path = Path(path).resolve()
if (path / ".hg").is_dir():
return HgRepository(path)
if (path / ".jj").is_dir():
avoid = os.getenv("MOZ_AVOID_JJ_VCS")
try_using_jj = avoid in (None, "0", "")
if try_using_jj:
try:
result = subprocess.run(
["jj", "--version"],
capture_output=True,
text=True,
check=False,
)
raw_jj_version = result.stdout.strip()
match = re.search(r"\b(\d+\.\d+\.\d+)\b", raw_jj_version)
if not match:
raise ValueError(
f"Could not parse jj version from output: {raw_jj_version}"
)
current_jj_version = Version(match.group(1))
if current_jj_version < MINIMUM_SUPPORTED_JJ_VERSION:
raise JjVersionError(
f"Detected jj version {current_jj_version}, "
f"but version {MINIMUM_SUPPORTED_JJ_VERSION} or newer is required.\n"
f'Full "jj --version" output was: "{raw_jj_version}"'
)
avoid_is_unset = avoid not in ("0", "")
if avoid_is_unset and not hasattr(get_repository_object, "_warned"):
# Warn (once) if MOZ_AVOID_JJ_VCS is unset. If it is set to 0, then use
# jj without warning. If it is set to anything else, do not use jj (so
# eg fall back to git if .git exists.)
get_repository_object._warned = True
print(USING_JJ_DETECTED, file=sys.stderr)
print(USING_JJ_WARNING, file=sys.stderr)
return JujutsuRepository(path)
except OSError:
print(".jj/ directory exists but jj binary not usable", file=sys.stderr)
if (path / ".git").exists():
return GitRepository(path)
if (path / "config" / "milestone.txt").exists():
return SrcRepository(path)
raise InvalidRepoPath(f"Unknown VCS, or not a source checkout: {path}")
def get_repository_from_build_config(config):
"""Obtain a repository from the build configuration.
Accepts an object that has a ``topsrcdir`` and ``subst`` attribute.
"""
flavor = config.substs.get("VCS_CHECKOUT_TYPE")
# If in build mode, only use what configure found. That way we ensure
# that everything in the build system can be controlled via configure.
if not flavor:
raise MissingConfigureInfo(
"could not find VCS_CHECKOUT_TYPE "
"in build config; check configure "
"output and verify it could find a "
"VCS binary"
)
if flavor == "hg":
return HgRepository(Path(config.topsrcdir), hg=config.substs["HG"])
elif flavor == "jj":
return JujutsuRepository(
Path(config.topsrcdir), jj=config.substs["JJ"], git=config.substs["GIT"]
)
elif flavor == "git":
return GitRepository(Path(config.topsrcdir), git=config.substs["GIT"])
elif flavor == "src":
return SrcRepository(Path(config.topsrcdir), src=config.substs["SRC"])
else:
raise MissingVCSInfo(f"unknown VCS_CHECKOUT_TYPE value: {flavor}")
def get_repository_from_env():
"""Obtain a repository object by looking at the environment.
If inside a build environment (denoted by presence of a ``buildconfig``
module), VCS info is obtained from it, as found via configure. This allows
us to respect what was passed into configure. Otherwise, we fall back to
scanning the filesystem.
"""
try:
import buildconfig
return get_repository_from_build_config(buildconfig)
except (ImportError, MissingVCSTool):
pass
paths_to_check = [Path.cwd(), *Path.cwd().parents]
for path in paths_to_check:
try:
return get_repository_object(path)
except InvalidRepoPath:
continue
raise MissingVCSInfo(
f"Could not find Mercurial / Git / JJ checkout for {Path.cwd()}"
)