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