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