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

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, 3 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
« no previous file with comments | « git_common.py ('k') | man/html/git-drover.html » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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 functools
9 import logging
10 import os
11 import shutil
12 import subprocess
13 import sys
14 import tempfile
15
16 import git_common
17
18
19 class Error(Exception):
20 pass
21
22
23 class _Drover(object):
24
25 def __init__(self, branch, revision, parent_repo, dry_run):
26 self._branch = branch
27 self._branch_ref = 'refs/remotes/branch-heads/%s' % branch
28 self._revision = revision
29 self._parent_repo = os.path.abspath(parent_repo)
30 self._dry_run = dry_run
31 self._workdir = None
32 self._branch_name = None
33 self._dev_null_file = open(os.devnull, 'w')
34
35 def run(self):
36 """Runs this Drover instance.
37
38 Raises:
39 Error: An error occurred while attempting to cherry-pick this change.
40 """
41 try:
42 self._run_internal()
43 finally:
44 self._cleanup()
45
46 def _run_internal(self):
47 self._check_inputs()
48 if not self._confirm('Going to cherry-pick\n"""\n%s"""\nto %s.' % (
49 self._run_git_command(['show', '-s', self._revision]), self._branch)):
50 return
51 self._create_checkout()
52 self._prepare_cherry_pick()
53 if self._dry_run:
54 logging.info('--dry_run enabled; not landing.')
55 return
56
57 self._run_git_command(['cl', 'upload'],
58 error_message='Upload failed',
59 interactive=True)
60
61 if not self._confirm('About to land on %s.' % self._branch):
62 return
63 self._run_git_command(['cl', 'land', '--bypass-hooks'], interactive=True)
64
65 def _cleanup(self):
66 if self._branch_name:
67 try:
68 self._run_git_command(['cherry-pick', '--abort'])
69 except Error:
70 pass
71 self._run_git_command(['checkout', '--detach'])
72 self._run_git_command(['branch', '-D', self._branch_name])
73 if self._workdir:
74 logging.debug('Deleting %s', self._workdir)
75 shutil.rmtree(self._workdir)
76 self._dev_null_file.close()
77
78 @staticmethod
79 def _confirm(message):
80 """Show a confirmation prompt with the given message.
81
82 Returns:
83 A bool representing whether the user wishes to continue.
84 """
85 result = ''
86 while result not in ('y', 'n'):
87 try:
88 result = raw_input('%s Continue (y/n)? ' % message)
89 except EOFError:
90 result = 'n'
91 return result == 'y'
92
93 def _check_inputs(self):
94 """Check the input arguments and ensure the parent repo is up to date."""
95
96 if not os.path.isdir(self._parent_repo):
97 raise Error('Invalid parent repo path %r' % self._parent_repo)
98 if not hasattr(os, 'symlink'):
99 raise Error('Symlink support is required')
100
101 self._run_git_command(['--help'], error_message='Unable to run git')
102 self._run_git_command(['status'],
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', '%s^{commit}' % self._branch_ref],
109 error_message='Branch %s not found' % self._branch_ref)
110 self._run_git_command(
111 ['rev-parse', '%s^{commit}' % self._revision],
112 error_message='Revision "%s" not found' % self._revision)
113
114 FILES_TO_LINK = [
115 'refs',
116 'logs/refs',
117 'info/refs',
118 'info/exclude',
119 'objects',
120 'hooks',
121 'packed-refs',
122 'remotes',
123 'rr-cache',
124 'svn',
125 ]
126 FILES_TO_COPY = ['config', 'HEAD']
127
128 def _create_checkout(self):
129 """Creates a checkout to use for cherry-picking.
130
131 This creates a checkout similarly to git-new-workdir. Most of the .git
132 directory is shared with the |self._parent_repo| using symlinks. This
133 differs from git-new-workdir in that the config is forked instead of shared.
134 This is so the new workdir can be a sparse checkout without affecting
135 |self._parent_repo|.
136 """
137 parent_git_dir = os.path.abspath(self._run_git_command(
138 ['rev-parse', '--git-dir']).strip())
139 self._workdir = tempfile.mkdtemp(prefix='drover_%s_' % self._branch)
140 logging.debug('Creating checkout in %s', self._workdir)
141 git_dir = os.path.join(self._workdir, '.git')
142 git_common.make_workdir_common(parent_git_dir, git_dir, self.FILES_TO_LINK,
143 self.FILES_TO_COPY)
144 self._run_git_command(['config', 'core.sparsecheckout', 'true'])
145 with open(os.path.join(git_dir, 'info', 'sparse-checkout'), 'w') as f:
146 f.write('codereview.settings')
147
148 branch_name = os.path.split(self._workdir)[-1]
149 self._run_git_command(['checkout', '-b', branch_name, self._branch_ref])
150 self._branch_name = branch_name
151
152 def _prepare_cherry_pick(self):
153 self._run_git_command(['cherry-pick', '-x', self._revision],
154 error_message='Patch failed to apply')
155 self._run_git_command(['reset', '--hard'])
156
157 def _run_git_command(self, args, error_message=None, interactive=False):
158 """Runs a git command.
159
160 Args:
161 args: A list of strings containing the args to pass to git.
162 interactive:
163 error_message: A string containing the error message to report if the
164 command fails.
165
166 Raises:
167 Error: The command failed to complete successfully.
168 """
169 cwd = self._workdir if self._workdir else self._parent_repo
170 logging.debug('Running git %s (cwd %r)', ' '.join('%s' % arg
171 for arg in args), cwd)
172
173 run = subprocess.check_call if interactive else subprocess.check_output
174
175 try:
176 return run(['git'] + args,
177 shell=False,
178 cwd=cwd,
179 stderr=self._dev_null_file)
180 except (OSError, subprocess.CalledProcessError) as e:
181 if error_message:
182 raise Error(error_message)
183 else:
184 raise Error('Command %r failed: %s' % (' '.join(args), e))
185
186
187 def cherry_pick_change(branch, revision, parent_repo, dry_run):
188 """Cherry-picks a change into a branch.
189
190 Args:
191 branch: A string containing the release branch number to which to
192 cherry-pick.
193 revision: A string containing the revision to cherry-pick. It can be any
194 string that git-rev-parse can identify as referring to a single
195 revision.
196 parent_repo: A string containing the path to the parent repo to use for this
197 cherry-pick.
198 dry_run: A boolean containing whether to stop before uploading the
199 cherry-pick cl.
200
201 Raises:
202 Error: An error occurred while attempting to cherry-pick |cl| to |branch|.
203 """
204 drover = _Drover(branch, revision, parent_repo, dry_run)
205 drover.run()
206
207
208 def main():
209 parser = argparse.ArgumentParser(
210 description='Cherry-pick a change into a release branch.')
211 parser.add_argument(
212 '--branch',
213 type=str,
214 required=True,
215 metavar='<branch>',
216 help='the name of the branch to which to cherry-pick; e.g. 1234')
217 parser.add_argument('--cherry-pick',
218 type=str,
219 required=True,
220 metavar='<change>',
221 help=('the change to cherry-pick; this can be any string '
222 'that unambiguously refers to a revision'))
223 parser.add_argument(
224 '--parent_checkout',
225 type=str,
226 default=os.path.abspath('.'),
227 metavar='<path_to_parent_checkout>',
228 help=('the path to the chromium checkout to use as the source for a '
229 'creating git-new-workdir workdir to use for cherry-picking; '
230 'if unspecified, the current directory is used'))
231 parser.add_argument(
232 '--dry-run',
233 action='store_true',
234 default=False,
235 help=("don't actually upload and land; "
236 "just check that cherry-picking would succeed"))
237 parser.add_argument('-v',
238 '--verbose',
239 action='store_true',
240 default=False,
241 help='show verbose logging')
242 options = parser.parse_args()
243 if options.verbose:
244 logging.getLogger().setLevel(logging.DEBUG)
245 try:
246 cherry_pick_change(options.branch, options.cherry_pick,
247 options.parent_checkout, options.dry_run)
248 except Error as e:
249 logging.error(e.message)
250 sys.exit(128)
251
252
253 if __name__ == '__main__':
254 main()
OLDNEW
« no previous file with comments | « git_common.py ('k') | man/html/git-drover.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698