| #!/usr/bin/env python3 |
| |
| # Copyright 2020 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| """This script is used to roll cobalt's submodules to match those used in Fuchsia. |
| |
| With a clean checkout, updating the submodules should be as simple as: |
| |
| ./cobaltb.py update_submodules --make_commit |
| |
| This will update all of the submodules to match those used in Fuchsia, and then |
| if there are any changes, will add them all to a commit that you can then upload |
| to Gerrit. |
| |
| If you omit the '--make_commit' flag, then the commit will not be generated, but |
| a suggested commit message will be printed out at the end of the command. |
| """ |
| |
| import tempfile |
| import xml.etree.ElementTree as ElementTree |
| import subprocess |
| import os |
| import re |
| |
| THIS_DIR = os.path.abspath(os.path.dirname(__file__)) |
| ROOT_DIR = os.path.abspath(os.path.join(THIS_DIR, '..')) |
| |
| |
| def short_hash(long_hash): |
| """Returns a shortened, still unique commit hash.""" |
| return subprocess.check_output(['git', 'rev-parse', '--short', |
| long_hash]).decode('utf-8').strip() |
| |
| |
| def hash_distance(start, end): |
| """Returns the number of commits between the two provided hashes.""" |
| return len( |
| subprocess.check_output(['git', 'rev-list', |
| '%s..%s' % (start, end) |
| ]).decode('utf-8').strip().split('\n')) |
| |
| |
| def update_from_project(project, update_report, commit_message): |
| """Attempts to update the submodule for the supplied project. |
| |
| The project is of the following format: |
| |
| <project name="third_party/abseil-cpp" |
| path="third_party/abseil-cpp" |
| revision="17b90e77683fd71c72478c2af966fe0a4c1c07be" |
| remote="https://fuchsia.googlesource.com/third_party/abseil-cpp"/> |
| """ |
| full_path = os.path.join(ROOT_DIR, project.attrib['path']) |
| if os.path.isdir(full_path): |
| savedDir = os.getcwd() |
| try: |
| os.chdir(full_path) |
| root_dir = subprocess.check_output( |
| ['git', 'rev-parse', '--show-toplevel']).decode('utf-8').strip() |
| if root_dir == ROOT_DIR: |
| # If the root_dir at this path is equal to the global ROOT_DIR, that means this path exists |
| # but is not actually a submodule. (for example //third_party/go) |
| return |
| |
| current_hash = subprocess.check_output(['git', 'rev-parse', |
| 'HEAD']).decode('utf-8').strip() |
| if current_hash != project.attrib['revision']: |
| # The current HEAD does not match the revision specified in the manifest |
| print('Updating %s to %s' % |
| (project.attrib['path'], project.attrib['revision'])) |
| subprocess.check_call(['git', 'fetch', 'origin']) |
| subprocess.check_call(['git', 'checkout', project.attrib['revision']]) |
| |
| # Generate a human readable submodule update message |
| num_commits = hash_distance(current_hash, project.attrib['revision']) |
| commit_count = '' |
| if num_commits == 1: |
| commit_count = ' (1 commit)' |
| raw_msg = subprocess.check_output([ |
| 'git', 'log', '--format=%B', '-n', '1', project.attrib['revision'] |
| ]).decode('utf-8').strip() |
| # Replace commit labels (like: Change-Id: ... with Original-Change-Id: ...) |
| filtered_msg = re.sub( |
| r'^([a-zA-Z-_]*: .*$)', r'Original-\1', raw_msg, flags=re.MULTILINE) |
| commit_message.append('[roll] Roll %s' % filtered_msg) |
| elif num_commits > 1: |
| commit_count = ' (%d commits)' % num_commits |
| update_report.append( |
| '%s %s..%s%s' % |
| (project.attrib['name'], short_hash(current_hash), |
| short_hash(project.attrib['revision']), commit_count)) |
| finally: |
| os.chdir(savedDir) |
| |
| |
| def update_from_manifest(manifest_path, commit_message): |
| """Iterates through each project in a manifest calling update_from_project for each one. |
| |
| The top level XML format is: |
| |
| <?xml version="1.0" encoding="UTF-8"?> |
| <manifest> |
| <projects> |
| <project ... /> |
| <project ... /> |
| </projects> |
| </manifest> |
| """ |
| print('Parsing %s' % manifest_path) |
| flower_tree = ElementTree.parse(manifest_path) |
| root = flower_tree.getroot() |
| update_report = [] |
| |
| print('Checking through projects...') |
| for project in root.findall('./projects/project'): |
| update_from_project(project, update_report, commit_message) |
| |
| return update_report |
| |
| |
| def update_submodules(integration_repo, manifest_files, make_commit=False): |
| """Checks out an integration repository and calls update_from_manifest on each manifest_file. |
| |
| If make_commit is true, the resulting submodule change will be added to a |
| commit. |
| """ |
| with tempfile.TemporaryDirectory( |
| prefix='fuchsia-integration-') as integration_dir: |
| print('Checking out integration repo...') |
| subprocess.check_call(['git', 'clone', integration_repo, integration_dir]) |
| print() |
| |
| update_report = [] |
| commit_message = [] |
| for manifest_file in manifest_files: |
| update_report.extend( |
| update_from_manifest( |
| os.path.join(integration_dir, manifest_file), commit_message)) |
| |
| commit_msg = '' |
| if len(update_report) > 1: |
| commit_msg = """[roll] Roll %d submodules |
| |
| %s""" % (len(update_report), '\n'.join(sorted(update_report))) |
| elif len(update_report) == 1: |
| if len(commit_message) == 1: |
| commit_msg = commit_message[0] |
| else: |
| commit_msg = "[roll] Roll %s" % update_report[0] |
| |
| if commit_msg != '': |
| if make_commit: |
| subprocess.check_call(['git', 'commit', '-am', commit_msg]) |
| else: |
| print() |
| print() |
| print(commit_msg) |
| print() |
| print() |
| |
| |
| if __name__ == '__main__': |
| update_submodules('http://fuchsia.googlesource.com/integration', |
| ['third_party/flower', 'third_party/boringssl/boringssl']) |