| #!/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 os |
| 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('--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) |
| 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: |
| 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') |
| 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:])) |