| #!/usr/bin/env python3 | |
| # Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file | |
| # for details. All rights reserved. Use of this source code is governed by a | |
| # BSD-style license that can be found in the LICENSE file. | |
| # Script that automatically pulls and uploads all upstream direct and indirect | |
| # branches into the current branch. | |
| # | |
| # Example: | |
| # | |
| # $ git branch -vv | |
| # * feature_final xxxxxxxxx [feature_prereq_c: ...] ... | |
| # feature_prereq_c xxxxxxxxx [feature_prereq_b: ...] ... | |
| # feature_prereq_b xxxxxxxxx [feature_prereq_a: ...] ... | |
| # feature_prereq_a xxxxxxxxx [main: ...] ... | |
| # main xxxxxxxxx [origin/main] ... | |
| # | |
| # Executing `git_sync_cl_chain.py -m <message>` causes the following chain of | |
| # commands to be executed: | |
| # | |
| # $ git checkout feature_prereq_a; git pull; git cl upload -m <message> | |
| # $ git checkout feature_prereq_b; git pull; git cl upload -m <message> | |
| # $ git checkout feature_prereq_c; git pull; git cl upload -m <message> | |
| # $ git checkout feature_final; git pull; git cl upload -m <message> | |
| import optparse | |
| import sys | |
| import defines | |
| import utils | |
| REPO_ROOT = defines.REPO_ROOT | |
| class Repo(object): | |
| def __init__(self, name, is_current, upstream): | |
| self.name = name | |
| self.is_current = is_current | |
| self.upstream = upstream | |
| def ParseOptions(argv): | |
| result = optparse.OptionParser() | |
| result.add_option('--bypass-hooks', | |
| help='Bypass presubmit hooks', | |
| action='store_true') | |
| result.add_option( | |
| '--dry-run', | |
| help='Send the top patchset to do a CQ dry run right after upload', | |
| action='store_true') | |
| result.add_option( | |
| '--dry-run-all', | |
| help='Send all patchset(s) to do a CQ dry run right after upload', | |
| action='store_true') | |
| result.add_option('--delete', | |
| '-d', | |
| help='Delete closed branches', | |
| choices=['y', 'n', 'ask'], | |
| default='ask') | |
| result.add_option('--from_branch', | |
| '-f', | |
| help='Uppermost upstream to sync from', | |
| default='main') | |
| result.add_option( | |
| '--leave_upstream', | |
| '--leave-upstream', | |
| help='To not update the upstream of the first open branch', | |
| action='store_true') | |
| result.add_option('--message', | |
| '-m', | |
| help='Message for patchset', | |
| default='Sync') | |
| result.add_option('--rebase', | |
| help='To use `git pull --rebase` instead of `git pull`', | |
| action='store_true') | |
| result.add_option('--no_upload', | |
| '--no-upload', | |
| help='Disable uploading to Gerrit', | |
| action='store_true') | |
| result.add_option('--skip_main', | |
| '--skip-main', | |
| help='Disable syncing for main', | |
| action='store_true') | |
| (options, args) = result.parse_args(argv) | |
| options.upload = not options.no_upload | |
| assert options.delete != 'y' or not options.leave_upstream, ( | |
| 'Inconsistent options: cannot leave the upstream of the first open ' + | |
| 'branch (--leave_upstream) and delete the closed branches at the same ' | |
| + 'time (--delete).') | |
| assert options.message, 'A message for the patchset is required.' | |
| assert len(args) == 0 | |
| return options | |
| def main(argv): | |
| options = ParseOptions(argv) | |
| utils.check_gcert() | |
| with utils.ChangedWorkingDirectory(REPO_ROOT, quiet=True): | |
| branches = [ | |
| parse(line) | |
| for line in utils.RunCmd(['git', 'branch', '-vv'], quiet=True) | |
| ] | |
| current_branch = None | |
| for branch in branches: | |
| if branch.is_current: | |
| current_branch = branch | |
| break | |
| assert current_branch is not None | |
| if is_root_branch(current_branch, options): | |
| print('Nothing to sync') | |
| return | |
| stack = [] | |
| while current_branch: | |
| stack.append(current_branch) | |
| if is_root_branch(current_branch, options): | |
| break | |
| current_branch = get_branch_with_name(current_branch.upstream, | |
| branches) | |
| closed_branches = [] | |
| has_seen_local_branch = False # A branch that is not uploaded. | |
| has_seen_open_branch = False # A branch that is not closed. | |
| while len(stack) > 0: | |
| top_of_stack = len(stack) == 0 | |
| branch = stack.pop() | |
| utils.RunCmd(['git', 'checkout', branch.name], quiet=True) | |
| status = get_status_for_current_branch(branch) | |
| print('Syncing %s (status: %s)' % (branch.name, status)) | |
| pull_for_current_branch(branch, options) | |
| if branch.name == 'main': | |
| continue | |
| if status == 'closed': | |
| assert not has_seen_local_branch, ( | |
| 'Unexpected closed branch %s after new branch' % | |
| branch.name) | |
| assert not has_seen_open_branch, ( | |
| 'Unexpected closed branch %s after open branch' % | |
| branch.name) | |
| closed_branches.append(branch.name) | |
| continue | |
| if not options.leave_upstream: | |
| if not has_seen_open_branch and len(closed_branches) > 0: | |
| print('Setting upstream for first open branch %s to main' % | |
| branch.name) | |
| set_upstream_for_current_branch_to_main() | |
| has_seen_open_branch = True | |
| has_seen_local_branch = has_seen_local_branch or (status == 'None') | |
| if options.upload and status != 'closed': | |
| if has_seen_local_branch: | |
| print( | |
| 'Cannot upload branch %s since it comes after a local branch' | |
| % branch.name) | |
| else: | |
| upload_cmd = ['git', 'cl', 'upload', '-m', options.message] | |
| if options.bypass_hooks: | |
| upload_cmd.append('--bypass-hooks') | |
| if (options.dry_run and | |
| top_of_stack) or options.dry_run_all: | |
| upload_cmd.append('--dry-run') | |
| utils.RunCmd(upload_cmd, quiet=True) | |
| if get_delete_branches_option(closed_branches, options): | |
| delete_branches(closed_branches) | |
| utils.RunCmd(['git', 'cl', 'issue']) | |
| def delete_branches(branches): | |
| assert len(branches) > 0 | |
| cmd = ['git', 'branch', '-D'] | |
| cmd.extend(branches) | |
| utils.RunCmd(cmd, quiet=True) | |
| def get_branch_with_name(name, branches): | |
| for branch in branches: | |
| if branch.name == name: | |
| return branch | |
| return None | |
| def get_delete_branches_option(closed_branches, options): | |
| if len(closed_branches) == 0: | |
| return False | |
| if options.leave_upstream: | |
| return False | |
| if options.delete == 'y': | |
| return True | |
| if options.delete == 'n': | |
| return False | |
| assert options.delete == 'ask' | |
| print('Delete closed branches: %s (Y/N)?' % ", ".join(closed_branches)) | |
| answer = sys.stdin.read(1) | |
| return answer.lower() == 'y' | |
| def get_status_for_current_branch(current_branch): | |
| if current_branch.name == 'main': | |
| return 'main' | |
| return utils.RunCmd(['git', 'cl', 'status', '--field', 'status'], | |
| quiet=True)[0].strip() | |
| def is_root_branch(branch, options): | |
| return branch.name == options.from_branch or branch.upstream is None | |
| def pull_for_current_branch(branch, options): | |
| if branch.name == 'main' and options.skip_main: | |
| return | |
| rebase_args = ['--rebase'] if options.rebase else [] | |
| utils.RunCmd(['git', 'pull'] + rebase_args, quiet=True) | |
| def set_upstream_for_current_branch_to_main(): | |
| utils.RunCmd(['git', 'cl', 'upstream', 'main'], quiet=True) | |
| # Parses a line from the output of `git branch -vv`. | |
| # | |
| # Example output ('*' denotes the current branch): | |
| # | |
| # $ git branch -vv | |
| # * feature_final xxxxxxxxx [feature_prereq_c: ...] ... | |
| # feature_prereq_c xxxxxxxxx [feature_prereq_b: ...] ... | |
| # feature_prereq_b xxxxxxxxx [feature_prereq_a: ...] ... | |
| # feature_prereq_a xxxxxxxxx [main: ...] ... | |
| # main xxxxxxxxx [origin/main] ... | |
| def parse(line): | |
| is_current = False | |
| if line.startswith('*'): | |
| is_current = True | |
| line = line[1:].lstrip() | |
| else: | |
| line = line.lstrip() | |
| name_end_index = line.index(' ') | |
| name = line[:name_end_index] | |
| line = line[name_end_index:].lstrip() | |
| if '[' in line: | |
| line = line[line.index('[') + 1:] | |
| if ':' in line: | |
| upstream = line[:line.index(':')] | |
| return Repo(name, is_current, upstream) | |
| if ']' in line: | |
| upstream = line[:line.index(']')] | |
| return Repo(name, is_current, upstream) | |
| return Repo(name, is_current, None) | |
| if __name__ == '__main__': | |
| sys.exit(main(sys.argv[1:])) |