Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(297)

Side by Side Diff: git_drover.py

Issue 1342383002: Add a git-drover. (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/depot_tools.git@master
Patch Set: Created 5 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 #!/usr/bin/env python
2 # Copyright 2015 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5 """git drover: A tool for merging changes to release branches."""
6
7 import argparse
8 import logging
9 import os
10 import shutil
11 import subprocess
12 import sys
13 import tempfile
14
15
16 class Error(Exception):
17 pass
18
19
20 class _Drover(object):
21
22 def __init__(self, branch, cl, parent_repo, dry_run):
23 self._branch = branch
24 self._cl = cl
25 self._parent_repo = os.path.abspath(parent_repo)
26 self._dry_run = dry_run
27 self._workdir = None
28 self._branch_name = None
29 self._dev_null_file = None
30 self._cherry_pick_in_progress = False
31 self._cwd = os.getcwd()
32
33 def run(self):
34 """Runs this Drover instance.
35
36 Raises:
37 Error: An error occurred while attempting to merge this change.
38 """
39 try:
40 self._run_internal()
41 finally:
42 self._cleanup()
43
44 def _run_internal(self):
45 self._check_inputs()
46 if not self._confirm('Going to merge this to %s.' % self._branch):
47 return
48 self._create_checkout()
49 self._prepare_merge()
50 if self._dry_run:
51 logging.info('--dry_run enabled; not landing.')
52 return
53
54 self._run_git_command(['cl', 'upload'], error_message='Upload failed')
55
56 if not self._confirm('About to land on %s.' % self._branch):
57 return
58 self._run_git_command(['cl', 'land', '--bypass-hooks'])
59
60 def _cleanup(self):
61 if self._cherry_pick_in_progress:
62 self._run_git_command(['cherry-pick', '--abort'])
iannucci 2015/09/22 04:18:11 you could also just run this always and swallow th
Sam McNally 2015/09/23 01:16:07 Done.
63 if self._branch_name:
64 self._run_git_command(['checkout', '--detach'], quiet=True)
65 self._run_git_command(['branch', '-D', self._branch_name], quiet=True)
66 os.chdir(self._cwd)
iannucci 2015/09/22 04:18:11 I'd very strongly advise to avoid chdir'ing (excep
Sam McNally 2015/09/23 01:16:07 Done.
67 if self._workdir:
68 logging.debug('Deleting %s', self._workdir)
69 shutil.rmtree(self._workdir)
70 if self._dev_null_file:
71 self._dev_null_file.close()
72 self._dev_null_file = None
73
74 @staticmethod
75 def _confirm(message):
76 """Show a confirmation prompt with the given message.
77
78 Returns:
79 A bool representing whether the user wishes to continue.
80 """
81 result = ''
82 while result not in ('y', 'n'):
83 try:
84 result = raw_input('%s Continue (y/n)? ' % message)
85 except EOFError:
86 result = 'n'
87 return result == 'y'
88
89 def _check_inputs(self):
90 """Check the input arguments and ensure the parent repo is up to date."""
91
92 if not os.path.isdir(self._parent_repo):
93 raise Error('Invalid parent repo path %r' % self._parent_repo)
94 if not hasattr(os, 'symlink'):
95 raise Error('Symlink support is required')
96
97 os.chdir(self._parent_repo)
98 self._run_git_command(['--help'],
99 error_message='Unable to run git',
100 quiet=True)
101 self._run_git_command(['status'],
102 quiet=True,
103 error_message='%r is not a valid git repo' %
104 os.path.abspath(self._parent_repo))
105 self._run_git_command(['fetch', 'origin'],
106 error_message='Failed to fetch origin')
107 self._run_git_command(
108 ['rev-parse', 'branch-heads/%s' % self._branch],
iannucci 2015/09/22 04:18:11 let's use absolute refs (so: `refs/branch-heads/__
Sam McNally 2015/09/23 01:16:07 Done. I had to use refs/remotes/branch-heads/...
109 quiet=True,
110 error_message='Branch %s not found' % self._branch)
111 self._run_git_command(['rev-parse', self._cl],
112 quiet=True,
113 error_message='Revision "%s" not found' % self._cl)
114 self._run_git_command(['show', '-s', self._cl])
115
116 @staticmethod
117 def _process_files(source_dir, target_dir, filenames, operation):
118 """Performs a given operation on a list of files.
119
120 Args:
121 source_dir: A string containing the source directory.
122 source_dir: A string containing the target directory.
123 filenames: A list of strings containing the files within |source_dir| to
124 be processed.
125 operation: A binary function to be called for each file in |filenames|, in
126 the form operation(|source_dir|/file, |target_dir|/file).
127 """
128 for filename in filenames:
129 if not isinstance(filename, tuple):
130 filename = (filename,)
131 target = os.path.join(target_dir, *filename)
132 source = os.path.join(source_dir, *filename)
133 if not os.path.exists(os.path.dirname(target)):
134 os.makedirs(os.path.dirname(target))
135 logging.debug('Cloning %r to %r', source, target)
136 operation(source, target)
137
138 FILES_TO_LINK = [
139 'refs',
140 ('logs', 'refs'),
141 'objects',
142 ('info', 'refs'),
143 ('info', 'exclude'),
144 'hooks',
145 'packed-refs',
146 'remotes',
147 'rr-cache',
148 'svn',
149 ]
150 FILES_TO_COPY = ['config', 'HEAD']
151
152 def _create_checkout(self):
153 """Creates a checkout to use for cherry-picking.
154
155 This creates a checkout similarly to git-new-workdir. Most of the .git
156 directory is shared with the |self._parent_repo| using symlinks. This
157 differs from git-new-workdir in that the config is forked instead of shared.
158 This is so the new workdir can be a sparse checkout without affecting
159 |self._parent_repo|.
160 """
161 self._workdir = tempfile.mkdtemp(prefix='drover_%s_' % self._branch)
162 logging.debug('Creating checkout in %s', self._workdir)
163 os.chdir(self._workdir)
164 parent_git_dir = os.path.join(self._parent_repo, '.git')
iannucci 2015/09/22 04:18:11 you can use `git rev-parse --git-dir` to get this
Sam McNally 2015/09/23 01:16:07 Done.
165 git_dir = os.path.join(self._workdir, '.git')
166 os.mkdir(git_dir)
167 logging.debug('Creating symlinks')
168 self._process_files(parent_git_dir, git_dir, self.FILES_TO_LINK, os.symlink)
169 logging.debug('Creating copies')
170 self._process_files(parent_git_dir, git_dir, self.FILES_TO_COPY,
171 shutil.copy)
172 self._run_git_command(['config', 'core.sparsecheckout', 'true'])
173 with open(os.path.join(git_dir, 'info', 'sparse-checkout'), 'w') as f:
174 f.write('codereview.settings')
175
176 branch_name = os.path.split(self._workdir)[-1]
177 self._run_git_command(
178 ['checkout', '-b', branch_name, 'branch-heads/%s' % self._branch],
iannucci 2015/09/22 04:18:11 absolute refs
Sam McNally 2015/09/23 01:16:07 Done.
179 quiet=True)
180 self._branch_name = branch_name
iannucci 2015/09/22 04:18:11 Is there any way we can use the actual git workdir
Sam McNally 2015/09/23 01:16:07 Moved it from gclient-new-workdir.py. This has to
181
182 def _prepare_merge(self):
iannucci 2015/09/22 04:18:11 s/merge/cherry_pick/ everywhere (mentioned below)
Sam McNally 2015/09/23 01:16:07 Done.
183 self._cherry_pick_in_progress = True
184 self._run_git_command(['cherry-pick', '-x', self._cl],
185 quiet=True,
186 error_message='Patch failed to apply')
187 self._cherry_pick_in_progress = False
iannucci 2015/09/22 04:18:11 fwiw, git tracks this bit of information in a file
Sam McNally 2015/09/23 01:16:07 Removed this.
188 self._run_git_command(['reset', '--hard'], quiet=True)
189
190 def _run_git_command(self, args, quiet=False, error_message=None):
191 """Runs a git command.
192
193 Args:
194 args: A list of strings containing the args to pass to git.
195 quiet: A bool containing whether to redirect output to /dev/null.
196 error_message: A string containing the error message to report if the
197 command fails.
198
199 Raises:
200 Error: The command failed to complete successfully.
201 """
202 logging.debug('Running git %s', ' '.join('%s' % arg for arg in args))
203 output = None
204 if quiet:
205 if self._dev_null_file is None:
206 self._dev_null_file = open(os.devnull, 'w')
iannucci 2015/09/22 04:18:11 I'd probably do this in __init__, since it's essen
Sam McNally 2015/09/23 01:16:07 Done.
207 output = self._dev_null_file
208
209 try:
210 subprocess.check_call(
211 ['git'] + args,
212 stdout=output,
213 stderr=output,
214 shell=False)
215 except (OSError, subprocess.CalledProcessError) as e:
216 if error_message:
217 raise Error(error_message)
218 else:
219 raise Error('Command %r failed: %s' % (' '.join(args), e))
220
221
222 def merge_change(branch, cl, parent_repo, dry_run):
223 """Merges a change into a branch.
224
225 Args:
226 branch: A string containing the release branch number to which to merge.
iannucci 2015/09/22 04:18:11 Does it work with 'mini' branches?
Sam McNally 2015/09/23 01:16:07 I'm not sure what 'mini' branches are.
227 cl: A string containing the git hash of the patch to merge.
iannucci 2015/09/22 04:18:11 maybe 'revision'? Is it required to be the full g
Sam McNally 2015/09/23 01:16:07 Done.
228 parent_repo: A string containing the path to the parent repo to use for this
229 merge.
230 dry_run: A boolean containing whether to stop before uploading the merge cl.
231
232 Raises:
233 Error: An error occurred while attempting to merge |cl| to |branch|.
234 """
235 drover = _Drover(branch, cl, parent_repo, dry_run)
236 drover.run()
237
238
239 def main():
240 parser = argparse.ArgumentParser(
241 description='Merge a change into a release branch.')
242 parser.add_argument('--branch',
243 type=str,
244 required=True,
245 metavar='<branch>',
246 help='the name of the branch to which to merge')
iannucci 2015/09/22 04:18:11 full name or just the bit after `refs/branch-heads
Sam McNally 2015/09/23 01:16:07 Just the bit after.
247 parser.add_argument('--merge',
248 type=str,
249 required=True,
250 metavar='<change>',
251 help='the hash of the change to merge')
iannucci 2015/09/22 04:18:11 s/merge/cherry-pick/ since 'merge' means something
Sam McNally 2015/09/23 01:16:07 Done.
252 parser.add_argument(
253 '--parent_checkout',
254 type=str,
255 default=os.path.abspath('.'),
256 metavar='<path_to_parent_checkout>',
257 help=('the path to the chromium checkout to use; '
iannucci 2015/09/22 04:18:11 definitely want to explain what 'use' means here (
Sam McNally 2015/09/23 01:16:07 Done.
258 'if unspecified, the current directory is used'))
259 parser.add_argument('--dry-run',
260 action='store_true',
261 default=False,
262 help=("Don't actually upload and land. "
263 'Just check that merging would succeed'))
iannucci 2015/09/22 04:18:11 mixed ' and " is weird... I see why you did it, bu
Sam McNally 2015/09/23 01:16:07 Done.
264 parser.add_argument('-v',
265 '--verbose',
266 action='store_true',
267 default=False,
268 help='Show verbose logging')
269 options = parser.parse_args()
270 if options.verbose:
271 logging.getLogger().setLevel(logging.DEBUG)
272 try:
273 merge_change(options.branch, options.merge, options.parent_checkout,
274 options.dry_run)
275 except Error as e:
276 logging.error(e.message)
277 sys.exit(128)
278
279
280 if __name__ == '__main__':
281 main()
OLDNEW
« git-drover ('K') | « git-drover ('k') | tests/git_drover_test.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698