blob: 9ea856fa41aea869429693586d08e31f4ef0e05f [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
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000011from distutils.version import LooseVersion
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000012import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000013import logging
14import optparse
15import os
16import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000017import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import textwrap
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000021import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022
23try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000024 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025except ImportError:
26 pass
27
maruel@chromium.org2a74d372011-03-29 19:05:50 +000028
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000029from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000030from third_party import upload
31import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000032import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000033import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000034import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000035import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000036import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000037import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000038import watchlists
39
40
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000041DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000042POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000043DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000044GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000045CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000046
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000047# Shortcut since it quickly becomes redundant.
48Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000049
maruel@chromium.orgddd59412011-11-30 14:20:38 +000050# Initialized in main()
51settings = None
52
53
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000054def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000055 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000056 sys.exit(1)
57
58
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000059def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000060 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000061 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000062 except subprocess2.CalledProcessError as e:
63 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000064 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000065 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066 'Command "%s" failed.\n%s' % (
67 ' '.join(args), error_message or e.stdout or ''))
68 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069
70
71def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000072 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000073 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
75
76def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000077 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000078 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +000079 env = os.environ.copy()
80 # 'cat' is a magical git string that disables pagers on all platforms.
81 env['GIT_PAGER'] = 'cat'
82 out, code = subprocess2.communicate(['git'] + args,
83 env=env,
bratell@opera.comf267b0e2013-05-02 09:11:43 +000084 stdout=subprocess2.PIPE)
szager@chromium.org9bb85e22012-06-13 20:28:23 +000085 return code, out[0]
86 except ValueError:
87 # When the subprocess fails, it returns None. That triggers a ValueError
88 # when trying to unpack the return value into (out, code).
89 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000090
91
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000092def IsGitVersionAtLeast(min_version):
93 PREFIX='git version '
94 version = RunGit(['--version']).strip()
95 return (version.startswith(PREFIX) and
96 LooseVersion(version[len(PREFIX):]) >= LooseVersion(min_version))
97
98
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000099def usage(more):
100 def hook(fn):
101 fn.usage_more = more
102 return fn
103 return hook
104
105
maruel@chromium.org90541732011-04-01 17:54:18 +0000106def ask_for_data(prompt):
107 try:
108 return raw_input(prompt)
109 except KeyboardInterrupt:
110 # Hide the exception.
111 sys.exit(1)
112
113
iannucci@chromium.org79540052012-10-19 23:15:26 +0000114def git_set_branch_value(key, value):
115 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000116 if not branch:
117 return
118
119 cmd = ['config']
120 if isinstance(value, int):
121 cmd.append('--int')
122 git_key = 'branch.%s.%s' % (branch, key)
123 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000124
125
126def git_get_branch_default(key, default):
127 branch = Changelist().GetBranch()
128 if branch:
129 git_key = 'branch.%s.%s' % (branch, key)
130 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
131 try:
132 return int(stdout.strip())
133 except ValueError:
134 pass
135 return default
136
137
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000138def add_git_similarity(parser):
139 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000140 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000141 help='Sets the percentage that a pair of files need to match in order to'
142 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000143 parser.add_option(
144 '--find-copies', action='store_true',
145 help='Allows git to look for copies.')
146 parser.add_option(
147 '--no-find-copies', action='store_false', dest='find_copies',
148 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000149
150 old_parser_args = parser.parse_args
151 def Parse(args):
152 options, args = old_parser_args(args)
153
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000154 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000155 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000156 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000157 print('Note: Saving similarity of %d%% in git config.'
158 % options.similarity)
159 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000160
iannucci@chromium.org79540052012-10-19 23:15:26 +0000161 options.similarity = max(0, min(options.similarity, 100))
162
163 if options.find_copies is None:
164 options.find_copies = bool(
165 git_get_branch_default('git-find-copies', True))
166 else:
167 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000168
169 print('Using %d%% similarity for rename/copy detection. '
170 'Override with --similarity.' % options.similarity)
171
172 return options, args
173 parser.parse_args = Parse
174
175
ukai@chromium.org259e4682012-10-25 07:36:33 +0000176def is_dirty_git_tree(cmd):
177 # Make sure index is up-to-date before running diff-index.
178 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
179 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
180 if dirty:
181 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
182 print 'Uncommitted files: (git diff-index --name-status HEAD)'
183 print dirty[:4096]
184 if len(dirty) > 4096:
185 print '... (run "git diff-index --name-status HEAD" to see full output).'
186 return True
187 return False
188
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000189
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000190def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
191 """Return the corresponding git ref if |base_url| together with |glob_spec|
192 matches the full |url|.
193
194 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
195 """
196 fetch_suburl, as_ref = glob_spec.split(':')
197 if allow_wildcards:
198 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
199 if glob_match:
200 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
201 # "branches/{472,597,648}/src:refs/remotes/svn/*".
202 branch_re = re.escape(base_url)
203 if glob_match.group(1):
204 branch_re += '/' + re.escape(glob_match.group(1))
205 wildcard = glob_match.group(2)
206 if wildcard == '*':
207 branch_re += '([^/]*)'
208 else:
209 # Escape and replace surrounding braces with parentheses and commas
210 # with pipe symbols.
211 wildcard = re.escape(wildcard)
212 wildcard = re.sub('^\\\\{', '(', wildcard)
213 wildcard = re.sub('\\\\,', '|', wildcard)
214 wildcard = re.sub('\\\\}$', ')', wildcard)
215 branch_re += wildcard
216 if glob_match.group(3):
217 branch_re += re.escape(glob_match.group(3))
218 match = re.match(branch_re, url)
219 if match:
220 return re.sub('\*$', match.group(1), as_ref)
221
222 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
223 if fetch_suburl:
224 full_url = base_url + '/' + fetch_suburl
225 else:
226 full_url = base_url
227 if full_url == url:
228 return as_ref
229 return None
230
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000231
iannucci@chromium.org79540052012-10-19 23:15:26 +0000232def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000233 """Prints statistics about the change to the user."""
234 # --no-ext-diff is broken in some versions of Git, so try to work around
235 # this by overriding the environment (but there is still a problem if the
236 # git config key "diff.external" is used).
237 env = os.environ.copy()
238 if 'GIT_EXTERNAL_DIFF' in env:
239 del env['GIT_EXTERNAL_DIFF']
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000240 # 'cat' is a magical git string that disables pagers on all platforms.
241 env['GIT_PAGER'] = 'cat'
iannucci@chromium.org79540052012-10-19 23:15:26 +0000242
243 if find_copies:
244 similarity_options = ['--find-copies-harder', '-l100000',
245 '-C%s' % similarity]
246 else:
247 similarity_options = ['-M%s' % similarity]
248
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000249 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000250 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000251 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000252 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000253
254
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000255class Settings(object):
256 def __init__(self):
257 self.default_server = None
258 self.cc = None
259 self.root = None
260 self.is_git_svn = None
261 self.svn_branch = None
262 self.tree_status_url = None
263 self.viewvc_url = None
264 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000265 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000266 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000267
268 def LazyUpdateIfNeeded(self):
269 """Updates the settings from a codereview.settings file, if available."""
270 if not self.updated:
271 cr_settings_file = FindCodereviewSettingsFile()
272 if cr_settings_file:
273 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000274 self.updated = True
275 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000276 self.updated = True
277
278 def GetDefaultServerUrl(self, error_ok=False):
279 if not self.default_server:
280 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000281 self.default_server = gclient_utils.UpgradeToHttps(
282 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000283 if error_ok:
284 return self.default_server
285 if not self.default_server:
286 error_message = ('Could not find settings file. You must configure '
287 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000288 self.default_server = gclient_utils.UpgradeToHttps(
289 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000290 return self.default_server
291
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000292 def GetRoot(self):
293 if not self.root:
294 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
295 return self.root
296
297 def GetIsGitSvn(self):
298 """Return true if this repo looks like it's using git-svn."""
299 if self.is_git_svn is None:
300 # If you have any "svn-remote.*" config keys, we think you're using svn.
301 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000302 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000303 return self.is_git_svn
304
305 def GetSVNBranch(self):
306 if self.svn_branch is None:
307 if not self.GetIsGitSvn():
308 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
309
310 # Try to figure out which remote branch we're based on.
311 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000312 # 1) iterate through our branch history and find the svn URL.
313 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000314
315 # regexp matching the git-svn line that contains the URL.
316 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
317
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000318 env = os.environ.copy()
319 # 'cat' is a magical git string that disables pagers on all platforms.
320 env['GIT_PAGER'] = 'cat'
321
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000322 # We don't want to go through all of history, so read a line from the
323 # pipe at a time.
324 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000325 cmd = ['git', 'log', '-100', '--pretty=medium']
326 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE, env=env)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000327 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000328 for line in proc.stdout:
329 match = git_svn_re.match(line)
330 if match:
331 url = match.group(1)
332 proc.stdout.close() # Cut pipe.
333 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000334
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000335 if url:
336 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
337 remotes = RunGit(['config', '--get-regexp',
338 r'^svn-remote\..*\.url']).splitlines()
339 for remote in remotes:
340 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000341 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000342 remote = match.group(1)
343 base_url = match.group(2)
344 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000345 ['config', 'svn-remote.%s.fetch' % remote],
346 error_ok=True).strip()
347 if fetch_spec:
348 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
349 if self.svn_branch:
350 break
351 branch_spec = RunGit(
352 ['config', 'svn-remote.%s.branches' % remote],
353 error_ok=True).strip()
354 if branch_spec:
355 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
356 if self.svn_branch:
357 break
358 tag_spec = RunGit(
359 ['config', 'svn-remote.%s.tags' % remote],
360 error_ok=True).strip()
361 if tag_spec:
362 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
363 if self.svn_branch:
364 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000365
366 if not self.svn_branch:
367 DieWithError('Can\'t guess svn branch -- try specifying it on the '
368 'command line')
369
370 return self.svn_branch
371
372 def GetTreeStatusUrl(self, error_ok=False):
373 if not self.tree_status_url:
374 error_message = ('You must configure your tree status URL by running '
375 '"git cl config".')
376 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
377 error_ok=error_ok,
378 error_message=error_message)
379 return self.tree_status_url
380
381 def GetViewVCUrl(self):
382 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000383 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000384 return self.viewvc_url
385
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000386 def GetDefaultCCList(self):
387 return self._GetConfig('rietveld.cc', error_ok=True)
388
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000389 def GetDefaultPrivateFlag(self):
390 return self._GetConfig('rietveld.private', error_ok=True)
391
ukai@chromium.orge8077812012-02-03 03:41:46 +0000392 def GetIsGerrit(self):
393 """Return true if this repo is assosiated with gerrit code review system."""
394 if self.is_gerrit is None:
395 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
396 return self.is_gerrit
397
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000398 def GetGitEditor(self):
399 """Return the editor specified in the git config, or None if none is."""
400 if self.git_editor is None:
401 self.git_editor = self._GetConfig('core.editor', error_ok=True)
402 return self.git_editor or None
403
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000404 def _GetConfig(self, param, **kwargs):
405 self.LazyUpdateIfNeeded()
406 return RunGit(['config', param], **kwargs).strip()
407
408
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000409def ShortBranchName(branch):
410 """Convert a name like 'refs/heads/foo' to just 'foo'."""
411 return branch.replace('refs/heads/', '')
412
413
414class Changelist(object):
415 def __init__(self, branchref=None):
416 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000417 global settings
418 if not settings:
419 # Happens when git_cl.py is used as a utility library.
420 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000421 settings.GetDefaultServerUrl()
422 self.branchref = branchref
423 if self.branchref:
424 self.branch = ShortBranchName(self.branchref)
425 else:
426 self.branch = None
427 self.rietveld_server = None
428 self.upstream_branch = None
429 self.has_issue = False
430 self.issue = None
431 self.has_description = False
432 self.description = None
433 self.has_patchset = False
434 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000435 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000436 self.cc = None
437 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000438 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000439
440 def GetCCList(self):
441 """Return the users cc'd on this CL.
442
443 Return is a string suitable for passing to gcl with the --cc flag.
444 """
445 if self.cc is None:
446 base_cc = settings .GetDefaultCCList()
447 more_cc = ','.join(self.watchers)
448 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
449 return self.cc
450
451 def SetWatchers(self, watchers):
452 """Set the list of email addresses that should be cc'd based on the changed
453 files in this CL.
454 """
455 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000456
457 def GetBranch(self):
458 """Returns the short branch name, e.g. 'master'."""
459 if not self.branch:
460 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
461 self.branch = ShortBranchName(self.branchref)
462 return self.branch
463
464 def GetBranchRef(self):
465 """Returns the full branch name, e.g. 'refs/heads/master'."""
466 self.GetBranch() # Poke the lazy loader.
467 return self.branchref
468
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000469 @staticmethod
470 def FetchUpstreamTuple(branch):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000471 """Returns a tuple containg remote and remote ref,
472 e.g. 'origin', 'refs/heads/master'
473 """
474 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000475 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
476 error_ok=True).strip()
477 if upstream_branch:
478 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
479 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000480 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
481 error_ok=True).strip()
482 if upstream_branch:
483 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000484 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000485 # Fall back on trying a git-svn upstream branch.
486 if settings.GetIsGitSvn():
487 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000488 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000489 # Else, try to guess the origin remote.
490 remote_branches = RunGit(['branch', '-r']).split()
491 if 'origin/master' in remote_branches:
492 # Fall back on origin/master if it exits.
493 remote = 'origin'
494 upstream_branch = 'refs/heads/master'
495 elif 'origin/trunk' in remote_branches:
496 # Fall back on origin/trunk if it exists. Generally a shared
497 # git-svn clone
498 remote = 'origin'
499 upstream_branch = 'refs/heads/trunk'
500 else:
501 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000502Either pass complete "git diff"-style arguments, like
503 git cl upload origin/master
504or verify this branch is set up to track another (via the --track argument to
505"git checkout -b ...").""")
506
507 return remote, upstream_branch
508
509 def GetUpstreamBranch(self):
510 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000511 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000512 if remote is not '.':
513 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
514 self.upstream_branch = upstream_branch
515 return self.upstream_branch
516
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000517 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000518 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000519 remote, branch = None, self.GetBranch()
520 seen_branches = set()
521 while branch not in seen_branches:
522 seen_branches.add(branch)
523 remote, branch = self.FetchUpstreamTuple(branch)
524 branch = ShortBranchName(branch)
525 if remote != '.' or branch.startswith('refs/remotes'):
526 break
527 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000528 remotes = RunGit(['remote'], error_ok=True).split()
529 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000530 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000531 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000532 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000533 logging.warning('Could not determine which remote this change is '
534 'associated with, so defaulting to "%s". This may '
535 'not be what you want. You may prevent this message '
536 'by running "git svn info" as documented here: %s',
537 self._remote,
538 GIT_INSTRUCTIONS_URL)
539 else:
540 logging.warn('Could not determine which remote this change is '
541 'associated with. You may prevent this message by '
542 'running "git svn info" as documented here: %s',
543 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000544 branch = 'HEAD'
545 if branch.startswith('refs/remotes'):
546 self._remote = (remote, branch)
547 else:
548 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000549 return self._remote
550
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000551 def GitSanityChecks(self, upstream_git_obj):
552 """Checks git repo status and ensures diff is from local commits."""
553
554 # Verify the commit we're diffing against is in our current branch.
555 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
556 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
557 if upstream_sha != common_ancestor:
558 print >> sys.stderr, (
559 'ERROR: %s is not in the current branch. You may need to rebase '
560 'your tracking branch' % upstream_sha)
561 return False
562
563 # List the commits inside the diff, and verify they are all local.
564 commits_in_diff = RunGit(
565 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
566 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
567 remote_branch = remote_branch.strip()
568 if code != 0:
569 _, remote_branch = self.GetRemoteBranch()
570
571 commits_in_remote = RunGit(
572 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
573
574 common_commits = set(commits_in_diff) & set(commits_in_remote)
575 if common_commits:
576 print >> sys.stderr, (
577 'ERROR: Your diff contains %d commits already in %s.\n'
578 'Run "git log --oneline %s..HEAD" to get a list of commits in '
579 'the diff. If you are using a custom git flow, you can override'
580 ' the reference used for this check with "git config '
581 'gitcl.remotebranch <git-ref>".' % (
582 len(common_commits), remote_branch, upstream_git_obj))
583 return False
584 return True
585
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000586 def GetGitBaseUrlFromConfig(self):
587 """Return the configured base URL from branch.<branchname>.baseurl.
588
589 Returns None if it is not set.
590 """
591 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
592 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000593
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000594 def GetRemoteUrl(self):
595 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
596
597 Returns None if there is no remote.
598 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000599 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000600 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
601
602 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000603 """Returns the issue number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000604 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000605 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
606 if issue:
maruel@chromium.org52424302012-08-29 15:14:30 +0000607 self.issue = int(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 else:
609 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000610 self.has_issue = True
611 return self.issue
612
613 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000614 if not self.rietveld_server:
615 # If we're on a branch then get the server potentially associated
616 # with that branch.
617 if self.GetIssue():
618 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
619 ['config', self._RietveldServer()], error_ok=True).strip())
620 if not self.rietveld_server:
621 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000622 return self.rietveld_server
623
624 def GetIssueURL(self):
625 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000626 if not self.GetIssue():
627 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000628 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
629
630 def GetDescription(self, pretty=False):
631 if not self.has_description:
632 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000633 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000634 try:
635 self.description = self.RpcServer().get_description(issue).strip()
636 except urllib2.HTTPError, e:
637 if e.code == 404:
638 DieWithError(
639 ('\nWhile fetching the description for issue %d, received a '
640 '404 (not found)\n'
641 'error. It is likely that you deleted this '
642 'issue on the server. If this is the\n'
643 'case, please run\n\n'
644 ' git cl issue 0\n\n'
645 'to clear the association with the deleted issue. Then run '
646 'this command again.') % issue)
647 else:
648 DieWithError(
649 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000650 self.has_description = True
651 if pretty:
652 wrapper = textwrap.TextWrapper()
653 wrapper.initial_indent = wrapper.subsequent_indent = ' '
654 return wrapper.fill(self.description)
655 return self.description
656
657 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000658 """Returns the patchset number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000659 if not self.has_patchset:
660 patchset = RunGit(['config', self._PatchsetSetting()],
661 error_ok=True).strip()
662 if patchset:
maruel@chromium.org52424302012-08-29 15:14:30 +0000663 self.patchset = int(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000664 else:
665 self.patchset = None
666 self.has_patchset = True
667 return self.patchset
668
669 def SetPatchset(self, patchset):
670 """Set this branch's patchset. If patchset=0, clears the patchset."""
671 if patchset:
672 RunGit(['config', self._PatchsetSetting(), str(patchset)])
673 else:
674 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000675 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000676 self.has_patchset = False
677
binji@chromium.org0281f522012-09-14 13:37:59 +0000678 def GetMostRecentPatchset(self, issue):
679 return self.RpcServer().get_issue_properties(
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000680 int(issue), False)['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000681
682 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000683 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000684 '/download/issue%s_%s.diff' % (issue, patchset))
685
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000686 def GetApprovingReviewers(self, issue):
687 return get_approving_reviewers(
688 self.RpcServer().get_issue_properties(int(issue), True))
689
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000690 def SetIssue(self, issue):
691 """Set this branch's issue. If issue=0, clears the issue."""
692 if issue:
693 RunGit(['config', self._IssueSetting(), str(issue)])
694 if self.rietveld_server:
695 RunGit(['config', self._RietveldServer(), self.rietveld_server])
696 else:
697 RunGit(['config', '--unset', self._IssueSetting()])
698 self.SetPatchset(0)
699 self.has_issue = False
700
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000701 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000702 if not self.GitSanityChecks(upstream_branch):
703 DieWithError('\nGit sanity check failure')
704
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000705 env = os.environ.copy()
706 # 'cat' is a magical git string that disables pagers on all platforms.
707 env['GIT_PAGER'] = 'cat'
708
709 root = RunCommand(['git', 'rev-parse', '--show-cdup'], env=env).strip()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000710 if not root:
711 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000712 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000713
714 # We use the sha1 of HEAD as a name of this change.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000715 name = RunCommand(['git', 'rev-parse', 'HEAD'], env=env).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000716 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000717 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000718 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000719 except subprocess2.CalledProcessError:
720 DieWithError(
721 ('\nFailed to diff against upstream branch %s!\n\n'
722 'This branch probably doesn\'t exist anymore. To reset the\n'
723 'tracking branch, please run\n'
724 ' git branch --set-upstream %s trunk\n'
725 'replacing trunk with origin/master or the relevant branch') %
726 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000727
maruel@chromium.org52424302012-08-29 15:14:30 +0000728 issue = self.GetIssue()
729 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000730 if issue:
731 description = self.GetDescription()
732 else:
733 # If the change was never uploaded, use the log messages of all commits
734 # up to the branch point, as git cl upload will prefill the description
735 # with these log messages.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000736 description = RunCommand(['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000737 'log', '--pretty=format:%s%n%n%b',
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000738 '%s...' % (upstream_branch)],
739 env=env).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000740
741 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000742 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000743 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000744 name,
745 description,
746 absroot,
747 files,
748 issue,
749 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000750 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000751
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000752 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000753 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000754
755 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000756 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000757 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000758 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000759 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000760 except presubmit_support.PresubmitFailure, e:
761 DieWithError(
762 ('%s\nMaybe your depot_tools is out of date?\n'
763 'If all fails, contact maruel@') % e)
764
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000765 def UpdateDescription(self, description):
766 self.description = description
767 return self.RpcServer().update_description(
768 self.GetIssue(), self.description)
769
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000770 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000771 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000772 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000773
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000774 def SetFlag(self, flag, value):
775 """Patchset must match."""
776 if not self.GetPatchset():
777 DieWithError('The patchset needs to match. Send another patchset.')
778 try:
779 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000780 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000781 except urllib2.HTTPError, e:
782 if e.code == 404:
783 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
784 if e.code == 403:
785 DieWithError(
786 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
787 'match?') % (self.GetIssue(), self.GetPatchset()))
788 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000789
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000790 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000791 """Returns an upload.RpcServer() to access this review's rietveld instance.
792 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000793 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000794 self._rpc_server = rietveld.CachingRietveld(
795 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000796 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797
798 def _IssueSetting(self):
799 """Return the git setting that stores this change's issue."""
800 return 'branch.%s.rietveldissue' % self.GetBranch()
801
802 def _PatchsetSetting(self):
803 """Return the git setting that stores this change's most recent patchset."""
804 return 'branch.%s.rietveldpatchset' % self.GetBranch()
805
806 def _RietveldServer(self):
807 """Returns the git setting that stores this change's rietveld server."""
808 return 'branch.%s.rietveldserver' % self.GetBranch()
809
810
811def GetCodereviewSettingsInteractively():
812 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000813 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814 server = settings.GetDefaultServerUrl(error_ok=True)
815 prompt = 'Rietveld server (host[:port])'
816 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000817 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000818 if not server and not newserver:
819 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000820 if newserver:
821 newserver = gclient_utils.UpgradeToHttps(newserver)
822 if newserver != server:
823 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000825 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826 prompt = caption
827 if initial:
828 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000829 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000830 if new_val == 'x':
831 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000832 elif new_val:
833 if is_url:
834 new_val = gclient_utils.UpgradeToHttps(new_val)
835 if new_val != initial:
836 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000837
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000838 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000839 SetProperty(settings.GetDefaultPrivateFlag(),
840 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000842 'tree-status-url', False)
843 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000844
845 # TODO: configure a default branch to diff against, rather than this
846 # svn-based hackery.
847
848
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000849class ChangeDescription(object):
850 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000851 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000852
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000853 def __init__(self, description):
854 self._description = (description or '').strip()
855
856 @property
857 def description(self):
858 return self._description
859
860 def update_reviewers(self, reviewers):
861 """Rewrites the R=/TBR= line(s) as a single line."""
862 assert isinstance(reviewers, list), reviewers
863 if not reviewers:
864 return
865 regexp = re.compile(self.R_LINE, re.MULTILINE)
866 matches = list(regexp.finditer(self._description))
867 is_tbr = any(m.group(1) == 'TBR' for m in matches)
868 if len(matches) > 1:
869 # Erase all except the first one.
870 for i in xrange(len(matches) - 1, 0, -1):
871 self._description = (
872 self._description[:matches[i].start()] +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000873 self._description[matches[i].end():])
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000874
875 if is_tbr:
876 new_r_line = 'TBR=' + ', '.join(reviewers)
877 else:
878 new_r_line = 'R=' + ', '.join(reviewers)
879
880 if matches:
881 self._description = (
882 self._description[:matches[0].start()] + new_r_line +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000883 self._description[matches[0].end():]).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000884 else:
885 self.append_footer(new_r_line)
886
887 def prompt(self):
888 """Asks the user to update the description."""
889 self._description = (
890 '# Enter a description of the change.\n'
janx@chromium.org104b2db2013-04-18 12:58:40 +0000891 '# This will be displayed on the codereview site.\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000892 '# The first line will also be used as the subject of the review.\n'
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000893 '#--------------------This line is 72 characters long'
894 '--------------------\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000895 ) + self._description
896
897 if '\nBUG=' not in self._description:
898 self.append_footer('BUG=')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000899 content = gclient_utils.RunEditor(self._description, True,
900 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000901 if not content:
902 DieWithError('Running editor failed')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000903
904 # Strip off comments.
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000905 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000906 if not content:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000907 DieWithError('No CL description, aborting')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000908 self._description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000909
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000910 def append_footer(self, line):
911 # Adds a LF if the last line did not have 'FOO=BAR' or if the new one isn't.
912 if self._description:
913 if '\n' not in self._description:
914 self._description += '\n'
915 else:
916 last_line = self._description.rsplit('\n', 1)[1]
917 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
918 not presubmit_support.Change.TAG_LINE_RE.match(line)):
919 self._description += '\n'
920 self._description += '\n' + line
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000921
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000922 def get_reviewers(self):
923 """Retrieves the list of reviewers."""
924 regexp = re.compile(self.R_LINE, re.MULTILINE)
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000925 reviewers = [i.group(2).strip() for i in regexp.finditer(self._description)]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000926 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000927
928
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000929def get_approving_reviewers(props):
930 """Retrieves the reviewers that approved a CL from the issue properties with
931 messages.
932
933 Note that the list may contain reviewers that are not committer, thus are not
934 considered by the CQ.
935 """
936 return sorted(
937 set(
938 message['sender']
939 for message in props['messages']
940 if message['approval'] and message['sender'] in props['reviewers']
941 )
942 )
943
944
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000945def FindCodereviewSettingsFile(filename='codereview.settings'):
946 """Finds the given file starting in the cwd and going up.
947
948 Only looks up to the top of the repository unless an
949 'inherit-review-settings-ok' file exists in the root of the repository.
950 """
951 inherit_ok_file = 'inherit-review-settings-ok'
952 cwd = os.getcwd()
953 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
954 if os.path.isfile(os.path.join(root, inherit_ok_file)):
955 root = '/'
956 while True:
957 if filename in os.listdir(cwd):
958 if os.path.isfile(os.path.join(cwd, filename)):
959 return open(os.path.join(cwd, filename))
960 if cwd == root:
961 break
962 cwd = os.path.dirname(cwd)
963
964
965def LoadCodereviewSettingsFromFile(fileobj):
966 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000967 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000968
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000969 def SetProperty(name, setting, unset_error_ok=False):
970 fullname = 'rietveld.' + name
971 if setting in keyvals:
972 RunGit(['config', fullname, keyvals[setting]])
973 else:
974 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
975
976 SetProperty('server', 'CODE_REVIEW_SERVER')
977 # Only server setting is required. Other settings can be absent.
978 # In that case, we ignore errors raised during option deletion attempt.
979 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000980 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000981 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
982 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
983
ukai@chromium.orge8077812012-02-03 03:41:46 +0000984 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
985 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
986 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000987
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000988 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
989 #should be of the form
990 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
991 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
992 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
993 keyvals['ORIGIN_URL_CONFIG']])
994
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000995
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000996def urlretrieve(source, destination):
997 """urllib is broken for SSL connections via a proxy therefore we
998 can't use urllib.urlretrieve()."""
999 with open(destination, 'w') as f:
1000 f.write(urllib2.urlopen(source).read())
1001
1002
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001003def DownloadHooks(force):
1004 """downloads hooks
1005
1006 Args:
1007 force: True to update hooks. False to install hooks if not present.
1008 """
1009 if not settings.GetIsGerrit():
1010 return
1011 server_url = settings.GetDefaultServerUrl()
1012 src = '%s/tools/hooks/commit-msg' % server_url
1013 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1014 if not os.access(dst, os.X_OK):
1015 if os.path.exists(dst):
1016 if not force:
1017 return
1018 os.remove(dst)
1019 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001020 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001021 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1022 except Exception:
1023 if os.path.exists(dst):
1024 os.remove(dst)
1025 DieWithError('\nFailed to download hooks from %s' % src)
1026
1027
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001028@usage('[repo root containing codereview.settings]')
1029def CMDconfig(parser, args):
1030 """edit configuration for this tree"""
1031
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001032 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001033 if len(args) == 0:
1034 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001035 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001036 return 0
1037
1038 url = args[0]
1039 if not url.endswith('codereview.settings'):
1040 url = os.path.join(url, 'codereview.settings')
1041
1042 # Load code review settings and download hooks (if available).
1043 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001044 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001045 return 0
1046
1047
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001048def CMDbaseurl(parser, args):
1049 """get or set base-url for this branch"""
1050 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1051 branch = ShortBranchName(branchref)
1052 _, args = parser.parse_args(args)
1053 if not args:
1054 print("Current base-url:")
1055 return RunGit(['config', 'branch.%s.base-url' % branch],
1056 error_ok=False).strip()
1057 else:
1058 print("Setting base-url to %s" % args[0])
1059 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1060 error_ok=False).strip()
1061
1062
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001063def CMDstatus(parser, args):
1064 """show status of changelists"""
1065 parser.add_option('--field',
1066 help='print only specific field (desc|id|patch|url)')
1067 (options, args) = parser.parse_args(args)
1068
1069 # TODO: maybe make show_branches a flag if necessary.
1070 show_branches = not options.field
1071
1072 if show_branches:
1073 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1074 if branches:
rch@chromium.org92d67162012-04-02 20:10:35 +00001075 changes = (Changelist(branchref=b) for b in branches.splitlines())
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001076 branches = dict((cl.GetBranch(), cl.GetIssueURL()) for cl in changes)
rch@chromium.org92d67162012-04-02 20:10:35 +00001077 alignment = max(5, max(len(b) for b in branches))
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001078 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +00001079 for branch in sorted(branches):
hinoka@google.com55c05c92013-06-26 22:13:31 +00001080 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081
1082 cl = Changelist()
1083 if options.field:
1084 if options.field.startswith('desc'):
1085 print cl.GetDescription()
1086 elif options.field == 'id':
1087 issueid = cl.GetIssue()
1088 if issueid:
1089 print issueid
1090 elif options.field == 'patch':
1091 patchset = cl.GetPatchset()
1092 if patchset:
1093 print patchset
1094 elif options.field == 'url':
1095 url = cl.GetIssueURL()
1096 if url:
1097 print url
1098 else:
1099 print
1100 print 'Current branch:',
1101 if not cl.GetIssue():
1102 print 'no issue assigned.'
1103 return 0
1104 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +00001105 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106 print 'Issue description:'
1107 print cl.GetDescription(pretty=True)
1108 return 0
1109
1110
1111@usage('[issue_number]')
1112def CMDissue(parser, args):
1113 """Set or display the current code review issue number.
1114
1115 Pass issue number 0 to clear the current issue.
1116"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001117 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001118
1119 cl = Changelist()
1120 if len(args) > 0:
1121 try:
1122 issue = int(args[0])
1123 except ValueError:
1124 DieWithError('Pass a number to set the issue or none to list it.\n'
1125 'Maybe you want to run git cl status?')
1126 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001127 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128 return 0
1129
1130
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001131def CMDcomments(parser, args):
1132 """show review comments of the current changelist"""
1133 (_, args) = parser.parse_args(args)
1134 if args:
1135 parser.error('Unsupported argument: %s' % args)
1136
1137 cl = Changelist()
1138 if cl.GetIssue():
1139 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
1140 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001141 if message['disapproval']:
1142 color = Fore.RED
1143 elif message['approval']:
1144 color = Fore.GREEN
1145 elif message['sender'] == data['owner_email']:
1146 color = Fore.MAGENTA
1147 else:
1148 color = Fore.BLUE
1149 print '\n%s%s %s%s' % (
1150 color, message['date'].split('.', 1)[0], message['sender'],
1151 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001152 if message['text'].strip():
1153 print '\n'.join(' ' + l for l in message['text'].splitlines())
1154 return 0
1155
1156
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001157def CMDdescription(parser, args):
1158 """brings up the editor for the current CL's description."""
1159 cl = Changelist()
1160 if not cl.GetIssue():
1161 DieWithError('This branch has no associated changelist.')
1162 description = ChangeDescription(cl.GetDescription())
1163 description.prompt()
1164 cl.UpdateDescription(description.description)
1165 return 0
1166
1167
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001168def CreateDescriptionFromLog(args):
1169 """Pulls out the commit log to use as a base for the CL description."""
1170 log_args = []
1171 if len(args) == 1 and not args[0].endswith('.'):
1172 log_args = [args[0] + '..']
1173 elif len(args) == 1 and args[0].endswith('...'):
1174 log_args = [args[0][:-1]]
1175 elif len(args) == 2:
1176 log_args = [args[0] + '..' + args[1]]
1177 else:
1178 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001179 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001180
1181
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001182def CMDpresubmit(parser, args):
1183 """run presubmit tests on the current changelist"""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001184 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001186 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001187 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001188 (options, args) = parser.parse_args(args)
1189
ukai@chromium.org259e4682012-10-25 07:36:33 +00001190 if not options.force and is_dirty_git_tree('presubmit'):
1191 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001192 return 1
1193
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001194 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001195 if args:
1196 base_branch = args[0]
1197 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001198 # Default to diffing against the common ancestor of the upstream branch.
1199 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001200
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001201 cl.RunHook(
1202 committing=not options.upload,
1203 may_prompt=False,
1204 verbose=options.verbose,
1205 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001206 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207
1208
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001209def AddChangeIdToCommitMessage(options, args):
1210 """Re-commits using the current message, assumes the commit hook is in
1211 place.
1212 """
1213 log_desc = options.message or CreateDescriptionFromLog(args)
1214 git_command = ['commit', '--amend', '-m', log_desc]
1215 RunGit(git_command)
1216 new_log_desc = CreateDescriptionFromLog(args)
1217 if CHANGE_ID in new_log_desc:
1218 print 'git-cl: Added Change-Id to commit message.'
1219 else:
1220 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1221
1222
ukai@chromium.orge8077812012-02-03 03:41:46 +00001223def GerritUpload(options, args, cl):
1224 """upload the current branch to gerrit."""
1225 # We assume the remote called "origin" is the one we want.
1226 # It is probably not worthwhile to support different workflows.
1227 remote = 'origin'
1228 branch = 'master'
1229 if options.target_branch:
1230 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001231
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001232 change_desc = ChangeDescription(
1233 options.message or CreateDescriptionFromLog(args))
1234 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001235 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001236 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001237 if CHANGE_ID not in change_desc.description:
1238 AddChangeIdToCommitMessage(options, args)
1239 if options.reviewers:
1240 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241
ukai@chromium.orge8077812012-02-03 03:41:46 +00001242 receive_options = []
1243 cc = cl.GetCCList().split(',')
1244 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001245 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001246 cc = filter(None, cc)
1247 if cc:
1248 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001249 if change_desc.get_reviewers():
1250 receive_options.extend(
1251 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252
ukai@chromium.orge8077812012-02-03 03:41:46 +00001253 git_command = ['push']
1254 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001255 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001256 ' '.join(receive_options))
1257 git_command += [remote, 'HEAD:refs/for/' + branch]
1258 RunGit(git_command)
1259 # TODO(ukai): parse Change-Id: and set issue number?
1260 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001261
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001262
ukai@chromium.orge8077812012-02-03 03:41:46 +00001263def RietveldUpload(options, args, cl):
1264 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1266 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267 if options.emulate_svn_auto_props:
1268 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001269
1270 change_desc = None
1271
1272 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001273 if options.title:
1274 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001275 if options.message:
1276 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001277 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001278 print ("This branch is associated with issue %s. "
1279 "Adding patch to that issue." % cl.GetIssue())
1280 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001281 if options.title:
1282 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001283 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001284 change_desc = ChangeDescription(message)
1285 if options.reviewers:
1286 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001287 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001288 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001289
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001290 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001291 print "Description is empty; aborting."
1292 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001293
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001294 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001295 if change_desc.get_reviewers():
1296 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001297 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001298 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001299 DieWithError("Must specify reviewers to send email.")
1300 upload_args.append('--send_mail')
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001301 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001302 if cc:
1303 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001304
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001305 if options.private or settings.GetDefaultPrivateFlag() == "True":
1306 upload_args.append('--private')
1307
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001308 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001309 if not options.find_copies:
1310 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001311
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001312 # Include the upstream repo's URL in the change -- this is useful for
1313 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001314 remote_url = cl.GetGitBaseUrlFromConfig()
1315 if not remote_url:
1316 if settings.GetIsGitSvn():
1317 # URL is dependent on the current directory.
1318 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1319 if data:
1320 keys = dict(line.split(': ', 1) for line in data.splitlines()
1321 if ': ' in line)
1322 remote_url = keys.get('URL', None)
1323 else:
1324 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1325 remote_url = (cl.GetRemoteUrl() + '@'
1326 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001327 if remote_url:
1328 upload_args.extend(['--base_url', remote_url])
1329
1330 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001331 upload_args = ['upload'] + upload_args + args
1332 logging.info('upload.RealMain(%s)', upload_args)
1333 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001334 except KeyboardInterrupt:
1335 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001336 except:
1337 # If we got an exception after the user typed a description for their
1338 # change, back up the description before re-raising.
1339 if change_desc:
1340 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1341 print '\nGot exception while uploading -- saving description to %s\n' \
1342 % backup_path
1343 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001344 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001345 backup_file.close()
1346 raise
1347
1348 if not cl.GetIssue():
1349 cl.SetIssue(issue)
1350 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001351
1352 if options.use_commit_queue:
1353 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001354 return 0
1355
1356
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001357def cleanup_list(l):
1358 """Fixes a list so that comma separated items are put as individual items.
1359
1360 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1361 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1362 """
1363 items = sum((i.split(',') for i in l), [])
1364 stripped_items = (i.strip() for i in items)
1365 return sorted(filter(None, stripped_items))
1366
1367
ukai@chromium.orge8077812012-02-03 03:41:46 +00001368@usage('[args to "git diff"]')
1369def CMDupload(parser, args):
1370 """upload the current changelist to codereview"""
1371 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1372 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001373 parser.add_option('--bypass-watchlists', action='store_true',
1374 dest='bypass_watchlists',
1375 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001376 parser.add_option('-f', action='store_true', dest='force',
1377 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001378 parser.add_option('-m', dest='message', help='message for patchset')
1379 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001380 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001381 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001382 help='reviewer email addresses')
1383 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001384 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001385 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001386 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001387 help='send email to reviewer immediately')
1388 parser.add_option("--emulate_svn_auto_props", action="store_true",
1389 dest="emulate_svn_auto_props",
1390 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001391 parser.add_option('-c', '--use-commit-queue', action='store_true',
1392 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001393 parser.add_option('--private', action='store_true',
1394 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001395 parser.add_option('--target_branch',
1396 help='When uploading to gerrit, remote branch to '
1397 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001398 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001399 (options, args) = parser.parse_args(args)
1400
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001401 if options.target_branch and not settings.GetIsGerrit():
1402 parser.error('Use --target_branch for non gerrit repository.')
1403
ukai@chromium.org259e4682012-10-25 07:36:33 +00001404 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001405 return 1
1406
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001407 options.reviewers = cleanup_list(options.reviewers)
1408 options.cc = cleanup_list(options.cc)
1409
ukai@chromium.orge8077812012-02-03 03:41:46 +00001410 cl = Changelist()
1411 if args:
1412 # TODO(ukai): is it ok for gerrit case?
1413 base_branch = args[0]
1414 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001415 # Default to diffing against common ancestor of upstream branch
1416 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001417 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001418
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001419 # Apply watchlists on upload.
1420 change = cl.GetChange(base_branch, None)
1421 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1422 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001423 if not options.bypass_watchlists:
1424 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001425
ukai@chromium.orge8077812012-02-03 03:41:46 +00001426 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001427 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001428 may_prompt=not options.force,
1429 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001430 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001431 if not hook_results.should_continue():
1432 return 1
1433 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001434 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001435
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001436 if cl.GetIssue():
1437 latest_patchset = cl.GetMostRecentPatchset(cl.GetIssue())
1438 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001439 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001440 print ('The last upload made from this repository was patchset #%d but '
1441 'the most recent patchset on the server is #%d.'
1442 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001443 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1444 'from another machine or branch the patch you\'re uploading now '
1445 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001446 ask_for_data('About to upload; enter to confirm.')
1447
iannucci@chromium.org79540052012-10-19 23:15:26 +00001448 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001449 if settings.GetIsGerrit():
1450 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001451 ret = RietveldUpload(options, args, cl)
1452 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001453 git_set_branch_value('last-upload-hash',
1454 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001455
1456 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001457
1458
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001459def IsSubmoduleMergeCommit(ref):
1460 # When submodules are added to the repo, we expect there to be a single
1461 # non-git-svn merge commit at remote HEAD with a signature comment.
1462 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001463 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001464 return RunGit(cmd) != ''
1465
1466
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001467def SendUpstream(parser, args, cmd):
1468 """Common code for CmdPush and CmdDCommit
1469
1470 Squashed commit into a single.
1471 Updates changelog with metadata (e.g. pointer to review).
1472 Pushes/dcommits the code upstream.
1473 Updates review and closes.
1474 """
1475 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1476 help='bypass upload presubmit hook')
1477 parser.add_option('-m', dest='message',
1478 help="override review description")
1479 parser.add_option('-f', action='store_true', dest='force',
1480 help="force yes to questions (don't prompt)")
1481 parser.add_option('-c', dest='contributor',
1482 help="external contributor for patch (appended to " +
1483 "description and used as author for git). Should be " +
1484 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001485 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001486 (options, args) = parser.parse_args(args)
1487 cl = Changelist()
1488
1489 if not args or cmd == 'push':
1490 # Default to merging against our best guess of the upstream branch.
1491 args = [cl.GetUpstreamBranch()]
1492
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001493 if options.contributor:
1494 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1495 print "Please provide contibutor as 'First Last <email@example.com>'"
1496 return 1
1497
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001498 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001499 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001500
ukai@chromium.org259e4682012-10-25 07:36:33 +00001501 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001502 return 1
1503
1504 # This rev-list syntax means "show all commits not in my branch that
1505 # are in base_branch".
1506 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1507 base_branch]).splitlines()
1508 if upstream_commits:
1509 print ('Base branch "%s" has %d commits '
1510 'not in this branch.' % (base_branch, len(upstream_commits)))
1511 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1512 return 1
1513
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001514 # This is the revision `svn dcommit` will commit on top of.
1515 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1516 '--pretty=format:%H'])
1517
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001518 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001519 # If the base_head is a submodule merge commit, the first parent of the
1520 # base_head should be a git-svn commit, which is what we're interested in.
1521 base_svn_head = base_branch
1522 if base_has_submodules:
1523 base_svn_head += '^1'
1524
1525 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001526 if extra_commits:
1527 print ('This branch has %d additional commits not upstreamed yet.'
1528 % len(extra_commits.splitlines()))
1529 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1530 'before attempting to %s.' % (base_branch, cmd))
1531 return 1
1532
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001533 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001534 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001535 author = None
1536 if options.contributor:
1537 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001538 hook_results = cl.RunHook(
1539 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001540 may_prompt=not options.force,
1541 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001542 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001543 if not hook_results.should_continue():
1544 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001545
1546 if cmd == 'dcommit':
1547 # Check the tree status if the tree status URL is set.
1548 status = GetTreeStatus()
1549 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001550 print('The tree is closed. Please wait for it to reopen. Use '
1551 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001552 return 1
1553 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001554 print('Unable to determine tree status. Please verify manually and '
1555 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001556 else:
1557 breakpad.SendStack(
1558 'GitClHooksBypassedCommit',
1559 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001560 (cl.GetRietveldServer(), cl.GetIssue()),
1561 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001562
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001563 change_desc = ChangeDescription(options.message)
1564 if not change_desc.description and cl.GetIssue():
1565 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001566
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001567 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001568 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001569 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001570 else:
1571 print 'No description set.'
1572 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1573 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001574
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001575 # Keep a separate copy for the commit message, because the commit message
1576 # contains the link to the Rietveld issue, while the Rietveld message contains
1577 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001578 # Keep a separate copy for the commit message.
1579 if cl.GetIssue():
1580 change_desc.update_reviewers(cl.GetApprovingReviewers(cl.GetIssue()))
1581
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001582 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001583 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001584 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001585 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001586 commit_desc.append_footer('Patch from %s.' % options.contributor)
1587
1588 print 'Description:', repr(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001589
1590 branches = [base_branch, cl.GetBranchRef()]
1591 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001592 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001593 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001594
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001595 # We want to squash all this branch's commits into one commit with the proper
1596 # description. We do this by doing a "reset --soft" to the base branch (which
1597 # keeps the working copy the same), then dcommitting that. If origin/master
1598 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1599 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001600 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001601 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1602 # Delete the branches if they exist.
1603 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1604 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1605 result = RunGitWithCode(showref_cmd)
1606 if result[0] == 0:
1607 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001608
1609 # We might be in a directory that's present in this branch but not in the
1610 # trunk. Move up to the top of the tree so that git commands that expect a
1611 # valid CWD won't fail after we check out the merge branch.
1612 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1613 if rel_base_path:
1614 os.chdir(rel_base_path)
1615
1616 # Stuff our change into the merge branch.
1617 # We wrap in a try...finally block so if anything goes wrong,
1618 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001619 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001620 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001621 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1622 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001623 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001624 RunGit(
1625 [
1626 'commit', '--author', options.contributor,
1627 '-m', commit_desc.description,
1628 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001629 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001630 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001631 if base_has_submodules:
1632 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1633 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1634 RunGit(['checkout', CHERRY_PICK_BRANCH])
1635 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001636 if cmd == 'push':
1637 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001638 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001639 retcode, output = RunGitWithCode(
1640 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1641 logging.debug(output)
1642 else:
1643 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001644 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001645 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001646 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001647 finally:
1648 # And then swap back to the original branch and clean up.
1649 RunGit(['checkout', '-q', cl.GetBranch()])
1650 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001651 if base_has_submodules:
1652 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001653
1654 if cl.GetIssue():
1655 if cmd == 'dcommit' and 'Committed r' in output:
1656 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1657 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001658 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1659 for l in output.splitlines(False))
1660 match = filter(None, match)
1661 if len(match) != 1:
1662 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1663 output)
1664 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001665 else:
1666 return 1
1667 viewvc_url = settings.GetViewVCUrl()
1668 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001669 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001670 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001671 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001672 print ('Closing issue '
1673 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001674 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001675 cl.CloseIssue()
iannucci@chromium.org16b51402013-02-17 05:33:36 +00001676 props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False)
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001677 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001678 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001679 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1680 cl.RpcServer().add_comment(cl.GetIssue(), comment)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001681 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001682
1683 if retcode == 0:
1684 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1685 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001686 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001687
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001688 return 0
1689
1690
1691@usage('[upstream branch to apply against]')
1692def CMDdcommit(parser, args):
1693 """commit the current changelist via git-svn"""
1694 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001695 message = """This doesn't appear to be an SVN repository.
1696If your project has a git mirror with an upstream SVN master, you probably need
1697to run 'git svn init', see your project's git mirror documentation.
1698If your project has a true writeable upstream repository, you probably want
1699to run 'git cl push' instead.
1700Choose wisely, if you get this wrong, your commit might appear to succeed but
1701will instead be silently ignored."""
1702 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001703 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001704 return SendUpstream(parser, args, 'dcommit')
1705
1706
1707@usage('[upstream branch to apply against]')
1708def CMDpush(parser, args):
1709 """commit the current changelist via git"""
1710 if settings.GetIsGitSvn():
1711 print('This appears to be an SVN repository.')
1712 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001713 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001714 return SendUpstream(parser, args, 'push')
1715
1716
1717@usage('<patch url or issue id>')
1718def CMDpatch(parser, args):
1719 """patch in a code review"""
1720 parser.add_option('-b', dest='newbranch',
1721 help='create a new branch off trunk for the patch')
1722 parser.add_option('-f', action='store_true', dest='force',
1723 help='with -b, clobber any existing branch')
1724 parser.add_option('--reject', action='store_true', dest='reject',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001725 help='failed patches spew .rej files rather than '
1726 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001727 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1728 help="don't commit after patch applies")
1729 (options, args) = parser.parse_args(args)
1730 if len(args) != 1:
1731 parser.print_help()
1732 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001733 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001734
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001735 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001736 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001737
maruel@chromium.org52424302012-08-29 15:14:30 +00001738 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001739 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001740 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001741 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001742 patchset = cl.GetMostRecentPatchset(issue)
1743 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001744 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001745 # Assume it's a URL to the patch. Default to https.
1746 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001747 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001748 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001749 DieWithError('Must pass an issue ID or full URL for '
1750 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001751 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001752 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001753 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001754
1755 if options.newbranch:
1756 if options.force:
1757 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001758 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001759 RunGit(['checkout', '-b', options.newbranch,
1760 Changelist().GetUpstreamBranch()])
1761
1762 # Switch up to the top-level directory, if necessary, in preparation for
1763 # applying the patch.
1764 top = RunGit(['rev-parse', '--show-cdup']).strip()
1765 if top:
1766 os.chdir(top)
1767
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001768 # Git patches have a/ at the beginning of source paths. We strip that out
1769 # with a sed script rather than the -p flag to patch so we can feed either
1770 # Git or svn-style patches into the same apply command.
1771 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001772 try:
1773 patch_data = subprocess2.check_output(
1774 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1775 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001776 DieWithError('Git patch mungling failed.')
1777 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001778 env = os.environ.copy()
1779 # 'cat' is a magical git string that disables pagers on all platforms.
1780 env['GIT_PAGER'] = 'cat'
1781
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001782 # We use "git apply" to apply the patch instead of "patch" so that we can
1783 # pick up file adds.
1784 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001785 cmd = ['git', 'apply', '--index', '-p0']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001786 if options.reject:
1787 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001788 elif IsGitVersionAtLeast('1.7.12'):
1789 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001790 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001791 subprocess2.check_call(cmd, env=env,
1792 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001793 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001794 DieWithError('Failed to apply the patch')
1795
1796 # If we had an issue, commit the current state and register the issue.
1797 if not options.nocommit:
1798 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1799 cl = Changelist()
1800 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001801 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001802 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001803 else:
1804 print "Patch applied to index."
1805 return 0
1806
1807
1808def CMDrebase(parser, args):
1809 """rebase current branch on top of svn repo"""
1810 # Provide a wrapper for git svn rebase to help avoid accidental
1811 # git svn dcommit.
1812 # It's the only command that doesn't use parser at all since we just defer
1813 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001814 env = os.environ.copy()
1815 # 'cat' is a magical git string that disables pagers on all platforms.
1816 env['GIT_PAGER'] = 'cat'
1817
1818 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001819
1820
1821def GetTreeStatus():
1822 """Fetches the tree status and returns either 'open', 'closed',
1823 'unknown' or 'unset'."""
1824 url = settings.GetTreeStatusUrl(error_ok=True)
1825 if url:
1826 status = urllib2.urlopen(url).read().lower()
1827 if status.find('closed') != -1 or status == '0':
1828 return 'closed'
1829 elif status.find('open') != -1 or status == '1':
1830 return 'open'
1831 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001832 return 'unset'
1833
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001834
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001835def GetTreeStatusReason():
1836 """Fetches the tree status from a json url and returns the message
1837 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001838 url = settings.GetTreeStatusUrl()
1839 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001840 connection = urllib2.urlopen(json_url)
1841 status = json.loads(connection.read())
1842 connection.close()
1843 return status['message']
1844
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001845
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001846def CMDtree(parser, args):
1847 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001848 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001849 status = GetTreeStatus()
1850 if 'unset' == status:
1851 print 'You must configure your tree status URL by running "git cl config".'
1852 return 2
1853
1854 print "The tree is %s" % status
1855 print
1856 print GetTreeStatusReason()
1857 if status != 'open':
1858 return 1
1859 return 0
1860
1861
maruel@chromium.org15192402012-09-06 12:38:29 +00001862def CMDtry(parser, args):
1863 """Triggers a try job through Rietveld."""
1864 group = optparse.OptionGroup(parser, "Try job options")
1865 group.add_option(
1866 "-b", "--bot", action="append",
1867 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1868 "times to specify multiple builders. ex: "
1869 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1870 "the try server waterfall for the builders name and the tests "
1871 "available. Can also be used to specify gtest_filter, e.g. "
1872 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1873 group.add_option(
1874 "-r", "--revision",
1875 help="Revision to use for the try job; default: the "
1876 "revision will be determined by the try server; see "
1877 "its waterfall for more info")
1878 group.add_option(
1879 "-c", "--clobber", action="store_true", default=False,
1880 help="Force a clobber before building; e.g. don't do an "
1881 "incremental build")
1882 group.add_option(
1883 "--project",
1884 help="Override which project to use. Projects are defined "
1885 "server-side to define what default bot set to use")
1886 group.add_option(
1887 "-t", "--testfilter", action="append", default=[],
1888 help=("Apply a testfilter to all the selected builders. Unless the "
1889 "builders configurations are similar, use multiple "
1890 "--bot <builder>:<test> arguments."))
1891 group.add_option(
1892 "-n", "--name", help="Try job name; default to current branch name")
1893 parser.add_option_group(group)
1894 options, args = parser.parse_args(args)
1895
1896 if args:
1897 parser.error('Unknown arguments: %s' % args)
1898
1899 cl = Changelist()
1900 if not cl.GetIssue():
1901 parser.error('Need to upload first')
1902
1903 if not options.name:
1904 options.name = cl.GetBranch()
1905
1906 # Process --bot and --testfilter.
1907 if not options.bot:
1908 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001909 change = cl.GetChange(
1910 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1911 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001912 options.bot = presubmit_support.DoGetTrySlaves(
1913 change,
1914 change.LocalPaths(),
1915 settings.GetRoot(),
1916 None,
1917 None,
1918 options.verbose,
1919 sys.stdout)
1920 if not options.bot:
1921 parser.error('No default try builder to try, use --bot')
1922
1923 builders_and_tests = {}
1924 for bot in options.bot:
1925 if ':' in bot:
1926 builder, tests = bot.split(':', 1)
1927 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1928 elif ',' in bot:
1929 parser.error('Specify one bot per --bot flag')
1930 else:
1931 builders_and_tests.setdefault(bot, []).append('defaulttests')
1932
1933 if options.testfilter:
1934 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1935 builders_and_tests = dict(
1936 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1937 if t != ['compile'])
1938
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001939 if any('triggered' in b for b in builders_and_tests):
1940 print >> sys.stderr, (
1941 'ERROR You are trying to send a job to a triggered bot. This type of'
1942 ' bot requires an\ninitial job from a parent (usually a builder). '
1943 'Instead send your job to the parent.\n'
1944 'Bot list: %s' % builders_and_tests)
1945 return 1
1946
maruel@chromium.org15192402012-09-06 12:38:29 +00001947 patchset = cl.GetPatchset()
1948 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001949 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001950
1951 cl.RpcServer().trigger_try_jobs(
1952 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1953 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001954 print('Tried jobs on:')
1955 length = max(len(builder) for builder in builders_and_tests)
1956 for builder in sorted(builders_and_tests):
1957 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001958 return 0
1959
1960
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001961@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001962def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001963 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001964 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001965 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001966 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001967 return 0
1968
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001969 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001970 if args:
1971 # One arg means set upstream branch.
1972 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1973 cl = Changelist()
1974 print "Upstream branch set to " + cl.GetUpstreamBranch()
1975 else:
1976 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001977 return 0
1978
1979
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001980def CMDset_commit(parser, args):
1981 """set the commit bit"""
1982 _, args = parser.parse_args(args)
1983 if args:
1984 parser.error('Unrecognized args: %s' % ' '.join(args))
1985 cl = Changelist()
1986 cl.SetFlag('commit', '1')
1987 return 0
1988
1989
groby@chromium.org411034a2013-02-26 15:12:01 +00001990def CMDset_close(parser, args):
1991 """close the issue"""
1992 _, args = parser.parse_args(args)
1993 if args:
1994 parser.error('Unrecognized args: %s' % ' '.join(args))
1995 cl = Changelist()
1996 # Ensure there actually is an issue to close.
1997 cl.GetDescription()
1998 cl.CloseIssue()
1999 return 0
2000
2001
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002002def CMDformat(parser, args):
2003 """run clang-format on the diff"""
2004 CLANG_EXTS = ['.cc', '.cpp', '.h']
2005 parser.add_option('--full', action='store_true', default=False)
2006 opts, args = parser.parse_args(args)
2007 if args:
2008 parser.error('Unrecognized args: %s' % ' '.join(args))
2009
digit@chromium.org29e47272013-05-17 17:01:46 +00002010 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002011 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002012 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002013 # Only list the names of modified files.
2014 diff_cmd.append('--name-only')
2015 else:
2016 # Only generate context-less patches.
2017 diff_cmd.append('-U0')
2018
2019 # Grab the merge-base commit, i.e. the upstream commit of the current
2020 # branch when it was created or the last time it was rebased. This is
2021 # to cover the case where the user may have called "git fetch origin",
2022 # moving the origin branch to a newer commit, but hasn't rebased yet.
2023 upstream_commit = None
2024 cl = Changelist()
2025 upstream_branch = cl.GetUpstreamBranch()
2026 if upstream_branch:
2027 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2028 upstream_commit = upstream_commit.strip()
2029
2030 if not upstream_commit:
2031 DieWithError('Could not find base commit for this branch. '
2032 'Are you in detached state?')
2033
2034 diff_cmd.append(upstream_commit)
2035
2036 # Handle source file filtering.
2037 diff_cmd.append('--')
2038 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2039 diff_output = RunGit(diff_cmd)
2040
2041 if opts.full:
2042 # diff_output is a list of files to send to clang-format.
2043 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002044 if not files:
2045 print "Nothing to format."
2046 return 0
digit@chromium.org29e47272013-05-17 17:01:46 +00002047 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002048 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002049 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002050 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2051 'clang-format-diff.py')
2052 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002053 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
2054 cmd = [sys.executable, cfd_path, '-style', 'Chromium']
2055 RunCommand(cmd, stdin=diff_output)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002056
2057 return 0
2058
2059
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002060### Glue code for subcommand handling.
2061
2062
2063def Commands():
2064 """Returns a dict of command and their handling function."""
2065 module = sys.modules[__name__]
2066 cmds = (fn[3:] for fn in dir(module) if fn.startswith('CMD'))
2067 return dict((cmd, getattr(module, 'CMD' + cmd)) for cmd in cmds)
2068
2069
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002070def Command(name):
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002071 """Retrieves the function to handle a command."""
2072 commands = Commands()
2073 if name in commands:
2074 return commands[name]
2075
2076 # Try to be smart and look if there's something similar.
2077 commands_with_prefix = [c for c in commands if c.startswith(name)]
2078 if len(commands_with_prefix) == 1:
2079 return commands[commands_with_prefix[0]]
2080
2081 # A #closeenough approximation of levenshtein distance.
2082 def close_enough(a, b):
2083 return difflib.SequenceMatcher(a=a, b=b).ratio()
2084
2085 hamming_commands = sorted(
2086 ((close_enough(c, name), c) for c in commands),
2087 reverse=True)
2088 if (hamming_commands[0][0] - hamming_commands[1][0]) < 0.3:
2089 # Too ambiguous.
2090 return
2091
2092 if hamming_commands[0][0] < 0.8:
2093 # Not similar enough. Don't be a fool and run a random command.
2094 return
2095
2096 return commands[hamming_commands[0][1]]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002097
2098
2099def CMDhelp(parser, args):
2100 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002101 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002102 if len(args) == 1:
2103 return main(args + ['--help'])
2104 parser.print_help()
2105 return 0
2106
2107
2108def GenUsage(parser, command):
2109 """Modify an OptParse object with the function's documentation."""
2110 obj = Command(command)
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002111 # Get back the real command name in case Command() guess the actual command
2112 # name.
2113 command = obj.__name__[3:]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002114 more = getattr(obj, 'usage_more', '')
2115 if command == 'help':
2116 command = '<command>'
2117 else:
2118 # OptParser.description prefer nicely non-formatted strings.
2119 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
2120 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
2121
2122
2123def main(argv):
2124 """Doesn't parse the arguments here, just find the right subcommand to
2125 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002126 if sys.hexversion < 0x02060000:
2127 print >> sys.stderr, (
2128 '\nYour python version %s is unsupported, please upgrade.\n' %
2129 sys.version.split(' ', 1)[0])
2130 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002131
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002132 # Reload settings.
2133 global settings
2134 settings = Settings()
2135
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002136 # Do it late so all commands are listed.
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002137 commands = Commands()
2138 length = max(len(c) for c in commands)
2139 docs = sorted(
2140 (name, handler.__doc__.split('\n')[0].strip())
2141 for name, handler in commands.iteritems())
2142 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join(
2143 ' %-*s %s' % (length, name, doc) for name, doc in docs))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002144
2145 # Create the option parse and add --verbose support.
2146 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002147 parser.add_option(
2148 '-v', '--verbose', action='count', default=0,
2149 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002150 old_parser_args = parser.parse_args
2151 def Parse(args):
2152 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002153 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002154 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002155 elif options.verbose:
2156 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002157 else:
2158 logging.basicConfig(level=logging.WARNING)
2159 return options, args
2160 parser.parse_args = Parse
2161
2162 if argv:
2163 command = Command(argv[0])
2164 if command:
2165 # "fix" the usage and the description now that we know the subcommand.
2166 GenUsage(parser, argv[0])
2167 try:
2168 return command(parser, argv[1:])
2169 except urllib2.HTTPError, e:
2170 if e.code != 500:
2171 raise
2172 DieWithError(
2173 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2174 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
2175
2176 # Not a known command. Default to help.
2177 GenUsage(parser, 'help')
2178 return CMDhelp(parser, argv)
2179
2180
2181if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002182 # These affect sys.stdout so do it outside of main() to simplify mocks in
2183 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002184 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002185 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002186 sys.exit(main(sys.argv[1:]))