blob: ce91817a0ad3728180370b3f63bbd136025a26ef [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.org967c0a82013-06-17 22:52:24 +000010import difflib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000011import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000012import logging
13import optparse
14import os
15import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000016import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import textwrap
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000020import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021
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
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000028from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000029from third_party import upload
30import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000031import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000032import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000034import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000036import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000037import watchlists
38
39
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000040DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000041POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000042DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000043GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000044CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000045
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000046# Shortcut since it quickly becomes redundant.
47Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000048
maruel@chromium.orgddd59412011-11-30 14:20:38 +000049# Initialized in main()
50settings = None
51
52
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000053def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000054 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000055 sys.exit(1)
56
57
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000058def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000059 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000060 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000061 except subprocess2.CalledProcessError as e:
62 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000063 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000064 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000065 'Command "%s" failed.\n%s' % (
66 ' '.join(args), error_message or e.stdout or ''))
67 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068
69
70def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000071 """Returns stdout."""
bratell@opera.comf267b0e2013-05-02 09:11:43 +000072 return RunCommand(['git', '--no-pager'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073
74
75def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000076 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000077 try:
bratell@opera.comf267b0e2013-05-02 09:11:43 +000078 out, code = subprocess2.communicate(['git', '--no-pager'] + args,
79 stdout=subprocess2.PIPE)
szager@chromium.org9bb85e22012-06-13 20:28:23 +000080 return code, out[0]
81 except ValueError:
82 # When the subprocess fails, it returns None. That triggers a ValueError
83 # when trying to unpack the return value into (out, code).
84 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000085
86
87def usage(more):
88 def hook(fn):
89 fn.usage_more = more
90 return fn
91 return hook
92
93
maruel@chromium.org90541732011-04-01 17:54:18 +000094def ask_for_data(prompt):
95 try:
96 return raw_input(prompt)
97 except KeyboardInterrupt:
98 # Hide the exception.
99 sys.exit(1)
100
101
iannucci@chromium.org79540052012-10-19 23:15:26 +0000102def git_set_branch_value(key, value):
103 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000104 if not branch:
105 return
106
107 cmd = ['config']
108 if isinstance(value, int):
109 cmd.append('--int')
110 git_key = 'branch.%s.%s' % (branch, key)
111 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000112
113
114def git_get_branch_default(key, default):
115 branch = Changelist().GetBranch()
116 if branch:
117 git_key = 'branch.%s.%s' % (branch, key)
118 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
119 try:
120 return int(stdout.strip())
121 except ValueError:
122 pass
123 return default
124
125
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000126def add_git_similarity(parser):
127 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000128 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000129 help='Sets the percentage that a pair of files need to match in order to'
130 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000131 parser.add_option(
132 '--find-copies', action='store_true',
133 help='Allows git to look for copies.')
134 parser.add_option(
135 '--no-find-copies', action='store_false', dest='find_copies',
136 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000137
138 old_parser_args = parser.parse_args
139 def Parse(args):
140 options, args = old_parser_args(args)
141
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000142 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000143 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000144 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000145 print('Note: Saving similarity of %d%% in git config.'
146 % options.similarity)
147 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000148
iannucci@chromium.org79540052012-10-19 23:15:26 +0000149 options.similarity = max(0, min(options.similarity, 100))
150
151 if options.find_copies is None:
152 options.find_copies = bool(
153 git_get_branch_default('git-find-copies', True))
154 else:
155 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000156
157 print('Using %d%% similarity for rename/copy detection. '
158 'Override with --similarity.' % options.similarity)
159
160 return options, args
161 parser.parse_args = Parse
162
163
ukai@chromium.org259e4682012-10-25 07:36:33 +0000164def is_dirty_git_tree(cmd):
165 # Make sure index is up-to-date before running diff-index.
166 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
167 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
168 if dirty:
169 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
170 print 'Uncommitted files: (git diff-index --name-status HEAD)'
171 print dirty[:4096]
172 if len(dirty) > 4096:
173 print '... (run "git diff-index --name-status HEAD" to see full output).'
174 return True
175 return False
176
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000177
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000178def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
179 """Return the corresponding git ref if |base_url| together with |glob_spec|
180 matches the full |url|.
181
182 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
183 """
184 fetch_suburl, as_ref = glob_spec.split(':')
185 if allow_wildcards:
186 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
187 if glob_match:
188 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
189 # "branches/{472,597,648}/src:refs/remotes/svn/*".
190 branch_re = re.escape(base_url)
191 if glob_match.group(1):
192 branch_re += '/' + re.escape(glob_match.group(1))
193 wildcard = glob_match.group(2)
194 if wildcard == '*':
195 branch_re += '([^/]*)'
196 else:
197 # Escape and replace surrounding braces with parentheses and commas
198 # with pipe symbols.
199 wildcard = re.escape(wildcard)
200 wildcard = re.sub('^\\\\{', '(', wildcard)
201 wildcard = re.sub('\\\\,', '|', wildcard)
202 wildcard = re.sub('\\\\}$', ')', wildcard)
203 branch_re += wildcard
204 if glob_match.group(3):
205 branch_re += re.escape(glob_match.group(3))
206 match = re.match(branch_re, url)
207 if match:
208 return re.sub('\*$', match.group(1), as_ref)
209
210 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
211 if fetch_suburl:
212 full_url = base_url + '/' + fetch_suburl
213 else:
214 full_url = base_url
215 if full_url == url:
216 return as_ref
217 return None
218
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000219
iannucci@chromium.org79540052012-10-19 23:15:26 +0000220def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000221 """Prints statistics about the change to the user."""
222 # --no-ext-diff is broken in some versions of Git, so try to work around
223 # this by overriding the environment (but there is still a problem if the
224 # git config key "diff.external" is used).
225 env = os.environ.copy()
226 if 'GIT_EXTERNAL_DIFF' in env:
227 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000228
229 if find_copies:
230 similarity_options = ['--find-copies-harder', '-l100000',
231 '-C%s' % similarity]
232 else:
233 similarity_options = ['-M%s' % similarity]
234
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000235 return subprocess2.call(
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000236 ['git', '--no-pager',
237 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000238 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000239
240
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000241class Settings(object):
242 def __init__(self):
243 self.default_server = None
244 self.cc = None
245 self.root = None
246 self.is_git_svn = None
247 self.svn_branch = None
248 self.tree_status_url = None
249 self.viewvc_url = None
250 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000251 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000252 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000253
254 def LazyUpdateIfNeeded(self):
255 """Updates the settings from a codereview.settings file, if available."""
256 if not self.updated:
257 cr_settings_file = FindCodereviewSettingsFile()
258 if cr_settings_file:
259 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000260 self.updated = True
261 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000262 self.updated = True
263
264 def GetDefaultServerUrl(self, error_ok=False):
265 if not self.default_server:
266 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000267 self.default_server = gclient_utils.UpgradeToHttps(
268 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000269 if error_ok:
270 return self.default_server
271 if not self.default_server:
272 error_message = ('Could not find settings file. You must configure '
273 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000274 self.default_server = gclient_utils.UpgradeToHttps(
275 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000276 return self.default_server
277
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000278 def GetRoot(self):
279 if not self.root:
280 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
281 return self.root
282
283 def GetIsGitSvn(self):
284 """Return true if this repo looks like it's using git-svn."""
285 if self.is_git_svn is None:
286 # If you have any "svn-remote.*" config keys, we think you're using svn.
287 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000288 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000289 return self.is_git_svn
290
291 def GetSVNBranch(self):
292 if self.svn_branch is None:
293 if not self.GetIsGitSvn():
294 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
295
296 # Try to figure out which remote branch we're based on.
297 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000298 # 1) iterate through our branch history and find the svn URL.
299 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000300
301 # regexp matching the git-svn line that contains the URL.
302 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
303
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000304 # We don't want to go through all of history, so read a line from the
305 # pipe at a time.
306 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000307 cmd = ['git', '--no-pager', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000308 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000309 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000310 for line in proc.stdout:
311 match = git_svn_re.match(line)
312 if match:
313 url = match.group(1)
314 proc.stdout.close() # Cut pipe.
315 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000316
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000317 if url:
318 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
319 remotes = RunGit(['config', '--get-regexp',
320 r'^svn-remote\..*\.url']).splitlines()
321 for remote in remotes:
322 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000323 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000324 remote = match.group(1)
325 base_url = match.group(2)
326 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000327 ['config', 'svn-remote.%s.fetch' % remote],
328 error_ok=True).strip()
329 if fetch_spec:
330 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
331 if self.svn_branch:
332 break
333 branch_spec = RunGit(
334 ['config', 'svn-remote.%s.branches' % remote],
335 error_ok=True).strip()
336 if branch_spec:
337 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
338 if self.svn_branch:
339 break
340 tag_spec = RunGit(
341 ['config', 'svn-remote.%s.tags' % remote],
342 error_ok=True).strip()
343 if tag_spec:
344 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
345 if self.svn_branch:
346 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000347
348 if not self.svn_branch:
349 DieWithError('Can\'t guess svn branch -- try specifying it on the '
350 'command line')
351
352 return self.svn_branch
353
354 def GetTreeStatusUrl(self, error_ok=False):
355 if not self.tree_status_url:
356 error_message = ('You must configure your tree status URL by running '
357 '"git cl config".')
358 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
359 error_ok=error_ok,
360 error_message=error_message)
361 return self.tree_status_url
362
363 def GetViewVCUrl(self):
364 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000365 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000366 return self.viewvc_url
367
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000368 def GetDefaultCCList(self):
369 return self._GetConfig('rietveld.cc', error_ok=True)
370
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000371 def GetDefaultPrivateFlag(self):
372 return self._GetConfig('rietveld.private', error_ok=True)
373
ukai@chromium.orge8077812012-02-03 03:41:46 +0000374 def GetIsGerrit(self):
375 """Return true if this repo is assosiated with gerrit code review system."""
376 if self.is_gerrit is None:
377 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
378 return self.is_gerrit
379
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000380 def GetGitEditor(self):
381 """Return the editor specified in the git config, or None if none is."""
382 if self.git_editor is None:
383 self.git_editor = self._GetConfig('core.editor', error_ok=True)
384 return self.git_editor or None
385
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000386 def _GetConfig(self, param, **kwargs):
387 self.LazyUpdateIfNeeded()
388 return RunGit(['config', param], **kwargs).strip()
389
390
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000391def ShortBranchName(branch):
392 """Convert a name like 'refs/heads/foo' to just 'foo'."""
393 return branch.replace('refs/heads/', '')
394
395
396class Changelist(object):
397 def __init__(self, branchref=None):
398 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000399 global settings
400 if not settings:
401 # Happens when git_cl.py is used as a utility library.
402 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000403 settings.GetDefaultServerUrl()
404 self.branchref = branchref
405 if self.branchref:
406 self.branch = ShortBranchName(self.branchref)
407 else:
408 self.branch = None
409 self.rietveld_server = None
410 self.upstream_branch = None
411 self.has_issue = False
412 self.issue = None
413 self.has_description = False
414 self.description = None
415 self.has_patchset = False
416 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000417 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000418 self.cc = None
419 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000420 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000421
422 def GetCCList(self):
423 """Return the users cc'd on this CL.
424
425 Return is a string suitable for passing to gcl with the --cc flag.
426 """
427 if self.cc is None:
428 base_cc = settings .GetDefaultCCList()
429 more_cc = ','.join(self.watchers)
430 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
431 return self.cc
432
433 def SetWatchers(self, watchers):
434 """Set the list of email addresses that should be cc'd based on the changed
435 files in this CL.
436 """
437 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000438
439 def GetBranch(self):
440 """Returns the short branch name, e.g. 'master'."""
441 if not self.branch:
442 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
443 self.branch = ShortBranchName(self.branchref)
444 return self.branch
445
446 def GetBranchRef(self):
447 """Returns the full branch name, e.g. 'refs/heads/master'."""
448 self.GetBranch() # Poke the lazy loader.
449 return self.branchref
450
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000451 @staticmethod
452 def FetchUpstreamTuple(branch):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000453 """Returns a tuple containg remote and remote ref,
454 e.g. 'origin', 'refs/heads/master'
455 """
456 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000457 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
458 error_ok=True).strip()
459 if upstream_branch:
460 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
461 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000462 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
463 error_ok=True).strip()
464 if upstream_branch:
465 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000466 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000467 # Fall back on trying a git-svn upstream branch.
468 if settings.GetIsGitSvn():
469 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000470 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000471 # Else, try to guess the origin remote.
472 remote_branches = RunGit(['branch', '-r']).split()
473 if 'origin/master' in remote_branches:
474 # Fall back on origin/master if it exits.
475 remote = 'origin'
476 upstream_branch = 'refs/heads/master'
477 elif 'origin/trunk' in remote_branches:
478 # Fall back on origin/trunk if it exists. Generally a shared
479 # git-svn clone
480 remote = 'origin'
481 upstream_branch = 'refs/heads/trunk'
482 else:
483 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000484Either pass complete "git diff"-style arguments, like
485 git cl upload origin/master
486or verify this branch is set up to track another (via the --track argument to
487"git checkout -b ...").""")
488
489 return remote, upstream_branch
490
491 def GetUpstreamBranch(self):
492 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000493 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000494 if remote is not '.':
495 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
496 self.upstream_branch = upstream_branch
497 return self.upstream_branch
498
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000499 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000500 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000501 remote, branch = None, self.GetBranch()
502 seen_branches = set()
503 while branch not in seen_branches:
504 seen_branches.add(branch)
505 remote, branch = self.FetchUpstreamTuple(branch)
506 branch = ShortBranchName(branch)
507 if remote != '.' or branch.startswith('refs/remotes'):
508 break
509 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000510 remotes = RunGit(['remote'], error_ok=True).split()
511 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000512 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000513 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000514 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000515 logging.warning('Could not determine which remote this change is '
516 'associated with, so defaulting to "%s". This may '
517 'not be what you want. You may prevent this message '
518 'by running "git svn info" as documented here: %s',
519 self._remote,
520 GIT_INSTRUCTIONS_URL)
521 else:
522 logging.warn('Could not determine which remote this change is '
523 'associated with. You may prevent this message by '
524 'running "git svn info" as documented here: %s',
525 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000526 branch = 'HEAD'
527 if branch.startswith('refs/remotes'):
528 self._remote = (remote, branch)
529 else:
530 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000531 return self._remote
532
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000533 def GitSanityChecks(self, upstream_git_obj):
534 """Checks git repo status and ensures diff is from local commits."""
535
536 # Verify the commit we're diffing against is in our current branch.
537 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
538 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
539 if upstream_sha != common_ancestor:
540 print >> sys.stderr, (
541 'ERROR: %s is not in the current branch. You may need to rebase '
542 'your tracking branch' % upstream_sha)
543 return False
544
545 # List the commits inside the diff, and verify they are all local.
546 commits_in_diff = RunGit(
547 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
548 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
549 remote_branch = remote_branch.strip()
550 if code != 0:
551 _, remote_branch = self.GetRemoteBranch()
552
553 commits_in_remote = RunGit(
554 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
555
556 common_commits = set(commits_in_diff) & set(commits_in_remote)
557 if common_commits:
558 print >> sys.stderr, (
559 'ERROR: Your diff contains %d commits already in %s.\n'
560 'Run "git log --oneline %s..HEAD" to get a list of commits in '
561 'the diff. If you are using a custom git flow, you can override'
562 ' the reference used for this check with "git config '
563 'gitcl.remotebranch <git-ref>".' % (
564 len(common_commits), remote_branch, upstream_git_obj))
565 return False
566 return True
567
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000568 def GetGitBaseUrlFromConfig(self):
569 """Return the configured base URL from branch.<branchname>.baseurl.
570
571 Returns None if it is not set.
572 """
573 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
574 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000575
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000576 def GetRemoteUrl(self):
577 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
578
579 Returns None if there is no remote.
580 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000581 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000582 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
583
584 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000585 """Returns the issue number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000586 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000587 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
588 if issue:
maruel@chromium.org52424302012-08-29 15:14:30 +0000589 self.issue = int(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000590 else:
591 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000592 self.has_issue = True
593 return self.issue
594
595 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000596 if not self.rietveld_server:
597 # If we're on a branch then get the server potentially associated
598 # with that branch.
599 if self.GetIssue():
600 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
601 ['config', self._RietveldServer()], error_ok=True).strip())
602 if not self.rietveld_server:
603 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000604 return self.rietveld_server
605
606 def GetIssueURL(self):
607 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000608 if not self.GetIssue():
609 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000610 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
611
612 def GetDescription(self, pretty=False):
613 if not self.has_description:
614 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000615 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000616 try:
617 self.description = self.RpcServer().get_description(issue).strip()
618 except urllib2.HTTPError, e:
619 if e.code == 404:
620 DieWithError(
621 ('\nWhile fetching the description for issue %d, received a '
622 '404 (not found)\n'
623 'error. It is likely that you deleted this '
624 'issue on the server. If this is the\n'
625 'case, please run\n\n'
626 ' git cl issue 0\n\n'
627 'to clear the association with the deleted issue. Then run '
628 'this command again.') % issue)
629 else:
630 DieWithError(
631 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000632 self.has_description = True
633 if pretty:
634 wrapper = textwrap.TextWrapper()
635 wrapper.initial_indent = wrapper.subsequent_indent = ' '
636 return wrapper.fill(self.description)
637 return self.description
638
639 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000640 """Returns the patchset number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000641 if not self.has_patchset:
642 patchset = RunGit(['config', self._PatchsetSetting()],
643 error_ok=True).strip()
644 if patchset:
maruel@chromium.org52424302012-08-29 15:14:30 +0000645 self.patchset = int(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000646 else:
647 self.patchset = None
648 self.has_patchset = True
649 return self.patchset
650
651 def SetPatchset(self, patchset):
652 """Set this branch's patchset. If patchset=0, clears the patchset."""
653 if patchset:
654 RunGit(['config', self._PatchsetSetting(), str(patchset)])
655 else:
656 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000657 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000658 self.has_patchset = False
659
binji@chromium.org0281f522012-09-14 13:37:59 +0000660 def GetMostRecentPatchset(self, issue):
661 return self.RpcServer().get_issue_properties(
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000662 int(issue), False)['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000663
664 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000665 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000666 '/download/issue%s_%s.diff' % (issue, patchset))
667
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000668 def GetApprovingReviewers(self, issue):
669 return get_approving_reviewers(
670 self.RpcServer().get_issue_properties(int(issue), True))
671
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000672 def SetIssue(self, issue):
673 """Set this branch's issue. If issue=0, clears the issue."""
674 if issue:
675 RunGit(['config', self._IssueSetting(), str(issue)])
676 if self.rietveld_server:
677 RunGit(['config', self._RietveldServer(), self.rietveld_server])
678 else:
679 RunGit(['config', '--unset', self._IssueSetting()])
680 self.SetPatchset(0)
681 self.has_issue = False
682
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000683 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000684 if not self.GitSanityChecks(upstream_branch):
685 DieWithError('\nGit sanity check failure')
686
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000687 root = RunCommand(['git', '--no-pager', 'rev-parse', '--show-cdup']).strip()
688 if not root:
689 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000690 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000691
692 # We use the sha1 of HEAD as a name of this change.
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000693 name = RunCommand(['git', '--no-pager', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000694 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000695 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000696 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000697 except subprocess2.CalledProcessError:
698 DieWithError(
699 ('\nFailed to diff against upstream branch %s!\n\n'
700 'This branch probably doesn\'t exist anymore. To reset the\n'
701 'tracking branch, please run\n'
702 ' git branch --set-upstream %s trunk\n'
703 'replacing trunk with origin/master or the relevant branch') %
704 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000705
maruel@chromium.org52424302012-08-29 15:14:30 +0000706 issue = self.GetIssue()
707 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000708 if issue:
709 description = self.GetDescription()
710 else:
711 # If the change was never uploaded, use the log messages of all commits
712 # up to the branch point, as git cl upload will prefill the description
713 # with these log messages.
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000714 description = RunCommand(['git', '--no-pager',
715 'log', '--pretty=format:%s%n%n%b',
maruel@chromium.org373af802012-05-25 21:07:33 +0000716 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000717
718 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000719 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000720 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000721 name,
722 description,
723 absroot,
724 files,
725 issue,
726 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000727 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000728
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000729 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000730 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000731
732 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000733 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000734 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000735 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000736 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000737 except presubmit_support.PresubmitFailure, e:
738 DieWithError(
739 ('%s\nMaybe your depot_tools is out of date?\n'
740 'If all fails, contact maruel@') % e)
741
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000742 def UpdateDescription(self, description):
743 self.description = description
744 return self.RpcServer().update_description(
745 self.GetIssue(), self.description)
746
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000747 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000748 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000749 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000750
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000751 def SetFlag(self, flag, value):
752 """Patchset must match."""
753 if not self.GetPatchset():
754 DieWithError('The patchset needs to match. Send another patchset.')
755 try:
756 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000757 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000758 except urllib2.HTTPError, e:
759 if e.code == 404:
760 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
761 if e.code == 403:
762 DieWithError(
763 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
764 'match?') % (self.GetIssue(), self.GetPatchset()))
765 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000766
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000767 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000768 """Returns an upload.RpcServer() to access this review's rietveld instance.
769 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000770 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000771 self._rpc_server = rietveld.CachingRietveld(
772 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000773 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000774
775 def _IssueSetting(self):
776 """Return the git setting that stores this change's issue."""
777 return 'branch.%s.rietveldissue' % self.GetBranch()
778
779 def _PatchsetSetting(self):
780 """Return the git setting that stores this change's most recent patchset."""
781 return 'branch.%s.rietveldpatchset' % self.GetBranch()
782
783 def _RietveldServer(self):
784 """Returns the git setting that stores this change's rietveld server."""
785 return 'branch.%s.rietveldserver' % self.GetBranch()
786
787
788def GetCodereviewSettingsInteractively():
789 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000790 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000791 server = settings.GetDefaultServerUrl(error_ok=True)
792 prompt = 'Rietveld server (host[:port])'
793 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000794 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795 if not server and not newserver:
796 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000797 if newserver:
798 newserver = gclient_utils.UpgradeToHttps(newserver)
799 if newserver != server:
800 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000802 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000803 prompt = caption
804 if initial:
805 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000806 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807 if new_val == 'x':
808 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000809 elif new_val:
810 if is_url:
811 new_val = gclient_utils.UpgradeToHttps(new_val)
812 if new_val != initial:
813 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000815 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000816 SetProperty(settings.GetDefaultPrivateFlag(),
817 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000818 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000819 'tree-status-url', False)
820 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000821
822 # TODO: configure a default branch to diff against, rather than this
823 # svn-based hackery.
824
825
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000826class ChangeDescription(object):
827 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000828 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000829
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000830 def __init__(self, description):
831 self._description = (description or '').strip()
832
833 @property
834 def description(self):
835 return self._description
836
837 def update_reviewers(self, reviewers):
838 """Rewrites the R=/TBR= line(s) as a single line."""
839 assert isinstance(reviewers, list), reviewers
840 if not reviewers:
841 return
842 regexp = re.compile(self.R_LINE, re.MULTILINE)
843 matches = list(regexp.finditer(self._description))
844 is_tbr = any(m.group(1) == 'TBR' for m in matches)
845 if len(matches) > 1:
846 # Erase all except the first one.
847 for i in xrange(len(matches) - 1, 0, -1):
848 self._description = (
849 self._description[:matches[i].start()] +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000850 self._description[matches[i].end():])
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000851
852 if is_tbr:
853 new_r_line = 'TBR=' + ', '.join(reviewers)
854 else:
855 new_r_line = 'R=' + ', '.join(reviewers)
856
857 if matches:
858 self._description = (
859 self._description[:matches[0].start()] + new_r_line +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000860 self._description[matches[0].end():]).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000861 else:
862 self.append_footer(new_r_line)
863
864 def prompt(self):
865 """Asks the user to update the description."""
866 self._description = (
867 '# Enter a description of the change.\n'
janx@chromium.org104b2db2013-04-18 12:58:40 +0000868 '# This will be displayed on the codereview site.\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000869 '# The first line will also be used as the subject of the review.\n'
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000870 '#--------------------This line is 72 characters long'
871 '--------------------\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000872 ) + self._description
873
874 if '\nBUG=' not in self._description:
875 self.append_footer('BUG=')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000876 content = gclient_utils.RunEditor(self._description, True,
877 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000878 if not content:
879 DieWithError('Running editor failed')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000880
881 # Strip off comments.
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000882 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000883 if not content:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000884 DieWithError('No CL description, aborting')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000885 self._description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000886
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000887 def append_footer(self, line):
888 # Adds a LF if the last line did not have 'FOO=BAR' or if the new one isn't.
889 if self._description:
890 if '\n' not in self._description:
891 self._description += '\n'
892 else:
893 last_line = self._description.rsplit('\n', 1)[1]
894 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
895 not presubmit_support.Change.TAG_LINE_RE.match(line)):
896 self._description += '\n'
897 self._description += '\n' + line
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000898
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000899 def get_reviewers(self):
900 """Retrieves the list of reviewers."""
901 regexp = re.compile(self.R_LINE, re.MULTILINE)
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000902 reviewers = [i.group(2).strip() for i in regexp.finditer(self._description)]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000903 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000904
905
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000906def get_approving_reviewers(props):
907 """Retrieves the reviewers that approved a CL from the issue properties with
908 messages.
909
910 Note that the list may contain reviewers that are not committer, thus are not
911 considered by the CQ.
912 """
913 return sorted(
914 set(
915 message['sender']
916 for message in props['messages']
917 if message['approval'] and message['sender'] in props['reviewers']
918 )
919 )
920
921
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000922def FindCodereviewSettingsFile(filename='codereview.settings'):
923 """Finds the given file starting in the cwd and going up.
924
925 Only looks up to the top of the repository unless an
926 'inherit-review-settings-ok' file exists in the root of the repository.
927 """
928 inherit_ok_file = 'inherit-review-settings-ok'
929 cwd = os.getcwd()
930 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
931 if os.path.isfile(os.path.join(root, inherit_ok_file)):
932 root = '/'
933 while True:
934 if filename in os.listdir(cwd):
935 if os.path.isfile(os.path.join(cwd, filename)):
936 return open(os.path.join(cwd, filename))
937 if cwd == root:
938 break
939 cwd = os.path.dirname(cwd)
940
941
942def LoadCodereviewSettingsFromFile(fileobj):
943 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000944 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000945
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000946 def SetProperty(name, setting, unset_error_ok=False):
947 fullname = 'rietveld.' + name
948 if setting in keyvals:
949 RunGit(['config', fullname, keyvals[setting]])
950 else:
951 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
952
953 SetProperty('server', 'CODE_REVIEW_SERVER')
954 # Only server setting is required. Other settings can be absent.
955 # In that case, we ignore errors raised during option deletion attempt.
956 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000957 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000958 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
959 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
960
ukai@chromium.orge8077812012-02-03 03:41:46 +0000961 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
962 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
963 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000964
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000965 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
966 #should be of the form
967 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
968 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
969 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
970 keyvals['ORIGIN_URL_CONFIG']])
971
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000972
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000973def urlretrieve(source, destination):
974 """urllib is broken for SSL connections via a proxy therefore we
975 can't use urllib.urlretrieve()."""
976 with open(destination, 'w') as f:
977 f.write(urllib2.urlopen(source).read())
978
979
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000980def DownloadHooks(force):
981 """downloads hooks
982
983 Args:
984 force: True to update hooks. False to install hooks if not present.
985 """
986 if not settings.GetIsGerrit():
987 return
988 server_url = settings.GetDefaultServerUrl()
989 src = '%s/tools/hooks/commit-msg' % server_url
990 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
991 if not os.access(dst, os.X_OK):
992 if os.path.exists(dst):
993 if not force:
994 return
995 os.remove(dst)
996 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000997 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000998 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
999 except Exception:
1000 if os.path.exists(dst):
1001 os.remove(dst)
1002 DieWithError('\nFailed to download hooks from %s' % src)
1003
1004
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005@usage('[repo root containing codereview.settings]')
1006def CMDconfig(parser, args):
1007 """edit configuration for this tree"""
1008
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001009 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001010 if len(args) == 0:
1011 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001012 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001013 return 0
1014
1015 url = args[0]
1016 if not url.endswith('codereview.settings'):
1017 url = os.path.join(url, 'codereview.settings')
1018
1019 # Load code review settings and download hooks (if available).
1020 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001021 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001022 return 0
1023
1024
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001025def CMDbaseurl(parser, args):
1026 """get or set base-url for this branch"""
1027 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1028 branch = ShortBranchName(branchref)
1029 _, args = parser.parse_args(args)
1030 if not args:
1031 print("Current base-url:")
1032 return RunGit(['config', 'branch.%s.base-url' % branch],
1033 error_ok=False).strip()
1034 else:
1035 print("Setting base-url to %s" % args[0])
1036 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1037 error_ok=False).strip()
1038
1039
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001040def CMDstatus(parser, args):
1041 """show status of changelists"""
1042 parser.add_option('--field',
1043 help='print only specific field (desc|id|patch|url)')
1044 (options, args) = parser.parse_args(args)
1045
1046 # TODO: maybe make show_branches a flag if necessary.
1047 show_branches = not options.field
1048
1049 if show_branches:
1050 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1051 if branches:
rch@chromium.org92d67162012-04-02 20:10:35 +00001052 changes = (Changelist(branchref=b) for b in branches.splitlines())
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001053 branches = dict((cl.GetBranch(), cl.GetIssueURL()) for cl in changes)
rch@chromium.org92d67162012-04-02 20:10:35 +00001054 alignment = max(5, max(len(b) for b in branches))
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001055 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +00001056 for branch in sorted(branches):
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001057 print " %*s: %s" % (alignment, branch, branches[branch] or '')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001058
1059 cl = Changelist()
1060 if options.field:
1061 if options.field.startswith('desc'):
1062 print cl.GetDescription()
1063 elif options.field == 'id':
1064 issueid = cl.GetIssue()
1065 if issueid:
1066 print issueid
1067 elif options.field == 'patch':
1068 patchset = cl.GetPatchset()
1069 if patchset:
1070 print patchset
1071 elif options.field == 'url':
1072 url = cl.GetIssueURL()
1073 if url:
1074 print url
1075 else:
1076 print
1077 print 'Current branch:',
1078 if not cl.GetIssue():
1079 print 'no issue assigned.'
1080 return 0
1081 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +00001082 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001083 print 'Issue description:'
1084 print cl.GetDescription(pretty=True)
1085 return 0
1086
1087
1088@usage('[issue_number]')
1089def CMDissue(parser, args):
1090 """Set or display the current code review issue number.
1091
1092 Pass issue number 0 to clear the current issue.
1093"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001094 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001095
1096 cl = Changelist()
1097 if len(args) > 0:
1098 try:
1099 issue = int(args[0])
1100 except ValueError:
1101 DieWithError('Pass a number to set the issue or none to list it.\n'
1102 'Maybe you want to run git cl status?')
1103 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001104 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001105 return 0
1106
1107
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001108def CMDcomments(parser, args):
1109 """show review comments of the current changelist"""
1110 (_, args) = parser.parse_args(args)
1111 if args:
1112 parser.error('Unsupported argument: %s' % args)
1113
1114 cl = Changelist()
1115 if cl.GetIssue():
1116 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
1117 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001118 if message['disapproval']:
1119 color = Fore.RED
1120 elif message['approval']:
1121 color = Fore.GREEN
1122 elif message['sender'] == data['owner_email']:
1123 color = Fore.MAGENTA
1124 else:
1125 color = Fore.BLUE
1126 print '\n%s%s %s%s' % (
1127 color, message['date'].split('.', 1)[0], message['sender'],
1128 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001129 if message['text'].strip():
1130 print '\n'.join(' ' + l for l in message['text'].splitlines())
1131 return 0
1132
1133
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001134def CMDdescription(parser, args):
1135 """brings up the editor for the current CL's description."""
1136 cl = Changelist()
1137 if not cl.GetIssue():
1138 DieWithError('This branch has no associated changelist.')
1139 description = ChangeDescription(cl.GetDescription())
1140 description.prompt()
1141 cl.UpdateDescription(description.description)
1142 return 0
1143
1144
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145def CreateDescriptionFromLog(args):
1146 """Pulls out the commit log to use as a base for the CL description."""
1147 log_args = []
1148 if len(args) == 1 and not args[0].endswith('.'):
1149 log_args = [args[0] + '..']
1150 elif len(args) == 1 and args[0].endswith('...'):
1151 log_args = [args[0][:-1]]
1152 elif len(args) == 2:
1153 log_args = [args[0] + '..' + args[1]]
1154 else:
1155 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001156 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001157
1158
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001159def CMDpresubmit(parser, args):
1160 """run presubmit tests on the current changelist"""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001161 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001162 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001163 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001164 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 (options, args) = parser.parse_args(args)
1166
ukai@chromium.org259e4682012-10-25 07:36:33 +00001167 if not options.force and is_dirty_git_tree('presubmit'):
1168 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169 return 1
1170
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001171 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001172 if args:
1173 base_branch = args[0]
1174 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001175 # Default to diffing against the common ancestor of the upstream branch.
1176 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001177
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001178 cl.RunHook(
1179 committing=not options.upload,
1180 may_prompt=False,
1181 verbose=options.verbose,
1182 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001183 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184
1185
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001186def AddChangeIdToCommitMessage(options, args):
1187 """Re-commits using the current message, assumes the commit hook is in
1188 place.
1189 """
1190 log_desc = options.message or CreateDescriptionFromLog(args)
1191 git_command = ['commit', '--amend', '-m', log_desc]
1192 RunGit(git_command)
1193 new_log_desc = CreateDescriptionFromLog(args)
1194 if CHANGE_ID in new_log_desc:
1195 print 'git-cl: Added Change-Id to commit message.'
1196 else:
1197 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1198
1199
ukai@chromium.orge8077812012-02-03 03:41:46 +00001200def GerritUpload(options, args, cl):
1201 """upload the current branch to gerrit."""
1202 # We assume the remote called "origin" is the one we want.
1203 # It is probably not worthwhile to support different workflows.
1204 remote = 'origin'
1205 branch = 'master'
1206 if options.target_branch:
1207 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001208
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001209 change_desc = ChangeDescription(
1210 options.message or CreateDescriptionFromLog(args))
1211 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001212 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001214 if CHANGE_ID not in change_desc.description:
1215 AddChangeIdToCommitMessage(options, args)
1216 if options.reviewers:
1217 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218
ukai@chromium.orge8077812012-02-03 03:41:46 +00001219 receive_options = []
1220 cc = cl.GetCCList().split(',')
1221 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001222 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001223 cc = filter(None, cc)
1224 if cc:
1225 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001226 if change_desc.get_reviewers():
1227 receive_options.extend(
1228 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001229
ukai@chromium.orge8077812012-02-03 03:41:46 +00001230 git_command = ['push']
1231 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001232 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001233 ' '.join(receive_options))
1234 git_command += [remote, 'HEAD:refs/for/' + branch]
1235 RunGit(git_command)
1236 # TODO(ukai): parse Change-Id: and set issue number?
1237 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001238
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239
ukai@chromium.orge8077812012-02-03 03:41:46 +00001240def RietveldUpload(options, args, cl):
1241 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001242 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1243 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001244 if options.emulate_svn_auto_props:
1245 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246
1247 change_desc = None
1248
1249 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001250 if options.title:
1251 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001252 if options.message:
1253 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001254 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255 print ("This branch is associated with issue %s. "
1256 "Adding patch to that issue." % cl.GetIssue())
1257 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001258 if options.title:
1259 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001260 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001261 change_desc = ChangeDescription(message)
1262 if options.reviewers:
1263 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001264 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001265 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001266
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001267 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001268 print "Description is empty; aborting."
1269 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001270
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001271 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001272 if change_desc.get_reviewers():
1273 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001274 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001275 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001276 DieWithError("Must specify reviewers to send email.")
1277 upload_args.append('--send_mail')
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001278 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001279 if cc:
1280 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001281
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001282 if options.private or settings.GetDefaultPrivateFlag() == "True":
1283 upload_args.append('--private')
1284
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001285 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001286 if not options.find_copies:
1287 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001288
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001289 # Include the upstream repo's URL in the change -- this is useful for
1290 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001291 remote_url = cl.GetGitBaseUrlFromConfig()
1292 if not remote_url:
1293 if settings.GetIsGitSvn():
1294 # URL is dependent on the current directory.
1295 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1296 if data:
1297 keys = dict(line.split(': ', 1) for line in data.splitlines()
1298 if ': ' in line)
1299 remote_url = keys.get('URL', None)
1300 else:
1301 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1302 remote_url = (cl.GetRemoteUrl() + '@'
1303 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001304 if remote_url:
1305 upload_args.extend(['--base_url', remote_url])
1306
1307 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001308 upload_args = ['upload'] + upload_args + args
1309 logging.info('upload.RealMain(%s)', upload_args)
1310 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001311 except KeyboardInterrupt:
1312 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001313 except:
1314 # If we got an exception after the user typed a description for their
1315 # change, back up the description before re-raising.
1316 if change_desc:
1317 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1318 print '\nGot exception while uploading -- saving description to %s\n' \
1319 % backup_path
1320 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001321 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001322 backup_file.close()
1323 raise
1324
1325 if not cl.GetIssue():
1326 cl.SetIssue(issue)
1327 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001328
1329 if options.use_commit_queue:
1330 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001331 return 0
1332
1333
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001334def cleanup_list(l):
1335 """Fixes a list so that comma separated items are put as individual items.
1336
1337 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1338 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1339 """
1340 items = sum((i.split(',') for i in l), [])
1341 stripped_items = (i.strip() for i in items)
1342 return sorted(filter(None, stripped_items))
1343
1344
ukai@chromium.orge8077812012-02-03 03:41:46 +00001345@usage('[args to "git diff"]')
1346def CMDupload(parser, args):
1347 """upload the current changelist to codereview"""
1348 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1349 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001350 parser.add_option('--bypass-watchlists', action='store_true',
1351 dest='bypass_watchlists',
1352 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001353 parser.add_option('-f', action='store_true', dest='force',
1354 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001355 parser.add_option('-m', dest='message', help='message for patchset')
1356 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001357 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001358 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001359 help='reviewer email addresses')
1360 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001361 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001362 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001363 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001364 help='send email to reviewer immediately')
1365 parser.add_option("--emulate_svn_auto_props", action="store_true",
1366 dest="emulate_svn_auto_props",
1367 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001368 parser.add_option('-c', '--use-commit-queue', action='store_true',
1369 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001370 parser.add_option('--private', action='store_true',
1371 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001372 parser.add_option('--target_branch',
1373 help='When uploading to gerrit, remote branch to '
1374 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001375 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001376 (options, args) = parser.parse_args(args)
1377
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001378 if options.target_branch and not settings.GetIsGerrit():
1379 parser.error('Use --target_branch for non gerrit repository.')
1380
ukai@chromium.org259e4682012-10-25 07:36:33 +00001381 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001382 return 1
1383
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001384 options.reviewers = cleanup_list(options.reviewers)
1385 options.cc = cleanup_list(options.cc)
1386
ukai@chromium.orge8077812012-02-03 03:41:46 +00001387 cl = Changelist()
1388 if args:
1389 # TODO(ukai): is it ok for gerrit case?
1390 base_branch = args[0]
1391 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001392 # Default to diffing against common ancestor of upstream branch
1393 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001394 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001395
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001396 # Apply watchlists on upload.
1397 change = cl.GetChange(base_branch, None)
1398 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1399 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001400 if not options.bypass_watchlists:
1401 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001402
ukai@chromium.orge8077812012-02-03 03:41:46 +00001403 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001404 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001405 may_prompt=not options.force,
1406 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001407 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001408 if not hook_results.should_continue():
1409 return 1
1410 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001411 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001412
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001413 if cl.GetIssue():
1414 latest_patchset = cl.GetMostRecentPatchset(cl.GetIssue())
1415 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001416 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001417 print ('The last upload made from this repository was patchset #%d but '
1418 'the most recent patchset on the server is #%d.'
1419 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001420 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1421 'from another machine or branch the patch you\'re uploading now '
1422 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001423 ask_for_data('About to upload; enter to confirm.')
1424
iannucci@chromium.org79540052012-10-19 23:15:26 +00001425 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001426 if settings.GetIsGerrit():
1427 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001428 ret = RietveldUpload(options, args, cl)
1429 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001430 git_set_branch_value('last-upload-hash',
1431 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001432
1433 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001434
1435
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001436def IsSubmoduleMergeCommit(ref):
1437 # When submodules are added to the repo, we expect there to be a single
1438 # non-git-svn merge commit at remote HEAD with a signature comment.
1439 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001440 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001441 return RunGit(cmd) != ''
1442
1443
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001444def SendUpstream(parser, args, cmd):
1445 """Common code for CmdPush and CmdDCommit
1446
1447 Squashed commit into a single.
1448 Updates changelog with metadata (e.g. pointer to review).
1449 Pushes/dcommits the code upstream.
1450 Updates review and closes.
1451 """
1452 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1453 help='bypass upload presubmit hook')
1454 parser.add_option('-m', dest='message',
1455 help="override review description")
1456 parser.add_option('-f', action='store_true', dest='force',
1457 help="force yes to questions (don't prompt)")
1458 parser.add_option('-c', dest='contributor',
1459 help="external contributor for patch (appended to " +
1460 "description and used as author for git). Should be " +
1461 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001462 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001463 (options, args) = parser.parse_args(args)
1464 cl = Changelist()
1465
1466 if not args or cmd == 'push':
1467 # Default to merging against our best guess of the upstream branch.
1468 args = [cl.GetUpstreamBranch()]
1469
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001470 if options.contributor:
1471 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1472 print "Please provide contibutor as 'First Last <email@example.com>'"
1473 return 1
1474
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001476 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001477
ukai@chromium.org259e4682012-10-25 07:36:33 +00001478 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001479 return 1
1480
1481 # This rev-list syntax means "show all commits not in my branch that
1482 # are in base_branch".
1483 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1484 base_branch]).splitlines()
1485 if upstream_commits:
1486 print ('Base branch "%s" has %d commits '
1487 'not in this branch.' % (base_branch, len(upstream_commits)))
1488 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1489 return 1
1490
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001491 # This is the revision `svn dcommit` will commit on top of.
1492 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1493 '--pretty=format:%H'])
1494
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001495 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001496 # If the base_head is a submodule merge commit, the first parent of the
1497 # base_head should be a git-svn commit, which is what we're interested in.
1498 base_svn_head = base_branch
1499 if base_has_submodules:
1500 base_svn_head += '^1'
1501
1502 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001503 if extra_commits:
1504 print ('This branch has %d additional commits not upstreamed yet.'
1505 % len(extra_commits.splitlines()))
1506 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1507 'before attempting to %s.' % (base_branch, cmd))
1508 return 1
1509
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001510 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001511 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001512 author = None
1513 if options.contributor:
1514 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001515 hook_results = cl.RunHook(
1516 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001517 may_prompt=not options.force,
1518 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001519 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001520 if not hook_results.should_continue():
1521 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001522
1523 if cmd == 'dcommit':
1524 # Check the tree status if the tree status URL is set.
1525 status = GetTreeStatus()
1526 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001527 print('The tree is closed. Please wait for it to reopen. Use '
1528 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001529 return 1
1530 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001531 print('Unable to determine tree status. Please verify manually and '
1532 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001533 else:
1534 breakpad.SendStack(
1535 'GitClHooksBypassedCommit',
1536 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001537 (cl.GetRietveldServer(), cl.GetIssue()),
1538 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001539
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001540 change_desc = ChangeDescription(options.message)
1541 if not change_desc.description and cl.GetIssue():
1542 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001543
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001544 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001545 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001546 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001547 else:
1548 print 'No description set.'
1549 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1550 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001551
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001552 # Keep a separate copy for the commit message, because the commit message
1553 # contains the link to the Rietveld issue, while the Rietveld message contains
1554 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001555 # Keep a separate copy for the commit message.
1556 if cl.GetIssue():
1557 change_desc.update_reviewers(cl.GetApprovingReviewers(cl.GetIssue()))
1558
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001559 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001560 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001561 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001562 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001563 commit_desc.append_footer('Patch from %s.' % options.contributor)
1564
1565 print 'Description:', repr(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001566
1567 branches = [base_branch, cl.GetBranchRef()]
1568 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001569 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001570 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001571
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001572 # We want to squash all this branch's commits into one commit with the proper
1573 # description. We do this by doing a "reset --soft" to the base branch (which
1574 # keeps the working copy the same), then dcommitting that. If origin/master
1575 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1576 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001577 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001578 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1579 # Delete the branches if they exist.
1580 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1581 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1582 result = RunGitWithCode(showref_cmd)
1583 if result[0] == 0:
1584 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001585
1586 # We might be in a directory that's present in this branch but not in the
1587 # trunk. Move up to the top of the tree so that git commands that expect a
1588 # valid CWD won't fail after we check out the merge branch.
1589 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1590 if rel_base_path:
1591 os.chdir(rel_base_path)
1592
1593 # Stuff our change into the merge branch.
1594 # We wrap in a try...finally block so if anything goes wrong,
1595 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001596 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001597 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001598 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1599 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001600 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001601 RunGit(
1602 [
1603 'commit', '--author', options.contributor,
1604 '-m', commit_desc.description,
1605 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001606 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001607 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001608 if base_has_submodules:
1609 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1610 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1611 RunGit(['checkout', CHERRY_PICK_BRANCH])
1612 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001613 if cmd == 'push':
1614 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001615 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001616 retcode, output = RunGitWithCode(
1617 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1618 logging.debug(output)
1619 else:
1620 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001621 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001622 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001623 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001624 finally:
1625 # And then swap back to the original branch and clean up.
1626 RunGit(['checkout', '-q', cl.GetBranch()])
1627 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001628 if base_has_submodules:
1629 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001630
1631 if cl.GetIssue():
1632 if cmd == 'dcommit' and 'Committed r' in output:
1633 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1634 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001635 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1636 for l in output.splitlines(False))
1637 match = filter(None, match)
1638 if len(match) != 1:
1639 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1640 output)
1641 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001642 else:
1643 return 1
1644 viewvc_url = settings.GetViewVCUrl()
1645 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001646 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001647 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001648 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001649 print ('Closing issue '
1650 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001651 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001652 cl.CloseIssue()
iannucci@chromium.org16b51402013-02-17 05:33:36 +00001653 props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False)
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001654 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001655 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001656 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1657 cl.RpcServer().add_comment(cl.GetIssue(), comment)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001658 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001659
1660 if retcode == 0:
1661 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1662 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001663 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001664
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001665 return 0
1666
1667
1668@usage('[upstream branch to apply against]')
1669def CMDdcommit(parser, args):
1670 """commit the current changelist via git-svn"""
1671 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001672 message = """This doesn't appear to be an SVN repository.
1673If your project has a git mirror with an upstream SVN master, you probably need
1674to run 'git svn init', see your project's git mirror documentation.
1675If your project has a true writeable upstream repository, you probably want
1676to run 'git cl push' instead.
1677Choose wisely, if you get this wrong, your commit might appear to succeed but
1678will instead be silently ignored."""
1679 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001680 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001681 return SendUpstream(parser, args, 'dcommit')
1682
1683
1684@usage('[upstream branch to apply against]')
1685def CMDpush(parser, args):
1686 """commit the current changelist via git"""
1687 if settings.GetIsGitSvn():
1688 print('This appears to be an SVN repository.')
1689 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001690 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001691 return SendUpstream(parser, args, 'push')
1692
1693
1694@usage('<patch url or issue id>')
1695def CMDpatch(parser, args):
1696 """patch in a code review"""
1697 parser.add_option('-b', dest='newbranch',
1698 help='create a new branch off trunk for the patch')
1699 parser.add_option('-f', action='store_true', dest='force',
1700 help='with -b, clobber any existing branch')
1701 parser.add_option('--reject', action='store_true', dest='reject',
tapted@chromium.org0bdc2652013-06-07 23:47:05 +00001702 help='allow failed patches and spew .rej files')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001703 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1704 help="don't commit after patch applies")
1705 (options, args) = parser.parse_args(args)
1706 if len(args) != 1:
1707 parser.print_help()
1708 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001709 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001710
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001711 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001712 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001713
maruel@chromium.org52424302012-08-29 15:14:30 +00001714 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001715 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001716 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001717 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001718 patchset = cl.GetMostRecentPatchset(issue)
1719 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001720 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001721 # Assume it's a URL to the patch. Default to https.
1722 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001723 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001724 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001725 DieWithError('Must pass an issue ID or full URL for '
1726 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001727 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001728 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001729 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001730
1731 if options.newbranch:
1732 if options.force:
1733 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001734 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001735 RunGit(['checkout', '-b', options.newbranch,
1736 Changelist().GetUpstreamBranch()])
1737
1738 # Switch up to the top-level directory, if necessary, in preparation for
1739 # applying the patch.
1740 top = RunGit(['rev-parse', '--show-cdup']).strip()
1741 if top:
1742 os.chdir(top)
1743
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001744 # Git patches have a/ at the beginning of source paths. We strip that out
1745 # with a sed script rather than the -p flag to patch so we can feed either
1746 # Git or svn-style patches into the same apply command.
1747 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001748 try:
1749 patch_data = subprocess2.check_output(
1750 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1751 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001752 DieWithError('Git patch mungling failed.')
1753 logging.info(patch_data)
1754 # We use "git apply" to apply the patch instead of "patch" so that we can
1755 # pick up file adds.
1756 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001757 cmd = ['git', '--no-pager', 'apply', '--index', '-p0']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001758 if options.reject:
1759 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001760 try:
1761 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1762 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001763 DieWithError('Failed to apply the patch')
1764
1765 # If we had an issue, commit the current state and register the issue.
1766 if not options.nocommit:
1767 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1768 cl = Changelist()
1769 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001770 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001771 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001772 else:
1773 print "Patch applied to index."
1774 return 0
1775
1776
1777def CMDrebase(parser, args):
1778 """rebase current branch on top of svn repo"""
1779 # Provide a wrapper for git svn rebase to help avoid accidental
1780 # git svn dcommit.
1781 # It's the only command that doesn't use parser at all since we just defer
1782 # execution to git-svn.
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001783 return subprocess2.call(['git', '--no-pager', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001784
1785
1786def GetTreeStatus():
1787 """Fetches the tree status and returns either 'open', 'closed',
1788 'unknown' or 'unset'."""
1789 url = settings.GetTreeStatusUrl(error_ok=True)
1790 if url:
1791 status = urllib2.urlopen(url).read().lower()
1792 if status.find('closed') != -1 or status == '0':
1793 return 'closed'
1794 elif status.find('open') != -1 or status == '1':
1795 return 'open'
1796 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001797 return 'unset'
1798
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001799
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001800def GetTreeStatusReason():
1801 """Fetches the tree status from a json url and returns the message
1802 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001803 url = settings.GetTreeStatusUrl()
1804 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001805 connection = urllib2.urlopen(json_url)
1806 status = json.loads(connection.read())
1807 connection.close()
1808 return status['message']
1809
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001810
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001811def CMDtree(parser, args):
1812 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001813 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001814 status = GetTreeStatus()
1815 if 'unset' == status:
1816 print 'You must configure your tree status URL by running "git cl config".'
1817 return 2
1818
1819 print "The tree is %s" % status
1820 print
1821 print GetTreeStatusReason()
1822 if status != 'open':
1823 return 1
1824 return 0
1825
1826
maruel@chromium.org15192402012-09-06 12:38:29 +00001827def CMDtry(parser, args):
1828 """Triggers a try job through Rietveld."""
1829 group = optparse.OptionGroup(parser, "Try job options")
1830 group.add_option(
1831 "-b", "--bot", action="append",
1832 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1833 "times to specify multiple builders. ex: "
1834 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1835 "the try server waterfall for the builders name and the tests "
1836 "available. Can also be used to specify gtest_filter, e.g. "
1837 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1838 group.add_option(
1839 "-r", "--revision",
1840 help="Revision to use for the try job; default: the "
1841 "revision will be determined by the try server; see "
1842 "its waterfall for more info")
1843 group.add_option(
1844 "-c", "--clobber", action="store_true", default=False,
1845 help="Force a clobber before building; e.g. don't do an "
1846 "incremental build")
1847 group.add_option(
1848 "--project",
1849 help="Override which project to use. Projects are defined "
1850 "server-side to define what default bot set to use")
1851 group.add_option(
1852 "-t", "--testfilter", action="append", default=[],
1853 help=("Apply a testfilter to all the selected builders. Unless the "
1854 "builders configurations are similar, use multiple "
1855 "--bot <builder>:<test> arguments."))
1856 group.add_option(
1857 "-n", "--name", help="Try job name; default to current branch name")
1858 parser.add_option_group(group)
1859 options, args = parser.parse_args(args)
1860
1861 if args:
1862 parser.error('Unknown arguments: %s' % args)
1863
1864 cl = Changelist()
1865 if not cl.GetIssue():
1866 parser.error('Need to upload first')
1867
1868 if not options.name:
1869 options.name = cl.GetBranch()
1870
1871 # Process --bot and --testfilter.
1872 if not options.bot:
1873 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001874 change = cl.GetChange(
1875 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1876 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001877 options.bot = presubmit_support.DoGetTrySlaves(
1878 change,
1879 change.LocalPaths(),
1880 settings.GetRoot(),
1881 None,
1882 None,
1883 options.verbose,
1884 sys.stdout)
1885 if not options.bot:
1886 parser.error('No default try builder to try, use --bot')
1887
1888 builders_and_tests = {}
1889 for bot in options.bot:
1890 if ':' in bot:
1891 builder, tests = bot.split(':', 1)
1892 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1893 elif ',' in bot:
1894 parser.error('Specify one bot per --bot flag')
1895 else:
1896 builders_and_tests.setdefault(bot, []).append('defaulttests')
1897
1898 if options.testfilter:
1899 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1900 builders_and_tests = dict(
1901 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1902 if t != ['compile'])
1903
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001904 if any('triggered' in b for b in builders_and_tests):
1905 print >> sys.stderr, (
1906 'ERROR You are trying to send a job to a triggered bot. This type of'
1907 ' bot requires an\ninitial job from a parent (usually a builder). '
1908 'Instead send your job to the parent.\n'
1909 'Bot list: %s' % builders_and_tests)
1910 return 1
1911
maruel@chromium.org15192402012-09-06 12:38:29 +00001912 patchset = cl.GetPatchset()
1913 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001914 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001915
1916 cl.RpcServer().trigger_try_jobs(
1917 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1918 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001919 print('Tried jobs on:')
1920 length = max(len(builder) for builder in builders_and_tests)
1921 for builder in sorted(builders_and_tests):
1922 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001923 return 0
1924
1925
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001926@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001927def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001928 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001929 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001930 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001931 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001932 return 0
1933
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001934 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001935 if args:
1936 # One arg means set upstream branch.
1937 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1938 cl = Changelist()
1939 print "Upstream branch set to " + cl.GetUpstreamBranch()
1940 else:
1941 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001942 return 0
1943
1944
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001945def CMDset_commit(parser, args):
1946 """set the commit bit"""
1947 _, args = parser.parse_args(args)
1948 if args:
1949 parser.error('Unrecognized args: %s' % ' '.join(args))
1950 cl = Changelist()
1951 cl.SetFlag('commit', '1')
1952 return 0
1953
1954
groby@chromium.org411034a2013-02-26 15:12:01 +00001955def CMDset_close(parser, args):
1956 """close the issue"""
1957 _, args = parser.parse_args(args)
1958 if args:
1959 parser.error('Unrecognized args: %s' % ' '.join(args))
1960 cl = Changelist()
1961 # Ensure there actually is an issue to close.
1962 cl.GetDescription()
1963 cl.CloseIssue()
1964 return 0
1965
1966
agable@chromium.orgfab8f822013-05-06 17:43:09 +00001967def CMDformat(parser, args):
1968 """run clang-format on the diff"""
1969 CLANG_EXTS = ['.cc', '.cpp', '.h']
1970 parser.add_option('--full', action='store_true', default=False)
1971 opts, args = parser.parse_args(args)
1972 if args:
1973 parser.error('Unrecognized args: %s' % ' '.join(args))
1974
digit@chromium.org29e47272013-05-17 17:01:46 +00001975 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00001976 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00001977 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00001978 # Only list the names of modified files.
1979 diff_cmd.append('--name-only')
1980 else:
1981 # Only generate context-less patches.
1982 diff_cmd.append('-U0')
1983
1984 # Grab the merge-base commit, i.e. the upstream commit of the current
1985 # branch when it was created or the last time it was rebased. This is
1986 # to cover the case where the user may have called "git fetch origin",
1987 # moving the origin branch to a newer commit, but hasn't rebased yet.
1988 upstream_commit = None
1989 cl = Changelist()
1990 upstream_branch = cl.GetUpstreamBranch()
1991 if upstream_branch:
1992 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
1993 upstream_commit = upstream_commit.strip()
1994
1995 if not upstream_commit:
1996 DieWithError('Could not find base commit for this branch. '
1997 'Are you in detached state?')
1998
1999 diff_cmd.append(upstream_commit)
2000
2001 # Handle source file filtering.
2002 diff_cmd.append('--')
2003 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2004 diff_output = RunGit(diff_cmd)
2005
2006 if opts.full:
2007 # diff_output is a list of files to send to clang-format.
2008 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002009 if not files:
2010 print "Nothing to format."
2011 return 0
digit@chromium.org29e47272013-05-17 17:01:46 +00002012 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002013 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002014 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002015 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2016 'clang-format-diff.py')
2017 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002018 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
2019 cmd = [sys.executable, cfd_path, '-style', 'Chromium']
2020 RunCommand(cmd, stdin=diff_output)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002021
2022 return 0
2023
2024
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002025### Glue code for subcommand handling.
2026
2027
2028def Commands():
2029 """Returns a dict of command and their handling function."""
2030 module = sys.modules[__name__]
2031 cmds = (fn[3:] for fn in dir(module) if fn.startswith('CMD'))
2032 return dict((cmd, getattr(module, 'CMD' + cmd)) for cmd in cmds)
2033
2034
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002035def Command(name):
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002036 """Retrieves the function to handle a command."""
2037 commands = Commands()
2038 if name in commands:
2039 return commands[name]
2040
2041 # Try to be smart and look if there's something similar.
2042 commands_with_prefix = [c for c in commands if c.startswith(name)]
2043 if len(commands_with_prefix) == 1:
2044 return commands[commands_with_prefix[0]]
2045
2046 # A #closeenough approximation of levenshtein distance.
2047 def close_enough(a, b):
2048 return difflib.SequenceMatcher(a=a, b=b).ratio()
2049
2050 hamming_commands = sorted(
2051 ((close_enough(c, name), c) for c in commands),
2052 reverse=True)
2053 if (hamming_commands[0][0] - hamming_commands[1][0]) < 0.3:
2054 # Too ambiguous.
2055 return
2056
2057 if hamming_commands[0][0] < 0.8:
2058 # Not similar enough. Don't be a fool and run a random command.
2059 return
2060
2061 return commands[hamming_commands[0][1]]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002062
2063
2064def CMDhelp(parser, args):
2065 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002066 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002067 if len(args) == 1:
2068 return main(args + ['--help'])
2069 parser.print_help()
2070 return 0
2071
2072
2073def GenUsage(parser, command):
2074 """Modify an OptParse object with the function's documentation."""
2075 obj = Command(command)
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002076 # Get back the real command name in case Command() guess the actual command
2077 # name.
2078 command = obj.__name__[3:]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002079 more = getattr(obj, 'usage_more', '')
2080 if command == 'help':
2081 command = '<command>'
2082 else:
2083 # OptParser.description prefer nicely non-formatted strings.
2084 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
2085 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
2086
2087
2088def main(argv):
2089 """Doesn't parse the arguments here, just find the right subcommand to
2090 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002091 if sys.hexversion < 0x02060000:
2092 print >> sys.stderr, (
2093 '\nYour python version %s is unsupported, please upgrade.\n' %
2094 sys.version.split(' ', 1)[0])
2095 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002096
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002097 # Reload settings.
2098 global settings
2099 settings = Settings()
2100
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002101 # Do it late so all commands are listed.
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002102 commands = Commands()
2103 length = max(len(c) for c in commands)
2104 docs = sorted(
2105 (name, handler.__doc__.split('\n')[0].strip())
2106 for name, handler in commands.iteritems())
2107 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join(
2108 ' %-*s %s' % (length, name, doc) for name, doc in docs))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002109
2110 # Create the option parse and add --verbose support.
2111 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002112 parser.add_option(
2113 '-v', '--verbose', action='count', default=0,
2114 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002115 old_parser_args = parser.parse_args
2116 def Parse(args):
2117 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002118 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002119 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002120 elif options.verbose:
2121 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002122 else:
2123 logging.basicConfig(level=logging.WARNING)
2124 return options, args
2125 parser.parse_args = Parse
2126
2127 if argv:
2128 command = Command(argv[0])
2129 if command:
2130 # "fix" the usage and the description now that we know the subcommand.
2131 GenUsage(parser, argv[0])
2132 try:
2133 return command(parser, argv[1:])
2134 except urllib2.HTTPError, e:
2135 if e.code != 500:
2136 raise
2137 DieWithError(
2138 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2139 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
2140
2141 # Not a known command. Default to help.
2142 GenUsage(parser, 'help')
2143 return CMDhelp(parser, argv)
2144
2145
2146if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002147 # These affect sys.stdout so do it outside of main() to simplify mocks in
2148 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002149 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002150 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002151 sys.exit(main(sys.argv[1:]))