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