blob: 7fa0c2d31f001401abf512dcf96eae42436de1a1 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000010import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000011import logging
12import optparse
13import os
14import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000015import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import textwrap
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000018import urlparse
ukai@chromium.org78c4b982012-02-14 02:20:26 +000019import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import urllib2
21
22try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000023 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024except ImportError:
25 pass
26
maruel@chromium.org2a74d372011-03-29 19:05:50 +000027
28from third_party import upload
29import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000030import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000031import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000032import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000033import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000034import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000035import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000036import watchlists
37
38
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000039DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000040POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000041DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000042GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000043
maruel@chromium.org90541732011-04-01 17:54:18 +000044
maruel@chromium.orgddd59412011-11-30 14:20:38 +000045# Initialized in main()
46settings = None
47
48
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000049def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000050 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000051 sys.exit(1)
52
53
maruel@chromium.orgca2b8e72012-05-24 20:13:24 +000054def QuoteCommand(command):
55 """Quotes command on Windows so it runs fine even with & and | in the string.
56 """
57 if sys.platform == 'win32':
58 def fix(arg):
59 if ('&' in arg or '|' in arg) and '"' not in arg:
60 arg = '"%s"' % arg
61 return arg
62 command = [fix(arg) for arg in command]
63 return command
64
65
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067 try:
maruel@chromium.orgca2b8e72012-05-24 20:13:24 +000068 return subprocess2.check_output(QuoteCommand(args), **kwargs)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000069 except subprocess2.CalledProcessError, e:
70 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000071 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000072 'Command "%s" failed.\n%s' % (
73 ' '.join(args), error_message or e.stdout or ''))
74 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000075
76
77def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000078 """Returns stdout."""
79 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000080
81
82def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000083 """Returns return code and stdout."""
84 out, code = subprocess2.communicate(['git'] + args, stdout=subprocess2.PIPE)
85 return code, out[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086
87
88def usage(more):
89 def hook(fn):
90 fn.usage_more = more
91 return fn
92 return hook
93
94
maruel@chromium.org90541732011-04-01 17:54:18 +000095def ask_for_data(prompt):
96 try:
97 return raw_input(prompt)
98 except KeyboardInterrupt:
99 # Hide the exception.
100 sys.exit(1)
101
102
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000103def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
104 """Return the corresponding git ref if |base_url| together with |glob_spec|
105 matches the full |url|.
106
107 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
108 """
109 fetch_suburl, as_ref = glob_spec.split(':')
110 if allow_wildcards:
111 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
112 if glob_match:
113 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
114 # "branches/{472,597,648}/src:refs/remotes/svn/*".
115 branch_re = re.escape(base_url)
116 if glob_match.group(1):
117 branch_re += '/' + re.escape(glob_match.group(1))
118 wildcard = glob_match.group(2)
119 if wildcard == '*':
120 branch_re += '([^/]*)'
121 else:
122 # Escape and replace surrounding braces with parentheses and commas
123 # with pipe symbols.
124 wildcard = re.escape(wildcard)
125 wildcard = re.sub('^\\\\{', '(', wildcard)
126 wildcard = re.sub('\\\\,', '|', wildcard)
127 wildcard = re.sub('\\\\}$', ')', wildcard)
128 branch_re += wildcard
129 if glob_match.group(3):
130 branch_re += re.escape(glob_match.group(3))
131 match = re.match(branch_re, url)
132 if match:
133 return re.sub('\*$', match.group(1), as_ref)
134
135 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
136 if fetch_suburl:
137 full_url = base_url + '/' + fetch_suburl
138 else:
139 full_url = base_url
140 if full_url == url:
141 return as_ref
142 return None
143
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000144
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000145class Settings(object):
146 def __init__(self):
147 self.default_server = None
148 self.cc = None
149 self.root = None
150 self.is_git_svn = None
151 self.svn_branch = None
152 self.tree_status_url = None
153 self.viewvc_url = None
154 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000155 self.is_gerrit = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000156
157 def LazyUpdateIfNeeded(self):
158 """Updates the settings from a codereview.settings file, if available."""
159 if not self.updated:
160 cr_settings_file = FindCodereviewSettingsFile()
161 if cr_settings_file:
162 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000163 self.updated = True
164 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000165 self.updated = True
166
167 def GetDefaultServerUrl(self, error_ok=False):
168 if not self.default_server:
169 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000170 self.default_server = gclient_utils.UpgradeToHttps(
171 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000172 if error_ok:
173 return self.default_server
174 if not self.default_server:
175 error_message = ('Could not find settings file. You must configure '
176 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000177 self.default_server = gclient_utils.UpgradeToHttps(
178 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000179 return self.default_server
180
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000181 def GetRoot(self):
182 if not self.root:
183 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
184 return self.root
185
186 def GetIsGitSvn(self):
187 """Return true if this repo looks like it's using git-svn."""
188 if self.is_git_svn is None:
189 # If you have any "svn-remote.*" config keys, we think you're using svn.
190 self.is_git_svn = RunGitWithCode(
191 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
192 return self.is_git_svn
193
194 def GetSVNBranch(self):
195 if self.svn_branch is None:
196 if not self.GetIsGitSvn():
197 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
198
199 # Try to figure out which remote branch we're based on.
200 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000201 # 1) iterate through our branch history and find the svn URL.
202 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000203
204 # regexp matching the git-svn line that contains the URL.
205 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
206
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000207 # We don't want to go through all of history, so read a line from the
208 # pipe at a time.
209 # The -100 is an arbitrary limit so we don't search forever.
210 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000211 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000212 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000213 for line in proc.stdout:
214 match = git_svn_re.match(line)
215 if match:
216 url = match.group(1)
217 proc.stdout.close() # Cut pipe.
218 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000219
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000220 if url:
221 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
222 remotes = RunGit(['config', '--get-regexp',
223 r'^svn-remote\..*\.url']).splitlines()
224 for remote in remotes:
225 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000226 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000227 remote = match.group(1)
228 base_url = match.group(2)
229 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000230 ['config', 'svn-remote.%s.fetch' % remote],
231 error_ok=True).strip()
232 if fetch_spec:
233 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
234 if self.svn_branch:
235 break
236 branch_spec = RunGit(
237 ['config', 'svn-remote.%s.branches' % remote],
238 error_ok=True).strip()
239 if branch_spec:
240 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
241 if self.svn_branch:
242 break
243 tag_spec = RunGit(
244 ['config', 'svn-remote.%s.tags' % remote],
245 error_ok=True).strip()
246 if tag_spec:
247 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
248 if self.svn_branch:
249 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000250
251 if not self.svn_branch:
252 DieWithError('Can\'t guess svn branch -- try specifying it on the '
253 'command line')
254
255 return self.svn_branch
256
257 def GetTreeStatusUrl(self, error_ok=False):
258 if not self.tree_status_url:
259 error_message = ('You must configure your tree status URL by running '
260 '"git cl config".')
261 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
262 error_ok=error_ok,
263 error_message=error_message)
264 return self.tree_status_url
265
266 def GetViewVCUrl(self):
267 if not self.viewvc_url:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000268 self.viewvc_url = gclient_utils.UpgradeToHttps(
269 self._GetConfig('rietveld.viewvc-url', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000270 return self.viewvc_url
271
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000272 def GetDefaultCCList(self):
273 return self._GetConfig('rietveld.cc', error_ok=True)
274
ukai@chromium.orge8077812012-02-03 03:41:46 +0000275 def GetIsGerrit(self):
276 """Return true if this repo is assosiated with gerrit code review system."""
277 if self.is_gerrit is None:
278 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
279 return self.is_gerrit
280
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000281 def _GetConfig(self, param, **kwargs):
282 self.LazyUpdateIfNeeded()
283 return RunGit(['config', param], **kwargs).strip()
284
285
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000286def ShortBranchName(branch):
287 """Convert a name like 'refs/heads/foo' to just 'foo'."""
288 return branch.replace('refs/heads/', '')
289
290
291class Changelist(object):
292 def __init__(self, branchref=None):
293 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000294 global settings
295 if not settings:
296 # Happens when git_cl.py is used as a utility library.
297 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000298 settings.GetDefaultServerUrl()
299 self.branchref = branchref
300 if self.branchref:
301 self.branch = ShortBranchName(self.branchref)
302 else:
303 self.branch = None
304 self.rietveld_server = None
305 self.upstream_branch = None
306 self.has_issue = False
307 self.issue = None
308 self.has_description = False
309 self.description = None
310 self.has_patchset = False
311 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000312 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000313 self.cc = None
314 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000315 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000316
317 def GetCCList(self):
318 """Return the users cc'd on this CL.
319
320 Return is a string suitable for passing to gcl with the --cc flag.
321 """
322 if self.cc is None:
323 base_cc = settings .GetDefaultCCList()
324 more_cc = ','.join(self.watchers)
325 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
326 return self.cc
327
328 def SetWatchers(self, watchers):
329 """Set the list of email addresses that should be cc'd based on the changed
330 files in this CL.
331 """
332 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000333
334 def GetBranch(self):
335 """Returns the short branch name, e.g. 'master'."""
336 if not self.branch:
337 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
338 self.branch = ShortBranchName(self.branchref)
339 return self.branch
340
341 def GetBranchRef(self):
342 """Returns the full branch name, e.g. 'refs/heads/master'."""
343 self.GetBranch() # Poke the lazy loader.
344 return self.branchref
345
346 def FetchUpstreamTuple(self):
347 """Returns a tuple containg remote and remote ref,
348 e.g. 'origin', 'refs/heads/master'
349 """
350 remote = '.'
351 branch = self.GetBranch()
352 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
353 error_ok=True).strip()
354 if upstream_branch:
355 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
356 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000357 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
358 error_ok=True).strip()
359 if upstream_branch:
360 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000361 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000362 # Fall back on trying a git-svn upstream branch.
363 if settings.GetIsGitSvn():
364 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000365 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000366 # Else, try to guess the origin remote.
367 remote_branches = RunGit(['branch', '-r']).split()
368 if 'origin/master' in remote_branches:
369 # Fall back on origin/master if it exits.
370 remote = 'origin'
371 upstream_branch = 'refs/heads/master'
372 elif 'origin/trunk' in remote_branches:
373 # Fall back on origin/trunk if it exists. Generally a shared
374 # git-svn clone
375 remote = 'origin'
376 upstream_branch = 'refs/heads/trunk'
377 else:
378 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000379Either pass complete "git diff"-style arguments, like
380 git cl upload origin/master
381or verify this branch is set up to track another (via the --track argument to
382"git checkout -b ...").""")
383
384 return remote, upstream_branch
385
386 def GetUpstreamBranch(self):
387 if self.upstream_branch is None:
388 remote, upstream_branch = self.FetchUpstreamTuple()
389 if remote is not '.':
390 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
391 self.upstream_branch = upstream_branch
392 return self.upstream_branch
393
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000394 def GetRemote(self):
395 if not self._remote:
396 self._remote = self.FetchUpstreamTuple()[0]
397 if self._remote == '.':
398
399 remotes = RunGit(['remote'], error_ok=True).split()
400 if len(remotes) == 1:
401 self._remote, = remotes
402 elif 'origin' in remotes:
403 self._remote = 'origin'
404 logging.warning('Could not determine which remote this change is '
405 'associated with, so defaulting to "%s". This may '
406 'not be what you want. You may prevent this message '
407 'by running "git svn info" as documented here: %s',
408 self._remote,
409 GIT_INSTRUCTIONS_URL)
410 else:
411 logging.warn('Could not determine which remote this change is '
412 'associated with. You may prevent this message by '
413 'running "git svn info" as documented here: %s',
414 GIT_INSTRUCTIONS_URL)
415 return self._remote
416
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000417 def GetGitBaseUrlFromConfig(self):
418 """Return the configured base URL from branch.<branchname>.baseurl.
419
420 Returns None if it is not set.
421 """
422 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
423 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000424
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000425 def GetRemoteUrl(self):
426 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
427
428 Returns None if there is no remote.
429 """
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000430 remote = self.GetRemote()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000431 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
432
433 def GetIssue(self):
434 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000435 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
436 if issue:
437 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000438 else:
439 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000440 self.has_issue = True
441 return self.issue
442
443 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000444 if not self.rietveld_server:
445 # If we're on a branch then get the server potentially associated
446 # with that branch.
447 if self.GetIssue():
448 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
449 ['config', self._RietveldServer()], error_ok=True).strip())
450 if not self.rietveld_server:
451 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000452 return self.rietveld_server
453
454 def GetIssueURL(self):
455 """Get the URL for a particular issue."""
456 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
457
458 def GetDescription(self, pretty=False):
459 if not self.has_description:
460 if self.GetIssue():
miket@chromium.org183df1a2012-01-04 19:44:55 +0000461 issue = int(self.GetIssue())
462 try:
463 self.description = self.RpcServer().get_description(issue).strip()
464 except urllib2.HTTPError, e:
465 if e.code == 404:
466 DieWithError(
467 ('\nWhile fetching the description for issue %d, received a '
468 '404 (not found)\n'
469 'error. It is likely that you deleted this '
470 'issue on the server. If this is the\n'
471 'case, please run\n\n'
472 ' git cl issue 0\n\n'
473 'to clear the association with the deleted issue. Then run '
474 'this command again.') % issue)
475 else:
476 DieWithError(
477 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000478 self.has_description = True
479 if pretty:
480 wrapper = textwrap.TextWrapper()
481 wrapper.initial_indent = wrapper.subsequent_indent = ' '
482 return wrapper.fill(self.description)
483 return self.description
484
485 def GetPatchset(self):
486 if not self.has_patchset:
487 patchset = RunGit(['config', self._PatchsetSetting()],
488 error_ok=True).strip()
489 if patchset:
490 self.patchset = patchset
491 else:
492 self.patchset = None
493 self.has_patchset = True
494 return self.patchset
495
496 def SetPatchset(self, patchset):
497 """Set this branch's patchset. If patchset=0, clears the patchset."""
498 if patchset:
499 RunGit(['config', self._PatchsetSetting(), str(patchset)])
500 else:
501 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000502 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000503 self.has_patchset = False
504
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000505 def GetPatchSetDiff(self, issue):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000506 patchset = self.RpcServer().get_issue_properties(
507 int(issue), False)['patchsets'][-1]
508 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000509 '/download/issue%s_%s.diff' % (issue, patchset))
510
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000511 def SetIssue(self, issue):
512 """Set this branch's issue. If issue=0, clears the issue."""
513 if issue:
514 RunGit(['config', self._IssueSetting(), str(issue)])
515 if self.rietveld_server:
516 RunGit(['config', self._RietveldServer(), self.rietveld_server])
517 else:
518 RunGit(['config', '--unset', self._IssueSetting()])
519 self.SetPatchset(0)
520 self.has_issue = False
521
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000522 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000523 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
524 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000525
526 # We use the sha1 of HEAD as a name of this change.
527 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000528 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000529 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000530 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000531 except subprocess2.CalledProcessError:
532 DieWithError(
533 ('\nFailed to diff against upstream branch %s!\n\n'
534 'This branch probably doesn\'t exist anymore. To reset the\n'
535 'tracking branch, please run\n'
536 ' git branch --set-upstream %s trunk\n'
537 'replacing trunk with origin/master or the relevant branch') %
538 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000539
540 issue = ConvertToInteger(self.GetIssue())
541 patchset = ConvertToInteger(self.GetPatchset())
542 if issue:
543 description = self.GetDescription()
544 else:
545 # If the change was never uploaded, use the log messages of all commits
546 # up to the branch point, as git cl upload will prefill the description
547 # with these log messages.
maruel@chromium.orgca2b8e72012-05-24 20:13:24 +0000548 description = CreateDescriptionFromLog([upstream_branch + '..'])
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000549
550 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000551 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000552 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000553 name,
554 description,
555 absroot,
556 files,
557 issue,
558 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000559 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000560
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000561 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
562 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
563 change = self.GetChange(upstream_branch, author)
564
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000565 # Apply watchlists on upload.
566 if not committing:
567 watchlist = watchlists.Watchlists(change.RepositoryRoot())
568 files = [f.LocalPath() for f in change.AffectedFiles()]
569 self.SetWatchers(watchlist.GetWatchersForPaths(files))
570
571 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000572 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000573 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000574 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000575 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000576 except presubmit_support.PresubmitFailure, e:
577 DieWithError(
578 ('%s\nMaybe your depot_tools is out of date?\n'
579 'If all fails, contact maruel@') % e)
580
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000581 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000582 """Updates the description and closes the issue."""
583 issue = int(self.GetIssue())
584 self.RpcServer().update_description(issue, self.description)
585 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000586
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000587 def SetFlag(self, flag, value):
588 """Patchset must match."""
589 if not self.GetPatchset():
590 DieWithError('The patchset needs to match. Send another patchset.')
591 try:
592 return self.RpcServer().set_flag(
593 int(self.GetIssue()), int(self.GetPatchset()), flag, value)
594 except urllib2.HTTPError, e:
595 if e.code == 404:
596 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
597 if e.code == 403:
598 DieWithError(
599 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
600 'match?') % (self.GetIssue(), self.GetPatchset()))
601 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000602
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000603 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000604 """Returns an upload.RpcServer() to access this review's rietveld instance.
605 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000606 if not self._rpc_server:
evan@chromium.org0af9b702012-02-11 00:42:16 +0000607 self._rpc_server = rietveld.Rietveld(self.GetRietveldServer(),
608 None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000609 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000610
611 def _IssueSetting(self):
612 """Return the git setting that stores this change's issue."""
613 return 'branch.%s.rietveldissue' % self.GetBranch()
614
615 def _PatchsetSetting(self):
616 """Return the git setting that stores this change's most recent patchset."""
617 return 'branch.%s.rietveldpatchset' % self.GetBranch()
618
619 def _RietveldServer(self):
620 """Returns the git setting that stores this change's rietveld server."""
621 return 'branch.%s.rietveldserver' % self.GetBranch()
622
623
624def GetCodereviewSettingsInteractively():
625 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000626 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000627 server = settings.GetDefaultServerUrl(error_ok=True)
628 prompt = 'Rietveld server (host[:port])'
629 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000630 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000631 if not server and not newserver:
632 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000633 if newserver:
634 newserver = gclient_utils.UpgradeToHttps(newserver)
635 if newserver != server:
636 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000637
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000638 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000639 prompt = caption
640 if initial:
641 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000642 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000643 if new_val == 'x':
644 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000645 elif new_val:
646 if is_url:
647 new_val = gclient_utils.UpgradeToHttps(new_val)
648 if new_val != initial:
649 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000650
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000651 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000652 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000653 'tree-status-url', False)
654 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000655
656 # TODO: configure a default branch to diff against, rather than this
657 # svn-based hackery.
658
659
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000660class ChangeDescription(object):
661 """Contains a parsed form of the change description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000662 def __init__(self, log_desc, reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000663 self.log_desc = log_desc
664 self.reviewers = reviewers
665 self.description = self.log_desc
666
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000667 def Prompt(self):
668 content = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000669# This will displayed on the codereview site.
670# The first line will also be used as the subject of the review.
671"""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000672 content += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000673 if ('\nR=' not in self.description and
674 '\nTBR=' not in self.description and
675 self.reviewers):
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000676 content += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000677 if '\nBUG=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000678 content += '\nBUG='
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000679 if '\nTEST=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000680 content += '\nTEST='
681 content = content.rstrip('\n') + '\n'
682 content = gclient_utils.RunEditor(content, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000683 if not content:
684 DieWithError('Running editor failed')
685 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000686 if not content.strip():
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000687 DieWithError('No CL description, aborting')
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000688 self.description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000689
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000690 def ParseDescription(self):
jam@chromium.org31083642012-01-27 03:14:45 +0000691 """Updates the list of reviewers and subject from the description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000692 self.description = self.description.strip('\n') + '\n'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000693 # Retrieves all reviewer lines
694 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000695 reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000696 i.group(2).strip() for i in regexp.finditer(self.description))
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000697 if reviewers:
698 self.reviewers = reviewers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000699
700 def IsEmpty(self):
701 return not self.description
702
703
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000704def FindCodereviewSettingsFile(filename='codereview.settings'):
705 """Finds the given file starting in the cwd and going up.
706
707 Only looks up to the top of the repository unless an
708 'inherit-review-settings-ok' file exists in the root of the repository.
709 """
710 inherit_ok_file = 'inherit-review-settings-ok'
711 cwd = os.getcwd()
712 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
713 if os.path.isfile(os.path.join(root, inherit_ok_file)):
714 root = '/'
715 while True:
716 if filename in os.listdir(cwd):
717 if os.path.isfile(os.path.join(cwd, filename)):
718 return open(os.path.join(cwd, filename))
719 if cwd == root:
720 break
721 cwd = os.path.dirname(cwd)
722
723
724def LoadCodereviewSettingsFromFile(fileobj):
725 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000726 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000727
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000728 def SetProperty(name, setting, unset_error_ok=False):
729 fullname = 'rietveld.' + name
730 if setting in keyvals:
731 RunGit(['config', fullname, keyvals[setting]])
732 else:
733 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
734
735 SetProperty('server', 'CODE_REVIEW_SERVER')
736 # Only server setting is required. Other settings can be absent.
737 # In that case, we ignore errors raised during option deletion attempt.
738 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
739 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
740 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
741
ukai@chromium.orge8077812012-02-03 03:41:46 +0000742 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
743 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
744 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000745
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000746 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
747 #should be of the form
748 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
749 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
750 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
751 keyvals['ORIGIN_URL_CONFIG']])
752
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000753
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000754def DownloadHooks(force):
755 """downloads hooks
756
757 Args:
758 force: True to update hooks. False to install hooks if not present.
759 """
760 if not settings.GetIsGerrit():
761 return
762 server_url = settings.GetDefaultServerUrl()
763 src = '%s/tools/hooks/commit-msg' % server_url
764 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
765 if not os.access(dst, os.X_OK):
766 if os.path.exists(dst):
767 if not force:
768 return
769 os.remove(dst)
770 try:
771 urllib.urlretrieve(src, dst)
772 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
773 except Exception:
774 if os.path.exists(dst):
775 os.remove(dst)
776 DieWithError('\nFailed to download hooks from %s' % src)
777
778
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000779@usage('[repo root containing codereview.settings]')
780def CMDconfig(parser, args):
781 """edit configuration for this tree"""
782
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000783 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000784 if len(args) == 0:
785 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000786 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000787 return 0
788
789 url = args[0]
790 if not url.endswith('codereview.settings'):
791 url = os.path.join(url, 'codereview.settings')
792
793 # Load code review settings and download hooks (if available).
794 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000795 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000796 return 0
797
798
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000799def CMDbaseurl(parser, args):
800 """get or set base-url for this branch"""
801 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
802 branch = ShortBranchName(branchref)
803 _, args = parser.parse_args(args)
804 if not args:
805 print("Current base-url:")
806 return RunGit(['config', 'branch.%s.base-url' % branch],
807 error_ok=False).strip()
808 else:
809 print("Setting base-url to %s" % args[0])
810 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
811 error_ok=False).strip()
812
813
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814def CMDstatus(parser, args):
815 """show status of changelists"""
816 parser.add_option('--field',
817 help='print only specific field (desc|id|patch|url)')
818 (options, args) = parser.parse_args(args)
819
820 # TODO: maybe make show_branches a flag if necessary.
821 show_branches = not options.field
822
823 if show_branches:
824 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
825 if branches:
826 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +0000827 changes = (Changelist(branchref=b) for b in branches.splitlines())
828 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
829 alignment = max(5, max(len(b) for b in branches))
830 for branch in sorted(branches):
831 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000832
833 cl = Changelist()
834 if options.field:
835 if options.field.startswith('desc'):
836 print cl.GetDescription()
837 elif options.field == 'id':
838 issueid = cl.GetIssue()
839 if issueid:
840 print issueid
841 elif options.field == 'patch':
842 patchset = cl.GetPatchset()
843 if patchset:
844 print patchset
845 elif options.field == 'url':
846 url = cl.GetIssueURL()
847 if url:
848 print url
849 else:
850 print
851 print 'Current branch:',
852 if not cl.GetIssue():
853 print 'no issue assigned.'
854 return 0
855 print cl.GetBranch()
856 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
857 print 'Issue description:'
858 print cl.GetDescription(pretty=True)
859 return 0
860
861
862@usage('[issue_number]')
863def CMDissue(parser, args):
864 """Set or display the current code review issue number.
865
866 Pass issue number 0 to clear the current issue.
867"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000868 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000869
870 cl = Changelist()
871 if len(args) > 0:
872 try:
873 issue = int(args[0])
874 except ValueError:
875 DieWithError('Pass a number to set the issue or none to list it.\n'
876 'Maybe you want to run git cl status?')
877 cl.SetIssue(issue)
878 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
879 return 0
880
881
882def CreateDescriptionFromLog(args):
883 """Pulls out the commit log to use as a base for the CL description."""
884 log_args = []
885 if len(args) == 1 and not args[0].endswith('.'):
886 log_args = [args[0] + '..']
887 elif len(args) == 1 and args[0].endswith('...'):
888 log_args = [args[0][:-1]]
889 elif len(args) == 2:
890 log_args = [args[0] + '..' + args[1]]
891 else:
892 log_args = args[:] # Hope for the best!
maruel@chromium.orgca2b8e72012-05-24 20:13:24 +0000893 return RunGit(['log', '--pretty=format:%s%n%n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000894
895
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000896def ConvertToInteger(inputval):
897 """Convert a string to integer, but returns either an int or None."""
898 try:
899 return int(inputval)
900 except (TypeError, ValueError):
901 return None
902
903
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000904def CMDpresubmit(parser, args):
905 """run presubmit tests on the current changelist"""
906 parser.add_option('--upload', action='store_true',
907 help='Run upload hook instead of the push/dcommit hook')
908 (options, args) = parser.parse_args(args)
909
910 # Make sure index is up-to-date before running diff-index.
911 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
912 if RunGit(['diff-index', 'HEAD']):
913 # TODO(maruel): Is this really necessary?
914 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
915 return 1
916
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000917 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000918 if args:
919 base_branch = args[0]
920 else:
921 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000922 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000923
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000924 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000925 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000926 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000927 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000928
929
ukai@chromium.orge8077812012-02-03 03:41:46 +0000930def GerritUpload(options, args, cl):
931 """upload the current branch to gerrit."""
932 # We assume the remote called "origin" is the one we want.
933 # It is probably not worthwhile to support different workflows.
934 remote = 'origin'
935 branch = 'master'
936 if options.target_branch:
937 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000939 log_desc = options.message or CreateDescriptionFromLog(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +0000940 if options.reviewers:
941 log_desc += '\nR=' + options.reviewers
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000942 change_desc = ChangeDescription(log_desc, options.reviewers)
943 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +0000944 if change_desc.IsEmpty():
945 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000946 return 1
947
ukai@chromium.orge8077812012-02-03 03:41:46 +0000948 receive_options = []
949 cc = cl.GetCCList().split(',')
950 if options.cc:
951 cc += options.cc.split(',')
952 cc = filter(None, cc)
953 if cc:
954 receive_options += ['--cc=' + email for email in cc]
955 if change_desc.reviewers:
956 reviewers = filter(None, change_desc.reviewers.split(','))
957 if reviewers:
958 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000959
ukai@chromium.orge8077812012-02-03 03:41:46 +0000960 git_command = ['push']
961 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +0000962 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +0000963 ' '.join(receive_options))
964 git_command += [remote, 'HEAD:refs/for/' + branch]
965 RunGit(git_command)
966 # TODO(ukai): parse Change-Id: and set issue number?
967 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000968
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000969
ukai@chromium.orge8077812012-02-03 03:41:46 +0000970def RietveldUpload(options, args, cl):
971 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000972 upload_args = ['--assume_yes'] # Don't ask about untracked files.
973 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000974 if options.emulate_svn_auto_props:
975 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000976
977 change_desc = None
978
979 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +0000980 if options.title:
981 upload_args.extend(['--title', options.title])
982 elif options.message:
983 # TODO(rogerta): for now, the -m option will also set the --title option
984 # for upload.py. Soon this will be changed to set the --message option.
985 # Will wait until people are used to typing -t instead of -m.
986 upload_args.extend(['--title', options.message])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987 upload_args.extend(['--issue', cl.GetIssue()])
988 print ("This branch is associated with issue %s. "
989 "Adding patch to that issue." % cl.GetIssue())
990 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +0000991 if options.title:
992 upload_args.extend(['--title', options.title])
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000993 message = options.message or CreateDescriptionFromLog(args)
994 change_desc = ChangeDescription(message, options.reviewers)
995 if not options.force:
996 change_desc.Prompt()
997 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000998
999 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001000 print "Description is empty; aborting."
1001 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001002
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001003 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001004 if change_desc.reviewers:
1005 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +00001006 if options.send_mail:
1007 if not change_desc.reviewers:
1008 DieWithError("Must specify reviewers to send email.")
1009 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001010 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001011 if cc:
1012 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001013
1014 # Include the upstream repo's URL in the change -- this is useful for
1015 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001016 remote_url = cl.GetGitBaseUrlFromConfig()
1017 if not remote_url:
1018 if settings.GetIsGitSvn():
1019 # URL is dependent on the current directory.
1020 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1021 if data:
1022 keys = dict(line.split(': ', 1) for line in data.splitlines()
1023 if ': ' in line)
1024 remote_url = keys.get('URL', None)
1025 else:
1026 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1027 remote_url = (cl.GetRemoteUrl() + '@'
1028 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001029 if remote_url:
1030 upload_args.extend(['--base_url', remote_url])
1031
1032 try:
1033 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001034 except KeyboardInterrupt:
1035 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001036 except:
1037 # If we got an exception after the user typed a description for their
1038 # change, back up the description before re-raising.
1039 if change_desc:
1040 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1041 print '\nGot exception while uploading -- saving description to %s\n' \
1042 % backup_path
1043 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001044 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001045 backup_file.close()
1046 raise
1047
1048 if not cl.GetIssue():
1049 cl.SetIssue(issue)
1050 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001051
1052 if options.use_commit_queue:
1053 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001054 return 0
1055
1056
ukai@chromium.orge8077812012-02-03 03:41:46 +00001057@usage('[args to "git diff"]')
1058def CMDupload(parser, args):
1059 """upload the current changelist to codereview"""
1060 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1061 help='bypass upload presubmit hook')
1062 parser.add_option('-f', action='store_true', dest='force',
1063 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001064 parser.add_option('-m', dest='message', help='message for patchset')
1065 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001066 parser.add_option('-r', '--reviewers',
1067 help='reviewer email addresses')
1068 parser.add_option('--cc',
1069 help='cc email addresses')
1070 parser.add_option('--send-mail', action='store_true',
1071 help='send email to reviewer immediately')
1072 parser.add_option("--emulate_svn_auto_props", action="store_true",
1073 dest="emulate_svn_auto_props",
1074 help="Emulate Subversion's auto properties feature.")
1075 parser.add_option("--desc_from_logs", action="store_true",
1076 dest="from_logs",
1077 help="""Squashes git commit logs into change description and
1078 uses message as subject""")
1079 parser.add_option('-c', '--use-commit-queue', action='store_true',
1080 help='tell the commit queue to commit this patchset')
1081 if settings.GetIsGerrit():
1082 parser.add_option('--target_branch', dest='target_branch', default='master',
1083 help='target branch to upload')
1084 (options, args) = parser.parse_args(args)
1085
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001086 # Print warning if the user used the -m/--message argument. This will soon
1087 # change to -t/--title.
1088 if options.message:
1089 print >> sys.stderr, (
1090 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1091 'In the near future, -m or --message will send a message instead.\n'
1092 'See http://goo.gl/JGg0Z for details.\n')
1093
ukai@chromium.orge8077812012-02-03 03:41:46 +00001094 # Make sure index is up-to-date before running diff-index.
1095 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1096 if RunGit(['diff-index', 'HEAD']):
1097 print 'Cannot upload with a dirty tree. You must commit locally first.'
1098 return 1
1099
1100 cl = Changelist()
1101 if args:
1102 # TODO(ukai): is it ok for gerrit case?
1103 base_branch = args[0]
1104 else:
1105 # Default to diffing against the "upstream" branch.
1106 base_branch = cl.GetUpstreamBranch()
1107 args = [base_branch + "..."]
1108
1109 if not options.bypass_hooks:
1110 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1111 may_prompt=not options.force,
1112 verbose=options.verbose,
1113 author=None)
1114 if not hook_results.should_continue():
1115 return 1
1116 if not options.reviewers and hook_results.reviewers:
1117 options.reviewers = hook_results.reviewers
1118
1119 # --no-ext-diff is broken in some versions of Git, so try to work around
1120 # this by overriding the environment (but there is still a problem if the
1121 # git config key "diff.external" is used).
1122 env = os.environ.copy()
1123 if 'GIT_EXTERNAL_DIFF' in env:
1124 del env['GIT_EXTERNAL_DIFF']
1125 subprocess2.call(
1126 ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env)
1127
1128 if settings.GetIsGerrit():
1129 return GerritUpload(options, args, cl)
1130 return RietveldUpload(options, args, cl)
1131
1132
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001133def SendUpstream(parser, args, cmd):
1134 """Common code for CmdPush and CmdDCommit
1135
1136 Squashed commit into a single.
1137 Updates changelog with metadata (e.g. pointer to review).
1138 Pushes/dcommits the code upstream.
1139 Updates review and closes.
1140 """
1141 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1142 help='bypass upload presubmit hook')
1143 parser.add_option('-m', dest='message',
1144 help="override review description")
1145 parser.add_option('-f', action='store_true', dest='force',
1146 help="force yes to questions (don't prompt)")
1147 parser.add_option('-c', dest='contributor',
1148 help="external contributor for patch (appended to " +
1149 "description and used as author for git). Should be " +
1150 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001151 (options, args) = parser.parse_args(args)
1152 cl = Changelist()
1153
1154 if not args or cmd == 'push':
1155 # Default to merging against our best guess of the upstream branch.
1156 args = [cl.GetUpstreamBranch()]
1157
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001158 if options.contributor:
1159 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1160 print "Please provide contibutor as 'First Last <email@example.com>'"
1161 return 1
1162
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001163 base_branch = args[0]
1164
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001165 # Make sure index is up-to-date before running diff-index.
1166 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001167 if RunGit(['diff-index', 'HEAD']):
1168 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1169 return 1
1170
1171 # This rev-list syntax means "show all commits not in my branch that
1172 # are in base_branch".
1173 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1174 base_branch]).splitlines()
1175 if upstream_commits:
1176 print ('Base branch "%s" has %d commits '
1177 'not in this branch.' % (base_branch, len(upstream_commits)))
1178 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1179 return 1
1180
1181 if cmd == 'dcommit':
1182 # This is the revision `svn dcommit` will commit on top of.
1183 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1184 '--pretty=format:%H'])
1185 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1186 if extra_commits:
1187 print ('This branch has %d additional commits not upstreamed yet.'
1188 % len(extra_commits.splitlines()))
1189 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1190 'before attempting to %s.' % (base_branch, cmd))
1191 return 1
1192
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001193 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001194 author = None
1195 if options.contributor:
1196 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001197 hook_results = cl.RunHook(
1198 committing=True,
1199 upstream_branch=base_branch,
1200 may_prompt=not options.force,
1201 verbose=options.verbose,
1202 author=author)
1203 if not hook_results.should_continue():
1204 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001205
1206 if cmd == 'dcommit':
1207 # Check the tree status if the tree status URL is set.
1208 status = GetTreeStatus()
1209 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001210 print('The tree is closed. Please wait for it to reopen. Use '
1211 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212 return 1
1213 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001214 print('Unable to determine tree status. Please verify manually and '
1215 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001216 else:
1217 breakpad.SendStack(
1218 'GitClHooksBypassedCommit',
1219 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001220 (cl.GetRietveldServer(), cl.GetIssue()),
1221 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222
1223 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001224 if not description and cl.GetIssue():
1225 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001226
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001227 if not description:
1228 print 'No description set.'
1229 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1230 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001231
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001232 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001234
1235 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001236 description += "\nPatch from %s." % options.contributor
1237 print 'Description:', repr(description)
1238
1239 branches = [base_branch, cl.GetBranchRef()]
1240 if not options.force:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001241 subprocess2.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001242 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001243
1244 # We want to squash all this branch's commits into one commit with the
1245 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001246 # We do this by doing a "reset --soft" to the base branch (which keeps
1247 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001248 MERGE_BRANCH = 'git-cl-commit'
1249 # Delete the merge branch if it already exists.
1250 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1251 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1252 RunGit(['branch', '-D', MERGE_BRANCH])
1253
1254 # We might be in a directory that's present in this branch but not in the
1255 # trunk. Move up to the top of the tree so that git commands that expect a
1256 # valid CWD won't fail after we check out the merge branch.
1257 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1258 if rel_base_path:
1259 os.chdir(rel_base_path)
1260
1261 # Stuff our change into the merge branch.
1262 # We wrap in a try...finally block so if anything goes wrong,
1263 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001264 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001266 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1267 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001268 if options.contributor:
1269 RunGit(['commit', '--author', options.contributor, '-m', description])
1270 else:
1271 RunGit(['commit', '-m', description])
1272 if cmd == 'push':
1273 # push the merge branch.
1274 remote, branch = cl.FetchUpstreamTuple()
1275 retcode, output = RunGitWithCode(
1276 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1277 logging.debug(output)
1278 else:
1279 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001280 retcode, output = RunGitWithCode(['svn', 'dcommit',
1281 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001282 finally:
1283 # And then swap back to the original branch and clean up.
1284 RunGit(['checkout', '-q', cl.GetBranch()])
1285 RunGit(['branch', '-D', MERGE_BRANCH])
1286
1287 if cl.GetIssue():
1288 if cmd == 'dcommit' and 'Committed r' in output:
1289 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1290 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001291 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1292 for l in output.splitlines(False))
1293 match = filter(None, match)
1294 if len(match) != 1:
1295 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1296 output)
1297 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298 else:
1299 return 1
1300 viewvc_url = settings.GetViewVCUrl()
1301 if viewvc_url and revision:
1302 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1303 print ('Closing issue '
1304 '(you may be prompted for your codereview password)...')
1305 cl.CloseIssue()
1306 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001307
1308 if retcode == 0:
1309 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1310 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001311 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001312
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001313 return 0
1314
1315
1316@usage('[upstream branch to apply against]')
1317def CMDdcommit(parser, args):
1318 """commit the current changelist via git-svn"""
1319 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001320 message = """This doesn't appear to be an SVN repository.
1321If your project has a git mirror with an upstream SVN master, you probably need
1322to run 'git svn init', see your project's git mirror documentation.
1323If your project has a true writeable upstream repository, you probably want
1324to run 'git cl push' instead.
1325Choose wisely, if you get this wrong, your commit might appear to succeed but
1326will instead be silently ignored."""
1327 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001328 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001329 return SendUpstream(parser, args, 'dcommit')
1330
1331
1332@usage('[upstream branch to apply against]')
1333def CMDpush(parser, args):
1334 """commit the current changelist via git"""
1335 if settings.GetIsGitSvn():
1336 print('This appears to be an SVN repository.')
1337 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001338 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339 return SendUpstream(parser, args, 'push')
1340
1341
1342@usage('<patch url or issue id>')
1343def CMDpatch(parser, args):
1344 """patch in a code review"""
1345 parser.add_option('-b', dest='newbranch',
1346 help='create a new branch off trunk for the patch')
1347 parser.add_option('-f', action='store_true', dest='force',
1348 help='with -b, clobber any existing branch')
1349 parser.add_option('--reject', action='store_true', dest='reject',
1350 help='allow failed patches and spew .rej files')
1351 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1352 help="don't commit after patch applies")
1353 (options, args) = parser.parse_args(args)
1354 if len(args) != 1:
1355 parser.print_help()
1356 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001357 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001358
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001359 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001360 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001361
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001362 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001363 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001364 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001365 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001367 # Assume it's a URL to the patch. Default to https.
1368 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001369 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001370 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 DieWithError('Must pass an issue ID or full URL for '
1372 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001373 issue = match.group(1)
1374 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001375
1376 if options.newbranch:
1377 if options.force:
1378 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001379 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001380 RunGit(['checkout', '-b', options.newbranch,
1381 Changelist().GetUpstreamBranch()])
1382
1383 # Switch up to the top-level directory, if necessary, in preparation for
1384 # applying the patch.
1385 top = RunGit(['rev-parse', '--show-cdup']).strip()
1386 if top:
1387 os.chdir(top)
1388
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001389 # Git patches have a/ at the beginning of source paths. We strip that out
1390 # with a sed script rather than the -p flag to patch so we can feed either
1391 # Git or svn-style patches into the same apply command.
1392 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001393 try:
1394 patch_data = subprocess2.check_output(
1395 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1396 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001397 DieWithError('Git patch mungling failed.')
1398 logging.info(patch_data)
1399 # We use "git apply" to apply the patch instead of "patch" so that we can
1400 # pick up file adds.
1401 # The --index flag means: also insert into the index (so we catch adds).
1402 cmd = ['git', 'apply', '--index', '-p0']
1403 if options.reject:
1404 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001405 try:
1406 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1407 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001408 DieWithError('Failed to apply the patch')
1409
1410 # If we had an issue, commit the current state and register the issue.
1411 if not options.nocommit:
1412 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1413 cl = Changelist()
1414 cl.SetIssue(issue)
1415 print "Committed patch."
1416 else:
1417 print "Patch applied to index."
1418 return 0
1419
1420
1421def CMDrebase(parser, args):
1422 """rebase current branch on top of svn repo"""
1423 # Provide a wrapper for git svn rebase to help avoid accidental
1424 # git svn dcommit.
1425 # It's the only command that doesn't use parser at all since we just defer
1426 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001427 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001428
1429
1430def GetTreeStatus():
1431 """Fetches the tree status and returns either 'open', 'closed',
1432 'unknown' or 'unset'."""
1433 url = settings.GetTreeStatusUrl(error_ok=True)
1434 if url:
1435 status = urllib2.urlopen(url).read().lower()
1436 if status.find('closed') != -1 or status == '0':
1437 return 'closed'
1438 elif status.find('open') != -1 or status == '1':
1439 return 'open'
1440 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001441 return 'unset'
1442
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001443
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001444def GetTreeStatusReason():
1445 """Fetches the tree status from a json url and returns the message
1446 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001447 url = settings.GetTreeStatusUrl()
1448 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001449 connection = urllib2.urlopen(json_url)
1450 status = json.loads(connection.read())
1451 connection.close()
1452 return status['message']
1453
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001454
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001455def CMDtree(parser, args):
1456 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001457 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001458 status = GetTreeStatus()
1459 if 'unset' == status:
1460 print 'You must configure your tree status URL by running "git cl config".'
1461 return 2
1462
1463 print "The tree is %s" % status
1464 print
1465 print GetTreeStatusReason()
1466 if status != 'open':
1467 return 1
1468 return 0
1469
1470
1471def CMDupstream(parser, args):
1472 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001473 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001474 if args:
1475 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001476 cl = Changelist()
1477 print cl.GetUpstreamBranch()
1478 return 0
1479
1480
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001481def CMDset_commit(parser, args):
1482 """set the commit bit"""
1483 _, args = parser.parse_args(args)
1484 if args:
1485 parser.error('Unrecognized args: %s' % ' '.join(args))
1486 cl = Changelist()
1487 cl.SetFlag('commit', '1')
1488 return 0
1489
1490
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001491def Command(name):
1492 return getattr(sys.modules[__name__], 'CMD' + name, None)
1493
1494
1495def CMDhelp(parser, args):
1496 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001497 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001498 if len(args) == 1:
1499 return main(args + ['--help'])
1500 parser.print_help()
1501 return 0
1502
1503
1504def GenUsage(parser, command):
1505 """Modify an OptParse object with the function's documentation."""
1506 obj = Command(command)
1507 more = getattr(obj, 'usage_more', '')
1508 if command == 'help':
1509 command = '<command>'
1510 else:
1511 # OptParser.description prefer nicely non-formatted strings.
1512 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1513 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1514
1515
1516def main(argv):
1517 """Doesn't parse the arguments here, just find the right subcommand to
1518 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001519 if sys.hexversion < 0x02060000:
1520 print >> sys.stderr, (
1521 '\nYour python version %s is unsupported, please upgrade.\n' %
1522 sys.version.split(' ', 1)[0])
1523 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001524 # Reload settings.
1525 global settings
1526 settings = Settings()
1527
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001528 # Do it late so all commands are listed.
1529 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1530 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1531 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1532
1533 # Create the option parse and add --verbose support.
1534 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001535 parser.add_option(
1536 '-v', '--verbose', action='count', default=0,
1537 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001538 old_parser_args = parser.parse_args
1539 def Parse(args):
1540 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001541 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001542 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001543 elif options.verbose:
1544 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001545 else:
1546 logging.basicConfig(level=logging.WARNING)
1547 return options, args
1548 parser.parse_args = Parse
1549
1550 if argv:
1551 command = Command(argv[0])
1552 if command:
1553 # "fix" the usage and the description now that we know the subcommand.
1554 GenUsage(parser, argv[0])
1555 try:
1556 return command(parser, argv[1:])
1557 except urllib2.HTTPError, e:
1558 if e.code != 500:
1559 raise
1560 DieWithError(
1561 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1562 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1563
1564 # Not a known command. Default to help.
1565 GenUsage(parser, 'help')
1566 return CMDhelp(parser, argv)
1567
1568
1569if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001570 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001571 sys.exit(main(sys.argv[1:]))