blob: d96e4d00aded918941901befb632f3c5a9fc37c9 [file] [log] [blame]
#!/usr/bin/python2
"""Syncs to a given Cobalt build id.
Syncs current gclient instance to a given build id, as
generated by "build_id.py" and stored on carbon-airlock-95823.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import argparse
import json
import os
import shutil
import subprocess
import sys
import requests
_BUILD_ID_QUERY_URL = (
"https://carbon-airlock-95823.appspot.com/build_version/search")
_BUILD_ID_QUERY_PARAMETER_NAME = "build_number"
class SubprocessFailedException(Exception):
"""Exception for non-zero subprocess exits."""
def __init__(self, command):
super(SubprocessFailedException, self).__init__() # pylint: disable=super-with-arguments
self.command = command
def __str__(self):
return "Subprocess failed '{0}'".format(self.command)
def _RunGitCommand(gitargs, **kwargs):
"""Runs a git command with "gitargs", returning the output splitlines().
Args:
gitargs: Commandline args that follow 'git'.
**kwargs: Keyword args for Popen.
Returns:
All of stdout, as an array of lines.
Raises:
SubprocessFailedException: if the exit code is nonzero.
"""
result_tuple = _RunGitCommandReturnExitCode(gitargs, **kwargs)
if result_tuple[0] != 0:
raise SubprocessFailedException(" ".join(["git"] + gitargs))
return result_tuple[1]
def _RunGitCommandReturnExitCode(gitargs, **kwargs):
"""Runs a git command with "gitargs", returning the exit code and output.
Args:
gitargs: Commandline args that follow 'git'.
**kwargs: Keyword args for Popen.
Returns:
Tuple of (exit code, all of stdout as an array of lines).
"""
popen_args = ["git"] + gitargs
with subprocess.Popen(popen_args, stdout=subprocess.PIPE, **kwargs) as p:
output = p.stdout.read().splitlines()
return p.wait(), output
def main():
dev_null = open(os.devnull, "w") # pylint: disable=consider-using-with
arg_parser = argparse.ArgumentParser(
description="Syncs to a given Cobalt build id")
arg_parser.add_argument("buildid", nargs=1)
arg_parser.add_argument(
"--force",
default=False,
action="store_true",
help="Deletes directories that don't match the requested format.")
args = arg_parser.parse_args()
r = requests.get(
_BUILD_ID_QUERY_URL,
params={_BUILD_ID_QUERY_PARAMETER_NAME: args.buildid[0]})
if not r.ok:
print(
"HTTP request failed\n{0} {1}\n{2}".format(r.status_code, r.reason,
r.text),
file=sys.stderr)
return 1
# The response starts with a security-related close expression line
outer_json = json.loads(r.text.splitlines()[1])
hashes = json.loads(outer_json["deps"])
git_root = os.getcwd()
for relpath, rep_hash in hashes.items():
path = os.path.normpath(os.path.join(git_root, relpath))
if not os.path.exists(path):
# No warning in this case, we will attempt to clone the repository in
# the next pass through the repos.
continue
is_dirty = (
bool(
_RunGitCommandReturnExitCode(["diff", "--no-ext-diff", "--quiet"],
cwd=path,
stderr=dev_null)[0]) or
bool(
_RunGitCommandReturnExitCode(
["diff", "--no-ext-diff", "--quiet", "--cached"],
cwd=path,
stderr=dev_null)[0]))
if is_dirty:
print("{0} is dirty, please resolve".format(relpath))
return 1
(requested_repo, _) = rep_hash.split("@")
remote_url = _RunGitCommand(["config", "--get", "remote.origin.url"],
cwd=path)[0].strip().decode("utf-8")
if requested_repo.endswith(".git"):
if remote_url + ".git" == requested_repo:
print(("WARNING: You are syncing to {0} instead of {1}. While these "
"point to the same repo, the differing extension will cause "
"different build ids to be generated. If you need the same "
"id, you'll need to specifically clone {0} (note the .git "
"extension).").format(requested_repo, remote_url))
remote_url += ".git"
if remote_url != requested_repo:
if args.force and path != git_root:
shutil.rmtree(path)
else:
print(("{0} exists but does not point to the requested repo for that "
"path, {1}. Either replace that directory manually or run this "
"script with --force. --force will not try to remove the top "
"level repository.").format(path, requested_repo))
return 1
for relpath, rep_hash in hashes.items():
path = os.path.normpath(os.path.join(git_root, relpath))
# repo_hash has a repo path prefix like this:
# 'https://chromium.googlesource.com/chromium/llvm-project/libcxx.git
# @48198f9110397fff47fe7c37cbfa296be7d44d3d'
(requested_repo, requested_hash) = rep_hash.split("@")
if not os.path.exists(path):
print("Missing path {0}, cloning from {1}.".format(path, requested_repo))
try:
# The clone command will create all missing directories leading to the
# path. If the clone is successful, we continue on as usual and let
# the subsequent logic here checkout the appropriate git hash.
_RunGitCommand(["clone", "-q", requested_repo, path])
except SubprocessFailedException:
print("There was an error cloning the repository.")
continue
current_hash = _RunGitCommand(["rev-parse", "HEAD"], cwd=path)[0]
if requested_hash == current_hash:
continue
symbolic_ref = None
try:
symbolic_ref = _RunGitCommand(["symbolic-ref", "--short", "-q", "HEAD"],
cwd=path,
stderr=dev_null)[0]
except SubprocessFailedException:
pass
user_visible_commit = symbolic_ref if symbolic_ref else current_hash[0:7]
print("{0} was at {1} now {2}".format(path, user_visible_commit,
requested_hash[0:7]))
_RunGitCommand(["checkout", "-q", "--detach", requested_hash], cwd=path)
return 0
if __name__ == "__main__":
try:
sys.exit(main())
except SubprocessFailedException as ex:
print(str(ex), file=sys.stderr)
sys.exit(1)