Ian Zerny | dcb172e | 2022-02-22 15:36:45 +0100 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
Christoffer Quist Adamsen | 4bb82e9 | 2019-08-14 12:30:34 +0200 | [diff] [blame] | 2 | # Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file |
| 3 | # for details. All rights reserved. Use of this source code is governed by a |
| 4 | # BSD-style license that can be found in the LICENSE file. |
| 5 | |
| 6 | # Script that automatically pulls and uploads all upstream direct and indirect |
| 7 | # branches into the current branch. |
| 8 | # |
| 9 | # Example: |
| 10 | # |
| 11 | # $ git branch -vv |
| 12 | # * feature_final xxxxxxxxx [feature_prereq_c: ...] ... |
| 13 | # feature_prereq_c xxxxxxxxx [feature_prereq_b: ...] ... |
| 14 | # feature_prereq_b xxxxxxxxx [feature_prereq_a: ...] ... |
Rico Wind | 1b52acf | 2021-03-21 12:36:55 +0100 | [diff] [blame] | 15 | # feature_prereq_a xxxxxxxxx [main: ...] ... |
| 16 | # main xxxxxxxxx [origin/main] ... |
Christoffer Quist Adamsen | 4bb82e9 | 2019-08-14 12:30:34 +0200 | [diff] [blame] | 17 | # |
| 18 | # Executing `git_sync_cl_chain.py -m <message>` causes the following chain of |
| 19 | # commands to be executed: |
| 20 | # |
| 21 | # $ git checkout feature_prereq_a; git pull; git cl upload -m <message> |
| 22 | # $ git checkout feature_prereq_b; git pull; git cl upload -m <message> |
| 23 | # $ git checkout feature_prereq_c; git pull; git cl upload -m <message> |
| 24 | # $ git checkout feature_final; git pull; git cl upload -m <message> |
| 25 | |
| 26 | import optparse |
| 27 | import os |
| 28 | import sys |
| 29 | |
| 30 | import defines |
| 31 | import utils |
| 32 | |
| 33 | REPO_ROOT = defines.REPO_ROOT |
| 34 | |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 35 | |
Christoffer Quist Adamsen | 4bb82e9 | 2019-08-14 12:30:34 +0200 | [diff] [blame] | 36 | class Repo(object): |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 37 | |
| 38 | def __init__(self, name, is_current, upstream): |
| 39 | self.name = name |
| 40 | self.is_current = is_current |
| 41 | self.upstream = upstream |
| 42 | |
Christoffer Quist Adamsen | 4bb82e9 | 2019-08-14 12:30:34 +0200 | [diff] [blame] | 43 | |
| 44 | def ParseOptions(argv): |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 45 | result = optparse.OptionParser() |
| 46 | result.add_option('--bypass-hooks', |
| 47 | help='Bypass presubmit hooks', |
| 48 | action='store_true') |
| 49 | result.add_option('--delete', |
| 50 | '-d', |
| 51 | help='Delete closed branches', |
| 52 | choices=['y', 'n', 'ask'], |
| 53 | default='ask') |
| 54 | result.add_option('--from_branch', |
| 55 | '-f', |
| 56 | help='Uppermost upstream to sync from', |
| 57 | default='main') |
| 58 | result.add_option( |
| 59 | '--leave_upstream', |
| 60 | '--leave-upstream', |
| 61 | help='To not update the upstream of the first open branch', |
| 62 | action='store_true') |
| 63 | result.add_option('--message', |
| 64 | '-m', |
| 65 | help='Message for patchset', |
| 66 | default='Sync') |
| 67 | result.add_option('--rebase', |
| 68 | help='To use `git pull --rebase` instead of `git pull`', |
| 69 | action='store_true') |
| 70 | result.add_option('--no_upload', |
| 71 | '--no-upload', |
| 72 | help='Disable uploading to Gerrit', |
| 73 | action='store_true') |
| 74 | result.add_option('--skip_main', |
| 75 | '--skip-main', |
| 76 | help='Disable syncing for main', |
| 77 | action='store_true') |
| 78 | (options, args) = result.parse_args(argv) |
| 79 | options.upload = not options.no_upload |
| 80 | assert options.delete != 'y' or not options.leave_upstream, ( |
| 81 | 'Inconsistent options: cannot leave the upstream of the first open ' + |
| 82 | 'branch (--leave_upstream) and delete the closed branches at the same ' |
| 83 | + 'time (--delete).') |
| 84 | assert options.message, 'A message for the patchset is required.' |
| 85 | assert len(args) == 0 |
| 86 | return options |
| 87 | |
Christoffer Quist Adamsen | 4bb82e9 | 2019-08-14 12:30:34 +0200 | [diff] [blame] | 88 | |
| 89 | def main(argv): |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 90 | options = ParseOptions(argv) |
| 91 | with utils.ChangedWorkingDirectory(REPO_ROOT, quiet=True): |
| 92 | branches = [ |
| 93 | parse(line) |
| 94 | for line in utils.RunCmd(['git', 'branch', '-vv'], quiet=True) |
| 95 | ] |
Christoffer Quist Adamsen | 4bb82e9 | 2019-08-14 12:30:34 +0200 | [diff] [blame] | 96 | |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 97 | current_branch = None |
| 98 | for branch in branches: |
| 99 | if branch.is_current: |
| 100 | current_branch = branch |
| 101 | break |
| 102 | assert current_branch is not None |
Christoffer Quist Adamsen | 4bb82e9 | 2019-08-14 12:30:34 +0200 | [diff] [blame] | 103 | |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 104 | if is_root_branch(current_branch, options): |
| 105 | print('Nothing to sync') |
| 106 | return |
Christoffer Quist Adamsen | 4bb82e9 | 2019-08-14 12:30:34 +0200 | [diff] [blame] | 107 | |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 108 | stack = [] |
| 109 | while current_branch: |
| 110 | stack.append(current_branch) |
| 111 | if is_root_branch(current_branch, options): |
| 112 | break |
| 113 | current_branch = get_branch_with_name(current_branch.upstream, |
| 114 | branches) |
Christoffer Quist Adamsen | 4bb82e9 | 2019-08-14 12:30:34 +0200 | [diff] [blame] | 115 | |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 116 | closed_branches = [] |
| 117 | has_seen_local_branch = False # A branch that is not uploaded. |
| 118 | has_seen_open_branch = False # A branch that is not closed. |
| 119 | while len(stack) > 0: |
| 120 | branch = stack.pop() |
Christoffer Quist Adamsen | 6fac89e | 2020-11-06 11:03:09 +0100 | [diff] [blame] | 121 | |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 122 | utils.RunCmd(['git', 'checkout', branch.name], quiet=True) |
Christoffer Quist Adamsen | 6fac89e | 2020-11-06 11:03:09 +0100 | [diff] [blame] | 123 | |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 124 | status = get_status_for_current_branch(branch) |
| 125 | print('Syncing %s (status: %s)' % (branch.name, status)) |
Christoffer Quist Adamsen | 6fac89e | 2020-11-06 11:03:09 +0100 | [diff] [blame] | 126 | |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 127 | pull_for_current_branch(branch, options) |
Christoffer Quist Adamsen | 6fac89e | 2020-11-06 11:03:09 +0100 | [diff] [blame] | 128 | |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 129 | if branch.name == 'main': |
| 130 | continue |
Christoffer Quist Adamsen | 6fac89e | 2020-11-06 11:03:09 +0100 | [diff] [blame] | 131 | |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 132 | if status == 'closed': |
| 133 | assert not has_seen_local_branch, ( |
| 134 | 'Unexpected closed branch %s after new branch' % |
| 135 | branch.name) |
| 136 | assert not has_seen_open_branch, ( |
| 137 | 'Unexpected closed branch %s after open branch' % |
| 138 | branch.name) |
| 139 | closed_branches.append(branch.name) |
| 140 | continue |
Christoffer Quist Adamsen | 6fac89e | 2020-11-06 11:03:09 +0100 | [diff] [blame] | 141 | |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 142 | if not options.leave_upstream: |
| 143 | if not has_seen_open_branch and len(closed_branches) > 0: |
| 144 | print('Setting upstream for first open branch %s to main' % |
| 145 | branch.name) |
| 146 | set_upstream_for_current_branch_to_main() |
Christoffer Quist Adamsen | 6fac89e | 2020-11-06 11:03:09 +0100 | [diff] [blame] | 147 | |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 148 | has_seen_open_branch = True |
| 149 | has_seen_local_branch = has_seen_local_branch or (status == 'None') |
Christoffer Quist Adamsen | 6fac89e | 2020-11-06 11:03:09 +0100 | [diff] [blame] | 150 | |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 151 | if options.upload and status != 'closed': |
| 152 | if has_seen_local_branch: |
| 153 | print( |
| 154 | 'Cannot upload branch %s since it comes after a local branch' |
| 155 | % branch.name) |
| 156 | else: |
| 157 | upload_cmd = ['git', 'cl', 'upload', '-m', options.message] |
| 158 | if options.bypass_hooks: |
| 159 | upload_cmd.append('--bypass-hooks') |
| 160 | utils.RunCmd(upload_cmd, quiet=True) |
Christoffer Quist Adamsen | 6fac89e | 2020-11-06 11:03:09 +0100 | [diff] [blame] | 161 | |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 162 | if get_delete_branches_option(closed_branches, options): |
| 163 | delete_branches(closed_branches) |
Christoffer Quist Adamsen | 4bb82e9 | 2019-08-14 12:30:34 +0200 | [diff] [blame] | 164 | |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 165 | utils.RunCmd(['git', 'cl', 'issue']) |
| 166 | |
Morten Krogh-Jespersen | c2be3f8 | 2019-08-14 15:30:55 +0200 | [diff] [blame] | 167 | |
Christoffer Quist Adamsen | 6fac89e | 2020-11-06 11:03:09 +0100 | [diff] [blame] | 168 | def delete_branches(branches): |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 169 | assert len(branches) > 0 |
| 170 | cmd = ['git', 'branch', '-D'] |
| 171 | cmd.extend(branches) |
| 172 | utils.RunCmd(cmd, quiet=True) |
| 173 | |
Christoffer Quist Adamsen | 6fac89e | 2020-11-06 11:03:09 +0100 | [diff] [blame] | 174 | |
Christoffer Quist Adamsen | 4bb82e9 | 2019-08-14 12:30:34 +0200 | [diff] [blame] | 175 | def get_branch_with_name(name, branches): |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 176 | for branch in branches: |
| 177 | if branch.name == name: |
| 178 | return branch |
| 179 | return None |
| 180 | |
Christoffer Quist Adamsen | 4bb82e9 | 2019-08-14 12:30:34 +0200 | [diff] [blame] | 181 | |
Christoffer Quist Adamsen | d323044 | 2020-11-06 11:12:14 +0100 | [diff] [blame] | 182 | def get_delete_branches_option(closed_branches, options): |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 183 | if len(closed_branches) == 0: |
| 184 | return False |
| 185 | if options.leave_upstream: |
| 186 | return False |
| 187 | if options.delete == 'y': |
| 188 | return True |
| 189 | if options.delete == 'n': |
| 190 | return False |
| 191 | assert options.delete == 'ask' |
| 192 | print('Delete closed branches: %s (Y/N)?' % ", ".join(closed_branches)) |
| 193 | answer = sys.stdin.read(1) |
| 194 | return answer.lower() == 'y' |
| 195 | |
Christoffer Quist Adamsen | 6fac89e | 2020-11-06 11:03:09 +0100 | [diff] [blame] | 196 | |
Christoffer Quist Adamsen | 7607ebe | 2022-06-28 11:52:46 +0200 | [diff] [blame] | 197 | def get_status_for_current_branch(current_branch): |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 198 | if current_branch.name == 'main': |
| 199 | return 'main' |
| 200 | return utils.RunCmd(['git', 'cl', 'status', '--field', 'status'], |
| 201 | quiet=True)[0].strip() |
| 202 | |
Christoffer Quist Adamsen | 6fac89e | 2020-11-06 11:03:09 +0100 | [diff] [blame] | 203 | |
Christoffer Quist Adamsen | e2d3f85 | 2022-01-12 13:24:07 +0100 | [diff] [blame] | 204 | def is_root_branch(branch, options): |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 205 | return branch.name == options.from_branch or branch.upstream is None |
| 206 | |
Christoffer Quist Adamsen | e2d3f85 | 2022-01-12 13:24:07 +0100 | [diff] [blame] | 207 | |
Christoffer Quist Adamsen | 6fac89e | 2020-11-06 11:03:09 +0100 | [diff] [blame] | 208 | def pull_for_current_branch(branch, options): |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 209 | if branch.name == 'main' and options.skip_main: |
| 210 | return |
| 211 | rebase_args = ['--rebase'] if options.rebase else [] |
| 212 | utils.RunCmd(['git', 'pull'] + rebase_args, quiet=True) |
Christoffer Quist Adamsen | 6fac89e | 2020-11-06 11:03:09 +0100 | [diff] [blame] | 213 | |
| 214 | |
Rico Wind | 1b52acf | 2021-03-21 12:36:55 +0100 | [diff] [blame] | 215 | def set_upstream_for_current_branch_to_main(): |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 216 | utils.RunCmd(['git', 'cl', 'upstream', 'main'], quiet=True) |
| 217 | |
Christoffer Quist Adamsen | 6fac89e | 2020-11-06 11:03:09 +0100 | [diff] [blame] | 218 | |
Christoffer Quist Adamsen | 4bb82e9 | 2019-08-14 12:30:34 +0200 | [diff] [blame] | 219 | # Parses a line from the output of `git branch -vv`. |
| 220 | # |
| 221 | # Example output ('*' denotes the current branch): |
| 222 | # |
| 223 | # $ git branch -vv |
| 224 | # * feature_final xxxxxxxxx [feature_prereq_c: ...] ... |
| 225 | # feature_prereq_c xxxxxxxxx [feature_prereq_b: ...] ... |
| 226 | # feature_prereq_b xxxxxxxxx [feature_prereq_a: ...] ... |
Rico Wind | 1b52acf | 2021-03-21 12:36:55 +0100 | [diff] [blame] | 227 | # feature_prereq_a xxxxxxxxx [main: ...] ... |
| 228 | # main xxxxxxxxx [origin/main] ... |
Christoffer Quist Adamsen | 4bb82e9 | 2019-08-14 12:30:34 +0200 | [diff] [blame] | 229 | def parse(line): |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 230 | is_current = False |
| 231 | if line.startswith('*'): |
| 232 | is_current = True |
| 233 | line = line[1:].lstrip() |
| 234 | else: |
| 235 | line = line.lstrip() |
Christoffer Quist Adamsen | 4bb82e9 | 2019-08-14 12:30:34 +0200 | [diff] [blame] | 236 | |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 237 | name_end_index = line.index(' ') |
| 238 | name = line[:name_end_index] |
| 239 | line = line[name_end_index:].lstrip() |
Christoffer Quist Adamsen | 4bb82e9 | 2019-08-14 12:30:34 +0200 | [diff] [blame] | 240 | |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 241 | if '[' in line: |
| 242 | line = line[line.index('[') + 1:] |
Christoffer Quist Adamsen | 4bb82e9 | 2019-08-14 12:30:34 +0200 | [diff] [blame] | 243 | |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 244 | if ':' in line: |
| 245 | upstream = line[:line.index(':')] |
| 246 | return Repo(name, is_current, upstream) |
Christoffer Quist Adamsen | 4bb82e9 | 2019-08-14 12:30:34 +0200 | [diff] [blame] | 247 | |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 248 | if ']' in line: |
| 249 | upstream = line[:line.index(']')] |
| 250 | return Repo(name, is_current, upstream) |
Christoffer Quist Adamsen | 7a5deed | 2020-11-16 11:31:15 +0100 | [diff] [blame] | 251 | |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 252 | return Repo(name, is_current, None) |
| 253 | |
Christoffer Quist Adamsen | 4bb82e9 | 2019-08-14 12:30:34 +0200 | [diff] [blame] | 254 | |
| 255 | if __name__ == '__main__': |
Christoffer Quist Adamsen | 2434a4d | 2023-10-16 11:29:03 +0200 | [diff] [blame] | 256 | sys.exit(main(sys.argv[1:])) |