sync/gitutils.py (114 lines of code) (raw):

import subprocess import time import git from . import log from .env import Environment from .errors import RetryableError from .lock import RepoLock from .repos import cinnabar from git.objects.commit import Commit from git.repo.base import Repo from typing import Any, Dict, Callable, Optional env = Environment() logger = log.get_logger(__name__) def have_gecko_hg_commit(git_gecko: Repo, hg_rev: str) -> bool: try: cinnabar(git_gecko).hg2git(hg_rev) except ValueError: return False return True def update_repositories(git_gecko: Optional[Repo], git_wpt: Optional[Repo], wait_gecko_commit: Optional[str] = None) -> None: if git_gecko is not None: if wait_gecko_commit is not None: def wait_fn(): assert wait_gecko_commit is not None return have_gecko_hg_commit(git_gecko, wait_gecko_commit) def _update(): assert git_gecko is not None return _update_gecko(git_gecko) success = until(_update, wait_fn) if not success: raise RetryableError( ValueError("Failed to fetch gecko commit %s" % wait_gecko_commit)) else: _update_gecko(git_gecko) if git_wpt is not None: _update_wpt(git_wpt) def until(func: Callable, cond: Callable, max_tries: int = 5) -> bool: for i in range(max_tries): func() if cond(): break time.sleep(1 * (i + 1)) else: return False return True def _fetch(git_gecko, remote): cmd = ["git", "--git-dir", git_gecko.git_dir, "fetch", remote] logger.info(" ".join(cmd)) subprocess.check_call(cmd) def _update_gecko(git_gecko: Repo) -> None: with RepoLock(git_gecko): logger.info("Fetching mozilla-unified") # Not using the built in fetch() function since that tries to parse the output # and sometimes fails _fetch(git_gecko, "mozilla") if "autoland" in [item.name for item in git_gecko.remotes]: logger.info("Fetching autoland") _fetch(git_gecko, "autoland") def _update_wpt(git_wpt: Repo) -> None: with RepoLock(git_wpt): logger.info("Fetching web-platform-tests") git_wpt.git.fetch("origin") def refs(git: Repo, prefix: Optional[str] = None) -> Dict[str, str]: rv = {} refs = git.git.show_ref().split("\n") for item in refs: sha1, ref = item.split(" ", 1) if prefix and not ref.startswith(prefix): continue rv[sha1] = ref return rv def pr_for_commit(git_wpt: Repo, rev: str) -> Optional[int]: prefix = "refs/remotes/origin/pr/" pr_refs = refs(git_wpt, prefix) if rev in pr_refs: return int(pr_refs[rev][len(prefix):]) return None def gecko_repo(git_gecko: Repo, head: Commit) -> Optional[str]: repos = ([("central", env.config["gecko"]["refs"]["central"])] + [(name, ref) for name, ref in env.config["gecko"]["refs"].items() if name != "central"]) for name, ref in repos: if git_gecko.is_ancestor(head, git_gecko.rev_parse(ref)): return name return None def status(repo: Repo) -> Dict[str, Dict[str, Any]]: status_entries = repo.git.status(z=True).split("\0") rv = {} for item in status_entries: if not item.strip(): continue code = item[:2] filenames = item[3:].rsplit(" -> ", 1) if len(filenames) == 2: filename, rename = filenames else: filename, rename = filenames[0], None rv[filename] = {"code": code, "rename": rename} return rv def handle_empty_commit(worktree, e): # If git exits with return code 1 and mentions an empty # cherry pick, then we tried to cherry pick something # that results in an empty commit so reset the index and # continue. gitpython doesn't really enforce anything about # the type of status, so just convert it to a string to be # sure if (str(e.status) == "1" and "The previous cherry-pick is now empty" in e.stderr or "nothing to commit" in e.stdout): logger.info("Cherry pick resulted in an empty commit") # If the cherry pick would result in an empty commit, # just reset and continue worktree.git.reset() return True return False def cherry_pick(worktree: Repo, commit: str) -> bool: try: worktree.git.cherry_pick(commit) return True except git.GitCommandError as e: if handle_empty_commit(worktree, e): return True return False