blob: 6d047fb9af1e8f005a4558e45729ba90b00800ec [file] [log] [blame]
#!/usr/bin/env python
# utils/update-checkout - Utility to update your local checkouts -*- python -*-
#
# This source file is part of the Swift.org open source project
#
# Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
# Licensed under Apache License v2.0 with Runtime Library Exception
#
# See https://swift.org/LICENSE.txt for license information
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
from __future__ import print_function
import argparse
import json
import os
import re
import sys
from functools import reduce
sys.path.append(os.path.dirname(__file__))
from SwiftBuildSupport import (
SWIFT_SOURCE_ROOT,
) # noqa (E402 module level import not at top of file)
SCRIPT_FILE = os.path.abspath(__file__)
SCRIPT_DIR = os.path.dirname(SCRIPT_FILE)
sys.path.append(os.path.join(SCRIPT_DIR, 'swift_build_support'))
from swift_build_support import shell # noqa (E402)
def update_single_repository(repo_path, branch, reset_to_remote, should_clean,
cross_repo):
if not os.path.isdir(repo_path):
return
print("--- Updating '" + repo_path + "' ---")
with shell.pushd(repo_path, dry_run=False, echo=False):
shell.call(["git", "fetch", "--recurse-submodules=yes"], echo=True)
if should_clean:
shell.call(['git', 'clean', '-fdx'],
echo=True)
shell.call(['git', 'submodule', 'foreach', '--recursive', 'git',
'clean', '-fdx'], echo=True)
shell.call(['git', 'submodule', 'foreach', '--recursive', 'git',
'reset', '--hard', 'HEAD'], echo=True)
status = shell.call(['git', 'reset', '--hard', 'HEAD'],
echo=True)
if status:
print("Please, commit your changes.")
print(status)
exit(1)
if branch:
status = shell.capture(['git', 'status', '--porcelain', '-uno'],
echo=False)
if status:
print("Please, commit your changes.")
print(status)
exit(1)
shell.call(['git', 'checkout', branch], echo=True)
# If we were asked to reset to the specified branch, do the hard
# reset and return.
if reset_to_remote and not cross_repo:
shell.call(['git', 'reset', '--hard', "origin/%s" % branch],
echo=True)
return
# Prior to Git 2.6, this is the way to do a "git pull
# --rebase" that respects rebase.autostash. See
# http://stackoverflow.com/a/30209750/125349
if not cross_repo:
shell.call(["git", "rebase", "FETCH_HEAD"], echo=True)
shell.call(["git", "submodule", "update", "--recursive"],
echo=True)
def update_repository_to_tag(args, repo_name, repo_path, tag_name):
with shell.pushd(repo_path, dry_run=False, echo=False):
tag_exists = shell.capture(['git', 'ls-remote', '--tags',
'origin', tag_name], echo=False)
if not tag_exists:
print("--- Skipping '" + repo_name + "' ---")
return
update_single_repository(repo_path,
tag_name,
args.reset_to_remote,
args.clean,
cross_repo=True)
def update_repository_to_scheme(
args, config, repo_name, repo_path, scheme_name, cross_repos_pr):
cross_repo = False
repo_branch = scheme_name
# This loop is only correct, since we know that each alias set has
# unique contents. This is checked by verify config. Thus the first
# branch scheme data that has scheme_name as one of its aliases is
# the only possible correct answer.
for v in config['branch-schemes'].values():
if scheme_name not in v['aliases']:
continue
repo_branch = v['repos'][repo_name]
remote_repo_id = config['repos'][repo_name]['remote']['id']
if remote_repo_id in cross_repos_pr:
cross_repo = True
pr_id = cross_repos_pr[remote_repo_id]
repo_branch = "ci_pr_{0}".format(pr_id)
with shell.pushd(repo_path, dry_run=False, echo=False):
shell.call(["git", "checkout", v['repos'][repo_name]],
echo=True)
shell.capture(["git", "branch", "-D", repo_branch],
echo=True, allow_non_zero_exit=True)
shell.call(["git", "fetch", "origin",
"pull/{0}/merge:{1}"
.format(pr_id, repo_branch)], echo=True)
break
update_single_repository(repo_path,
repo_branch,
args.reset_to_remote,
args.clean,
cross_repo)
def update_all_repositories(args, config, scheme_name, cross_repos_pr):
for repo_name in config['repos'].keys():
if repo_name in args.skip_repository_list:
print("--- Skipping '" + repo_name + "' ---")
continue
repo_path = os.path.join(SWIFT_SOURCE_ROOT, repo_name)
if args.tag:
update_repository_to_tag(args, repo_name, repo_path, args.tag)
elif scheme_name:
update_repository_to_scheme(args,
config,
repo_name,
repo_path,
scheme_name,
cross_repos_pr)
else:
update_single_repository(repo_path,
branch=None,
reset_to_remote=args.reset_to_remote,
should_clean=args.clean,
cross_repo=False)
def obtain_additional_swift_sources(
config, with_ssh, scheme_name, skip_history, skip_repository_list):
with shell.pushd(SWIFT_SOURCE_ROOT, dry_run=False,
echo=False):
for repo_name, repo_info in config['repos'].items():
if repo_name in skip_repository_list:
print("--- Skipping '" + repo_name + "' ---")
continue
if os.path.isdir(os.path.join(repo_name, ".git")):
continue
print("--- Cloning '" + repo_name + "' ---")
# If we have a url override, use that url instead of
# interpolating.
remote_repo_info = repo_info['remote']
if 'url' in remote_repo_info:
remote = remote_repo_info['url']
else:
remote_repo_id = remote_repo_info['id']
if with_ssh is True or 'https-clone-pattern' not in config:
remote = config['ssh-clone-pattern'] % remote_repo_id
else:
remote = config['https-clone-pattern'] % remote_repo_id
if skip_history:
shell.call(['git', 'clone', '--recursive', '--depth', '1',
remote, repo_name], echo=True)
else:
shell.call(['git', 'clone', '--recursive', remote,
repo_name], echo=True)
if scheme_name:
for v in config['branch-schemes'].values():
if scheme_name not in v['aliases']:
continue
repo_branch = v['repos'][repo_name]
break
else:
repo_branch = scheme_name
src_path = os.path.join(SWIFT_SOURCE_ROOT, repo_name,
".git")
shell.call(['git', '--git-dir', src_path, '--work-tree',
os.path.join(SWIFT_SOURCE_ROOT, repo_name),
'checkout', repo_branch], echo=False)
with shell.pushd(os.path.join(SWIFT_SOURCE_ROOT, repo_name),
dry_run=False, echo=False):
shell.call(["git", "submodule", "update", "--recursive"],
echo=False)
def dump_repo_hashes(config):
max_len = reduce(lambda acc, x: max(acc, len(x)),
config['repos'].keys(), 0)
fmt = "{:<%r}{}" % (max_len+5)
for repo_name, repo_info in sorted(config['repos'].items(),
key=lambda x: x[0]):
with shell.pushd(os.path.join(SWIFT_SOURCE_ROOT, repo_name),
dry_run=False,
echo=False):
h = shell.capture(["git", "log", "--oneline", "-n", "1"],
echo=False).strip()
print(fmt.format(repo_name, h))
def validate_config(config):
# Make sure that our branch-names are unique.
scheme_names = config['branch-schemes'].keys()
if len(scheme_names) != len(set(scheme_names)):
raise RuntimeError('Configuration file has duplicate schemes?!')
# Then make sure the alias names used by our branches are unique.
#
# We do this by constructing a list consisting of len(names),
# set(names). Then we reduce over that list summing the counts and taking
# the union of the sets. We have uniqueness if the length of the union
# equals the length of the sum of the counts.
data = [(len(v['aliases']), set(v['aliases']))
for v in config['branch-schemes'].values()]
result = reduce(lambda acc, x: (acc[0] + x[0], acc[1] | x[1]), data,
(0, set([])))
if result[0] == len(result[1]):
return
raise RuntimeError('Configuration file has schemes with duplicate '
'aliases?!')
def main():
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description="""
repositories.
By default, updates your checkouts of Swift, SourceKit, LLDB, and SwiftPM.""")
parser.add_argument(
"--clone",
help="Obtain Sources for Swift and Related Projects",
action="store_true")
parser.add_argument(
"--clone-with-ssh",
help="Obtain Sources for Swift and Related Projects via SSH",
action="store_true")
parser.add_argument(
"--skip-history",
help="Skip histories when obtaining sources",
action="store_true")
parser.add_argument(
"--skip-repository",
metavar="DIRECTORY",
default=[],
help="Skip the specified repository",
dest='skip_repository_list',
action="append")
parser.add_argument(
"--scheme", "--branch",
help='Use branches from the specified branch-scheme',
metavar='BRANCH-SCHEME',
dest='scheme')
parser.add_argument(
'--reset-to-remote',
help='Reset each branch to the remote state.',
action='store_true')
parser.add_argument(
'--clean',
help='Clean unrelated files from each repository.',
action='store_true')
parser.add_argument(
"--config",
default=os.path.join(SCRIPT_DIR, "update-checkout-config.json"),
help="Configuration file to use")
parser.add_argument(
"--github-comment",
help="""Check out related pull requests referenced in the given
free-form GitHub-style comment.""",
metavar='GITHUB-COMMENT',
dest='github_comment')
parser.add_argument(
'--dump-hashes',
action='store_true',
help='Dump the git hashes of all repositories being tracked')
parser.add_argument(
"--tag",
help="""Check out each repository to the specified tag.""",
metavar='TAG-NAME')
args = parser.parse_args()
clone = args.clone
clone_with_ssh = args.clone_with_ssh
skip_history = args.skip_history
scheme = args.scheme
github_comment = args.github_comment
with open(args.config) as f:
config = json.load(f)
validate_config(config)
if args.dump_hashes:
dump_repo_hashes(config)
return 0
cross_repos_pr = {}
if github_comment:
regex_pr = r'(apple/[-a-zA-Z0-9_]+/pull/\d+|apple/[-a-zA-Z0-9_]+#\d+)'
repos_with_pr = re.findall(regex_pr, github_comment)
print("Found related pull requests:", str(repos_with_pr))
repos_with_pr = [pr.replace('/pull/', '#') for pr in repos_with_pr]
cross_repos_pr = dict(pr.split('#') for pr in repos_with_pr)
if clone or clone_with_ssh:
# If branch is None, default to using the default branch alias
# specified by our configuration file.
if scheme is None:
scheme = config['default-branch-scheme']
obtain_additional_swift_sources(
config, clone_with_ssh, scheme, skip_history,
args.skip_repository_list)
update_all_repositories(args, config, scheme, cross_repos_pr)
return 0
if __name__ == "__main__":
sys.exit(main())