blob: 66713faf888f04cd60eb84fbd96ae8c61f84f014 [file] [log] [blame]
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +02001#!/usr/bin/env python
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: ...] ...
15# feature_prereq_a xxxxxxxxx [master: ...] ...
16# master xxxxxxxxx [origin/master] ...
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
26import optparse
27import os
28import sys
29
30import defines
31import utils
32
33REPO_ROOT = defines.REPO_ROOT
34
35class Repo(object):
36 def __init__(self, name, is_current, upstream):
37 self.name = name
38 self.is_current = is_current
39 self.upstream = upstream
40
41def ParseOptions(argv):
42 result = optparse.OptionParser()
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +010043 result.add_option('--delete', '-d',
44 help='Delete closed branches',
45 choices=['y', 'n', 'ask'],
46 default='ask')
47 result.add_option('--leave_upstream', '--leave-upstream',
48 help='To not update the upstream of the first open branch',
49 action='store_true')
50 result.add_option('--message', '-m',
51 help='Message for patchset', default='Sync')
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020052 result.add_option('--rebase',
53 help='To use `git pull --rebase` instead of `git pull`',
54 action='store_true')
Christoffer Quist Adamsena8212812019-08-14 15:38:50 +020055 result.add_option('--no_upload', '--no-upload',
56 help='Disable uploading to Gerrit', action='store_true')
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +010057 result.add_option('--skip_master', '--skip-master',
58 help='Disable syncing for master',
59 action='store_true')
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020060 (options, args) = result.parse_args(argv)
Christoffer Quist Adamsena8212812019-08-14 15:38:50 +020061 options.upload = not options.no_upload
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +010062 assert options.delete != 'y' or not options.leave_upstream, (
63 'Inconsistent options: cannot leave the upstream of the first open ' +
64 'branch (--leave_upstream) and delete the closed branches at the same ' +
65 'time (--delete).')
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020066 assert options.message, 'A message for the patchset is required.'
67 assert len(args) == 0
68 return options
69
70def main(argv):
71 options = ParseOptions(argv)
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020072 with utils.ChangedWorkingDirectory(REPO_ROOT, quiet=True):
73 branches = [
74 parse(line)
75 for line in utils.RunCmd(['git', 'branch', '-vv'], quiet=True)]
76
77 current_branch = None
78 for branch in branches:
79 if branch.is_current:
80 current_branch = branch
81 break
82 assert current_branch is not None
83
84 if current_branch.upstream == None:
85 print('Nothing to sync')
86 return
87
88 stack = []
Morten Krogh-Jespersen5b225f42019-08-14 15:26:06 +020089 while current_branch:
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020090 stack.append(current_branch)
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +010091 if current_branch.upstream is None:
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020092 break
93 current_branch = get_branch_with_name(current_branch.upstream, branches)
94
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +010095 closed_branches = []
96 has_seen_local_branch = False # A branch that is not uploaded.
97 has_seen_open_branch = False # A branch that is not closed.
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +020098 while len(stack) > 0:
99 branch = stack.pop()
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100100
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200101 utils.RunCmd(['git', 'checkout', branch.name], quiet=True)
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100102
103 status = get_status_for_current_branch()
104 print('Syncing %s (status: %s)' % (branch.name, status))
105
106 pull_for_current_branch(branch, options)
107
108 if branch.name == 'master':
109 continue
110
111 if status == 'closed':
112 assert not has_seen_local_branch, (
113 'Unexpected closed branch %s after new branch' % branch.name)
114 assert not has_seen_open_branch, (
115 'Unexpected closed branch %s after open branch' % branch.name)
116 closed_branches.append(branch.name)
117 continue
118
119 if not options.leave_upstream:
120 if not has_seen_open_branch and len(closed_branches) > 0:
121 print(
122 'Setting upstream for first open branch %s to master'
123 % branch.name)
124 set_upstream_for_current_branch_to_master()
125
126 has_seen_open_branch = True
127 has_seen_local_branch = has_seen_local_branch or (status == 'None')
128
Morten Krogh-Jespersen25b512f2020-11-10 11:52:26 +0100129 if options.upload and status != 'closed':
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100130 if has_seen_local_branch:
131 print(
132 'Cannot upload branch %s since it comes after a local branch'
133 % branch.name)
134 else:
135 utils.RunCmd(
136 ['git', 'cl', 'upload', '-m', options.message], quiet=True)
137
Christoffer Quist Adamsend3230442020-11-06 11:12:14 +0100138 if get_delete_branches_option(closed_branches, options):
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100139 delete_branches(closed_branches)
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200140
Morten Krogh-Jespersenc2be3f82019-08-14 15:30:55 +0200141 utils.RunCmd(['git', 'cl', 'issue'])
142
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100143def delete_branches(branches):
144 assert len(branches) > 0
145 utils.RunCmd(['git', 'branch', '-D'].extend(branches), quiet=True)
146
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200147def get_branch_with_name(name, branches):
148 for branch in branches:
149 if branch.name == name:
150 return branch
151 return None
152
Christoffer Quist Adamsend3230442020-11-06 11:12:14 +0100153def get_delete_branches_option(closed_branches, options):
154 if len(closed_branches) == 0:
155 return False
Christoffer Quist Adamsen6fac89e2020-11-06 11:03:09 +0100156 if options.leave_upstream:
157 return False
158 if options.delete == 'y':
159 return True
160 if options.delete == 'n':
161 return False
162 assert options.delete == 'ask'
163 print('Delete closed branches: %s (Y/N)?' % ", ".join(closed_branches))
164 answer = sys.stdin.read(1)
165 return answer.lower() == 'y'
166
167def get_status_for_current_branch():
168 return utils.RunCmd(['git', 'cl', 'status', '--field', 'status'], quiet=True)[0].strip()
169
170def pull_for_current_branch(branch, options):
171 if branch.name == 'master' and options.skip_master:
172 return
173 rebase_args = ['--rebase'] if options.rebase else []
174 utils.RunCmd(['git', 'pull'] + rebase_args, quiet=True)
175
176
177def set_upstream_for_current_branch_to_master():
178 utils.RunCmd(['git', 'cl', 'upstream', 'master'], quiet=True)
179
Christoffer Quist Adamsen4bb82e92019-08-14 12:30:34 +0200180# Parses a line from the output of `git branch -vv`.
181#
182# Example output ('*' denotes the current branch):
183#
184# $ git branch -vv
185# * feature_final xxxxxxxxx [feature_prereq_c: ...] ...
186# feature_prereq_c xxxxxxxxx [feature_prereq_b: ...] ...
187# feature_prereq_b xxxxxxxxx [feature_prereq_a: ...] ...
188# feature_prereq_a xxxxxxxxx [master: ...] ...
189# master xxxxxxxxx [origin/master] ...
190def parse(line):
191 is_current = False
192 if line.startswith('*'):
193 is_current = True
194 line = line[1:].lstrip()
195 else:
196 line = line.lstrip()
197
198 name_end_index = line.index(' ')
199 name = line[:name_end_index]
200 line = line[name_end_index:].lstrip()
201
202 if ('[') not in line or ':' not in line:
203 return Repo(name, is_current, None)
204
205 upstream_start_index = line.index('[')
206 line = line[upstream_start_index+1:]
207 upstream_end_index = line.index(':')
208 upstream = line[:upstream_end_index]
209
210 return Repo(name, is_current, upstream)
211
212if __name__ == '__main__':
213 sys.exit(main(sys.argv[1:]))