blob: 7e9180e8a963c15d7a013406830091089f3b150f [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
maruel@chromium.org967c0a82013-06-17 22:52:24 +000010import difflib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000011import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000012import logging
13import optparse
14import os
15import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000016import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import textwrap
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000020import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021
22try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000023 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024except ImportError:
25 pass
26
maruel@chromium.org2a74d372011-03-29 19:05:50 +000027
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000028from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000029from third_party import upload
30import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000031import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000032import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000034import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000036import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000037import watchlists
38
39
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000040DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000041POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000042DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000043GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000044CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000045
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000046# Shortcut since it quickly becomes redundant.
47Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000048
maruel@chromium.orgddd59412011-11-30 14:20:38 +000049# Initialized in main()
50settings = None
51
52
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000053def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000054 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000055 sys.exit(1)
56
57
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000058def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000059 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000060 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000061 except subprocess2.CalledProcessError as e:
62 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000063 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000064 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000065 'Command "%s" failed.\n%s' % (
66 ' '.join(args), error_message or e.stdout or ''))
67 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068
69
70def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000071 """Returns stdout."""
bratell@opera.comf267b0e2013-05-02 09:11:43 +000072 return RunCommand(['git', '--no-pager'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073
74
75def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000076 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000077 try:
bratell@opera.comf267b0e2013-05-02 09:11:43 +000078 out, code = subprocess2.communicate(['git', '--no-pager'] + args,
79 stdout=subprocess2.PIPE)
szager@chromium.org9bb85e22012-06-13 20:28:23 +000080 return code, out[0]
81 except ValueError:
82 # When the subprocess fails, it returns None. That triggers a ValueError
83 # when trying to unpack the return value into (out, code).
84 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000085
86
87def usage(more):
88 def hook(fn):
89 fn.usage_more = more
90 return fn
91 return hook
92
93
maruel@chromium.org90541732011-04-01 17:54:18 +000094def ask_for_data(prompt):
95 try:
96 return raw_input(prompt)
97 except KeyboardInterrupt:
98 # Hide the exception.
99 sys.exit(1)
100
101
iannucci@chromium.org79540052012-10-19 23:15:26 +0000102def git_set_branch_value(key, value):
103 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000104 if not branch:
105 return
106
107 cmd = ['config']
108 if isinstance(value, int):
109 cmd.append('--int')
110 git_key = 'branch.%s.%s' % (branch, key)
111 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000112
113
114def git_get_branch_default(key, default):
115 branch = Changelist().GetBranch()
116 if branch:
117 git_key = 'branch.%s.%s' % (branch, key)
118 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
119 try:
120 return int(stdout.strip())
121 except ValueError:
122 pass
123 return default
124
125
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000126def add_git_similarity(parser):
127 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000128 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000129 help='Sets the percentage that a pair of files need to match in order to'
130 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000131 parser.add_option(
132 '--find-copies', action='store_true',
133 help='Allows git to look for copies.')
134 parser.add_option(
135 '--no-find-copies', action='store_false', dest='find_copies',
136 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000137
138 old_parser_args = parser.parse_args
139 def Parse(args):
140 options, args = old_parser_args(args)
141
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000142 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000143 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000144 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000145 print('Note: Saving similarity of %d%% in git config.'
146 % options.similarity)
147 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000148
iannucci@chromium.org79540052012-10-19 23:15:26 +0000149 options.similarity = max(0, min(options.similarity, 100))
150
151 if options.find_copies is None:
152 options.find_copies = bool(
153 git_get_branch_default('git-find-copies', True))
154 else:
155 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000156
157 print('Using %d%% similarity for rename/copy detection. '
158 'Override with --similarity.' % options.similarity)
159
160 return options, args
161 parser.parse_args = Parse
162
163
ukai@chromium.org259e4682012-10-25 07:36:33 +0000164def is_dirty_git_tree(cmd):
165 # Make sure index is up-to-date before running diff-index.
166 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
167 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
168 if dirty:
169 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
170 print 'Uncommitted files: (git diff-index --name-status HEAD)'
171 print dirty[:4096]
172 if len(dirty) > 4096:
173 print '... (run "git diff-index --name-status HEAD" to see full output).'
174 return True
175 return False
176
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000177
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000178def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
179 """Return the corresponding git ref if |base_url| together with |glob_spec|
180 matches the full |url|.
181
182 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
183 """
184 fetch_suburl, as_ref = glob_spec.split(':')
185 if allow_wildcards:
186 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
187 if glob_match:
188 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
189 # "branches/{472,597,648}/src:refs/remotes/svn/*".
190 branch_re = re.escape(base_url)
191 if glob_match.group(1):
192 branch_re += '/' + re.escape(glob_match.group(1))
193 wildcard = glob_match.group(2)
194 if wildcard == '*':
195 branch_re += '([^/]*)'
196 else:
197 # Escape and replace surrounding braces with parentheses and commas
198 # with pipe symbols.
199 wildcard = re.escape(wildcard)
200 wildcard = re.sub('^\\\\{', '(', wildcard)
201 wildcard = re.sub('\\\\,', '|', wildcard)
202 wildcard = re.sub('\\\\}$', ')', wildcard)
203 branch_re += wildcard
204 if glob_match.group(3):
205 branch_re += re.escape(glob_match.group(3))
206 match = re.match(branch_re, url)
207 if match:
208 return re.sub('\*$', match.group(1), as_ref)
209
210 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
211 if fetch_suburl:
212 full_url = base_url + '/' + fetch_suburl
213 else:
214 full_url = base_url
215 if full_url == url:
216 return as_ref
217 return None
218
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000219
iannucci@chromium.org79540052012-10-19 23:15:26 +0000220def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000221 """Prints statistics about the change to the user."""
222 # --no-ext-diff is broken in some versions of Git, so try to work around
223 # this by overriding the environment (but there is still a problem if the
224 # git config key "diff.external" is used).
225 env = os.environ.copy()
226 if 'GIT_EXTERNAL_DIFF' in env:
227 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000228
229 if find_copies:
230 similarity_options = ['--find-copies-harder', '-l100000',
231 '-C%s' % similarity]
232 else:
233 similarity_options = ['-M%s' % similarity]
234
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000235 return subprocess2.call(
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000236 ['git', '--no-pager',
237 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000238 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000239
240
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000241class Settings(object):
242 def __init__(self):
243 self.default_server = None
244 self.cc = None
245 self.root = None
246 self.is_git_svn = None
247 self.svn_branch = None
248 self.tree_status_url = None
249 self.viewvc_url = None
250 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000251 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000252 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000253
254 def LazyUpdateIfNeeded(self):
255 """Updates the settings from a codereview.settings file, if available."""
256 if not self.updated:
257 cr_settings_file = FindCodereviewSettingsFile()
258 if cr_settings_file:
259 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000260 self.updated = True
261 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000262 self.updated = True
263
264 def GetDefaultServerUrl(self, error_ok=False):
265 if not self.default_server:
266 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000267 self.default_server = gclient_utils.UpgradeToHttps(
268 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000269 if error_ok:
270 return self.default_server
271 if not self.default_server:
272 error_message = ('Could not find settings file. You must configure '
273 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000274 self.default_server = gclient_utils.UpgradeToHttps(
275 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000276 return self.default_server
277
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000278 def GetRoot(self):
279 if not self.root:
280 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
281 return self.root
282
283 def GetIsGitSvn(self):
284 """Return true if this repo looks like it's using git-svn."""
285 if self.is_git_svn is None:
286 # If you have any "svn-remote.*" config keys, we think you're using svn.
287 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000288 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000289 return self.is_git_svn
290
291 def GetSVNBranch(self):
292 if self.svn_branch is None:
293 if not self.GetIsGitSvn():
294 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
295
296 # Try to figure out which remote branch we're based on.
297 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000298 # 1) iterate through our branch history and find the svn URL.
299 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000300
301 # regexp matching the git-svn line that contains the URL.
302 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
303
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000304 # We don't want to go through all of history, so read a line from the
305 # pipe at a time.
306 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000307 cmd = ['git', '--no-pager', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000308 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000309 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000310 for line in proc.stdout:
311 match = git_svn_re.match(line)
312 if match:
313 url = match.group(1)
314 proc.stdout.close() # Cut pipe.
315 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000316
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000317 if url:
318 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
319 remotes = RunGit(['config', '--get-regexp',
320 r'^svn-remote\..*\.url']).splitlines()
321 for remote in remotes:
322 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000323 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000324 remote = match.group(1)
325 base_url = match.group(2)
326 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000327 ['config', 'svn-remote.%s.fetch' % remote],
328 error_ok=True).strip()
329 if fetch_spec:
330 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
331 if self.svn_branch:
332 break
333 branch_spec = RunGit(
334 ['config', 'svn-remote.%s.branches' % remote],
335 error_ok=True).strip()
336 if branch_spec:
337 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
338 if self.svn_branch:
339 break
340 tag_spec = RunGit(
341 ['config', 'svn-remote.%s.tags' % remote],
342 error_ok=True).strip()
343 if tag_spec:
344 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
345 if self.svn_branch:
346 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000347
348 if not self.svn_branch:
349 DieWithError('Can\'t guess svn branch -- try specifying it on the '
350 'command line')
351
352 return self.svn_branch
353
354 def GetTreeStatusUrl(self, error_ok=False):
355 if not self.tree_status_url:
356 error_message = ('You must configure your tree status URL by running '
357 '"git cl config".')
358 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
359 error_ok=error_ok,
360 error_message=error_message)
361 return self.tree_status_url
362
363 def GetViewVCUrl(self):
364 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000365 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000366 return self.viewvc_url
367
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000368 def GetDefaultCCList(self):
369 return self._GetConfig('rietveld.cc', error_ok=True)
370
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000371 def GetDefaultPrivateFlag(self):
372 return self._GetConfig('rietveld.private', error_ok=True)
373
ukai@chromium.orge8077812012-02-03 03:41:46 +0000374 def GetIsGerrit(self):
375 """Return true if this repo is assosiated with gerrit code review system."""
376 if self.is_gerrit is None:
377 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
378 return self.is_gerrit
379
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000380 def GetGitEditor(self):
381 """Return the editor specified in the git config, or None if none is."""
382 if self.git_editor is None:
383 self.git_editor = self._GetConfig('core.editor', error_ok=True)
384 return self.git_editor or None
385
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000386 def _GetConfig(self, param, **kwargs):
387 self.LazyUpdateIfNeeded()
388 return RunGit(['config', param], **kwargs).strip()
389
390
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000391def ShortBranchName(branch):
392 """Convert a name like 'refs/heads/foo' to just 'foo'."""
393 return branch.replace('refs/heads/', '')
394
395
396class Changelist(object):
397 def __init__(self, branchref=None):
398 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000399 global settings
400 if not settings:
401 # Happens when git_cl.py is used as a utility library.
402 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000403 settings.GetDefaultServerUrl()
404 self.branchref = branchref
405 if self.branchref:
406 self.branch = ShortBranchName(self.branchref)
407 else:
408 self.branch = None
409 self.rietveld_server = None
410 self.upstream_branch = None
411 self.has_issue = False
412 self.issue = None
413 self.has_description = False
414 self.description = None
415 self.has_patchset = False
416 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000417 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000418 self.cc = None
419 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000420 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000421
422 def GetCCList(self):
423 """Return the users cc'd on this CL.
424
425 Return is a string suitable for passing to gcl with the --cc flag.
426 """
427 if self.cc is None:
428 base_cc = settings .GetDefaultCCList()
429 more_cc = ','.join(self.watchers)
430 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
431 return self.cc
432
433 def SetWatchers(self, watchers):
434 """Set the list of email addresses that should be cc'd based on the changed
435 files in this CL.
436 """
437 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000438
439 def GetBranch(self):
440 """Returns the short branch name, e.g. 'master'."""
441 if not self.branch:
442 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
443 self.branch = ShortBranchName(self.branchref)
444 return self.branch
445
446 def GetBranchRef(self):
447 """Returns the full branch name, e.g. 'refs/heads/master'."""
448 self.GetBranch() # Poke the lazy loader.
449 return self.branchref
450
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000451 @staticmethod
452 def FetchUpstreamTuple(branch):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000453 """Returns a tuple containg remote and remote ref,
454 e.g. 'origin', 'refs/heads/master'
455 """
456 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000457 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
458 error_ok=True).strip()
459 if upstream_branch:
460 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
461 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000462 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
463 error_ok=True).strip()
464 if upstream_branch:
465 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000466 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000467 # Fall back on trying a git-svn upstream branch.
468 if settings.GetIsGitSvn():
469 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000470 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000471 # Else, try to guess the origin remote.
472 remote_branches = RunGit(['branch', '-r']).split()
473 if 'origin/master' in remote_branches:
474 # Fall back on origin/master if it exits.
475 remote = 'origin'
476 upstream_branch = 'refs/heads/master'
477 elif 'origin/trunk' in remote_branches:
478 # Fall back on origin/trunk if it exists. Generally a shared
479 # git-svn clone
480 remote = 'origin'
481 upstream_branch = 'refs/heads/trunk'
482 else:
483 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000484Either pass complete "git diff"-style arguments, like
485 git cl upload origin/master
486or verify this branch is set up to track another (via the --track argument to
487"git checkout -b ...").""")
488
489 return remote, upstream_branch
490
491 def GetUpstreamBranch(self):
492 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000493 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000494 if remote is not '.':
495 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
496 self.upstream_branch = upstream_branch
497 return self.upstream_branch
498
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000499 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000500 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000501 remote, branch = None, self.GetBranch()
502 seen_branches = set()
503 while branch not in seen_branches:
504 seen_branches.add(branch)
505 remote, branch = self.FetchUpstreamTuple(branch)
506 branch = ShortBranchName(branch)
507 if remote != '.' or branch.startswith('refs/remotes'):
508 break
509 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000510 remotes = RunGit(['remote'], error_ok=True).split()
511 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000512 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000513 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000514 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000515 logging.warning('Could not determine which remote this change is '
516 'associated with, so defaulting to "%s". This may '
517 'not be what you want. You may prevent this message '
518 'by running "git svn info" as documented here: %s',
519 self._remote,
520 GIT_INSTRUCTIONS_URL)
521 else:
522 logging.warn('Could not determine which remote this change is '
523 'associated with. You may prevent this message by '
524 'running "git svn info" as documented here: %s',
525 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000526 branch = 'HEAD'
527 if branch.startswith('refs/remotes'):
528 self._remote = (remote, branch)
529 else:
530 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000531 return self._remote
532
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000533 def GitSanityChecks(self, upstream_git_obj):
534 """Checks git repo status and ensures diff is from local commits."""
535
536 # Verify the commit we're diffing against is in our current branch.
537 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
538 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
539 if upstream_sha != common_ancestor:
540 print >> sys.stderr, (
541 'ERROR: %s is not in the current branch. You may need to rebase '
542 'your tracking branch' % upstream_sha)
543 return False
544
545 # List the commits inside the diff, and verify they are all local.
546 commits_in_diff = RunGit(
547 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
548 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
549 remote_branch = remote_branch.strip()
550 if code != 0:
551 _, remote_branch = self.GetRemoteBranch()
552
553 commits_in_remote = RunGit(
554 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
555
556 common_commits = set(commits_in_diff) & set(commits_in_remote)
557 if common_commits:
558 print >> sys.stderr, (
559 'ERROR: Your diff contains %d commits already in %s.\n'
560 'Run "git log --oneline %s..HEAD" to get a list of commits in '
561 'the diff. If you are using a custom git flow, you can override'
562 ' the reference used for this check with "git config '
563 'gitcl.remotebranch <git-ref>".' % (
564 len(common_commits), remote_branch, upstream_git_obj))
565 return False
566 return True
567
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000568 def GetGitBaseUrlFromConfig(self):
569 """Return the configured base URL from branch.<branchname>.baseurl.
570
571 Returns None if it is not set.
572 """
573 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
574 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000575
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000576 def GetRemoteUrl(self):
577 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
578
579 Returns None if there is no remote.
580 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000581 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000582 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
583
584 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000585 """Returns the issue number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000586 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000587 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
588 if issue:
maruel@chromium.org52424302012-08-29 15:14:30 +0000589 self.issue = int(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000590 else:
591 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000592 self.has_issue = True
593 return self.issue
594
595 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000596 if not self.rietveld_server:
597 # If we're on a branch then get the server potentially associated
598 # with that branch.
599 if self.GetIssue():
600 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
601 ['config', self._RietveldServer()], error_ok=True).strip())
602 if not self.rietveld_server:
603 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000604 return self.rietveld_server
605
606 def GetIssueURL(self):
607 """Get the URL for a particular issue."""
608 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
609
610 def GetDescription(self, pretty=False):
611 if not self.has_description:
612 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000613 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000614 try:
615 self.description = self.RpcServer().get_description(issue).strip()
616 except urllib2.HTTPError, e:
617 if e.code == 404:
618 DieWithError(
619 ('\nWhile fetching the description for issue %d, received a '
620 '404 (not found)\n'
621 'error. It is likely that you deleted this '
622 'issue on the server. If this is the\n'
623 'case, please run\n\n'
624 ' git cl issue 0\n\n'
625 'to clear the association with the deleted issue. Then run '
626 'this command again.') % issue)
627 else:
628 DieWithError(
629 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000630 self.has_description = True
631 if pretty:
632 wrapper = textwrap.TextWrapper()
633 wrapper.initial_indent = wrapper.subsequent_indent = ' '
634 return wrapper.fill(self.description)
635 return self.description
636
637 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000638 """Returns the patchset number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000639 if not self.has_patchset:
640 patchset = RunGit(['config', self._PatchsetSetting()],
641 error_ok=True).strip()
642 if patchset:
maruel@chromium.org52424302012-08-29 15:14:30 +0000643 self.patchset = int(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000644 else:
645 self.patchset = None
646 self.has_patchset = True
647 return self.patchset
648
649 def SetPatchset(self, patchset):
650 """Set this branch's patchset. If patchset=0, clears the patchset."""
651 if patchset:
652 RunGit(['config', self._PatchsetSetting(), str(patchset)])
653 else:
654 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000655 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000656 self.has_patchset = False
657
binji@chromium.org0281f522012-09-14 13:37:59 +0000658 def GetMostRecentPatchset(self, issue):
659 return self.RpcServer().get_issue_properties(
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000660 int(issue), False)['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000661
662 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000663 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000664 '/download/issue%s_%s.diff' % (issue, patchset))
665
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000666 def GetApprovingReviewers(self, issue):
667 return get_approving_reviewers(
668 self.RpcServer().get_issue_properties(int(issue), True))
669
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000670 def SetIssue(self, issue):
671 """Set this branch's issue. If issue=0, clears the issue."""
672 if issue:
673 RunGit(['config', self._IssueSetting(), str(issue)])
674 if self.rietveld_server:
675 RunGit(['config', self._RietveldServer(), self.rietveld_server])
676 else:
677 RunGit(['config', '--unset', self._IssueSetting()])
678 self.SetPatchset(0)
679 self.has_issue = False
680
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000681 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000682 if not self.GitSanityChecks(upstream_branch):
683 DieWithError('\nGit sanity check failure')
684
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000685 root = RunCommand(['git', '--no-pager', 'rev-parse', '--show-cdup']).strip()
686 if not root:
687 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000688 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000689
690 # We use the sha1 of HEAD as a name of this change.
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000691 name = RunCommand(['git', '--no-pager', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000692 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000693 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000694 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000695 except subprocess2.CalledProcessError:
696 DieWithError(
697 ('\nFailed to diff against upstream branch %s!\n\n'
698 'This branch probably doesn\'t exist anymore. To reset the\n'
699 'tracking branch, please run\n'
700 ' git branch --set-upstream %s trunk\n'
701 'replacing trunk with origin/master or the relevant branch') %
702 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000703
maruel@chromium.org52424302012-08-29 15:14:30 +0000704 issue = self.GetIssue()
705 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000706 if issue:
707 description = self.GetDescription()
708 else:
709 # If the change was never uploaded, use the log messages of all commits
710 # up to the branch point, as git cl upload will prefill the description
711 # with these log messages.
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000712 description = RunCommand(['git', '--no-pager',
713 'log', '--pretty=format:%s%n%n%b',
maruel@chromium.org373af802012-05-25 21:07:33 +0000714 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000715
716 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000717 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000718 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000719 name,
720 description,
721 absroot,
722 files,
723 issue,
724 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000725 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000726
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000727 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000728 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000729
730 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000731 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000732 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000733 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000734 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000735 except presubmit_support.PresubmitFailure, e:
736 DieWithError(
737 ('%s\nMaybe your depot_tools is out of date?\n'
738 'If all fails, contact maruel@') % e)
739
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000740 def UpdateDescription(self, description):
741 self.description = description
742 return self.RpcServer().update_description(
743 self.GetIssue(), self.description)
744
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000745 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000746 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000747 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000748
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000749 def SetFlag(self, flag, value):
750 """Patchset must match."""
751 if not self.GetPatchset():
752 DieWithError('The patchset needs to match. Send another patchset.')
753 try:
754 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000755 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000756 except urllib2.HTTPError, e:
757 if e.code == 404:
758 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
759 if e.code == 403:
760 DieWithError(
761 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
762 'match?') % (self.GetIssue(), self.GetPatchset()))
763 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000764
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000765 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000766 """Returns an upload.RpcServer() to access this review's rietveld instance.
767 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000768 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000769 self._rpc_server = rietveld.CachingRietveld(
770 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000771 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000772
773 def _IssueSetting(self):
774 """Return the git setting that stores this change's issue."""
775 return 'branch.%s.rietveldissue' % self.GetBranch()
776
777 def _PatchsetSetting(self):
778 """Return the git setting that stores this change's most recent patchset."""
779 return 'branch.%s.rietveldpatchset' % self.GetBranch()
780
781 def _RietveldServer(self):
782 """Returns the git setting that stores this change's rietveld server."""
783 return 'branch.%s.rietveldserver' % self.GetBranch()
784
785
786def GetCodereviewSettingsInteractively():
787 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000788 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000789 server = settings.GetDefaultServerUrl(error_ok=True)
790 prompt = 'Rietveld server (host[:port])'
791 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000792 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000793 if not server and not newserver:
794 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000795 if newserver:
796 newserver = gclient_utils.UpgradeToHttps(newserver)
797 if newserver != server:
798 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000799
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000800 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801 prompt = caption
802 if initial:
803 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000804 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000805 if new_val == 'x':
806 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000807 elif new_val:
808 if is_url:
809 new_val = gclient_utils.UpgradeToHttps(new_val)
810 if new_val != initial:
811 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000812
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000813 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000814 SetProperty(settings.GetDefaultPrivateFlag(),
815 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000816 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000817 'tree-status-url', False)
818 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000819
820 # TODO: configure a default branch to diff against, rather than this
821 # svn-based hackery.
822
823
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000824class ChangeDescription(object):
825 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000826 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000827
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000828 def __init__(self, description):
829 self._description = (description or '').strip()
830
831 @property
832 def description(self):
833 return self._description
834
835 def update_reviewers(self, reviewers):
836 """Rewrites the R=/TBR= line(s) as a single line."""
837 assert isinstance(reviewers, list), reviewers
838 if not reviewers:
839 return
840 regexp = re.compile(self.R_LINE, re.MULTILINE)
841 matches = list(regexp.finditer(self._description))
842 is_tbr = any(m.group(1) == 'TBR' for m in matches)
843 if len(matches) > 1:
844 # Erase all except the first one.
845 for i in xrange(len(matches) - 1, 0, -1):
846 self._description = (
847 self._description[:matches[i].start()] +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000848 self._description[matches[i].end():])
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000849
850 if is_tbr:
851 new_r_line = 'TBR=' + ', '.join(reviewers)
852 else:
853 new_r_line = 'R=' + ', '.join(reviewers)
854
855 if matches:
856 self._description = (
857 self._description[:matches[0].start()] + new_r_line +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000858 self._description[matches[0].end():]).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000859 else:
860 self.append_footer(new_r_line)
861
862 def prompt(self):
863 """Asks the user to update the description."""
864 self._description = (
865 '# Enter a description of the change.\n'
janx@chromium.org104b2db2013-04-18 12:58:40 +0000866 '# This will be displayed on the codereview site.\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000867 '# The first line will also be used as the subject of the review.\n'
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000868 '#--------------------This line is 72 characters long'
869 '--------------------\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000870 ) + self._description
871
872 if '\nBUG=' not in self._description:
873 self.append_footer('BUG=')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000874 content = gclient_utils.RunEditor(self._description, True,
875 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000876 if not content:
877 DieWithError('Running editor failed')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000878
879 # Strip off comments.
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000880 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000881 if not content:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000882 DieWithError('No CL description, aborting')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000883 self._description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000884
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000885 def append_footer(self, line):
886 # Adds a LF if the last line did not have 'FOO=BAR' or if the new one isn't.
887 if self._description:
888 if '\n' not in self._description:
889 self._description += '\n'
890 else:
891 last_line = self._description.rsplit('\n', 1)[1]
892 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
893 not presubmit_support.Change.TAG_LINE_RE.match(line)):
894 self._description += '\n'
895 self._description += '\n' + line
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000896
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000897 def get_reviewers(self):
898 """Retrieves the list of reviewers."""
899 regexp = re.compile(self.R_LINE, re.MULTILINE)
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000900 reviewers = [i.group(2).strip() for i in regexp.finditer(self._description)]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000901 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000902
903
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000904def get_approving_reviewers(props):
905 """Retrieves the reviewers that approved a CL from the issue properties with
906 messages.
907
908 Note that the list may contain reviewers that are not committer, thus are not
909 considered by the CQ.
910 """
911 return sorted(
912 set(
913 message['sender']
914 for message in props['messages']
915 if message['approval'] and message['sender'] in props['reviewers']
916 )
917 )
918
919
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000920def FindCodereviewSettingsFile(filename='codereview.settings'):
921 """Finds the given file starting in the cwd and going up.
922
923 Only looks up to the top of the repository unless an
924 'inherit-review-settings-ok' file exists in the root of the repository.
925 """
926 inherit_ok_file = 'inherit-review-settings-ok'
927 cwd = os.getcwd()
928 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
929 if os.path.isfile(os.path.join(root, inherit_ok_file)):
930 root = '/'
931 while True:
932 if filename in os.listdir(cwd):
933 if os.path.isfile(os.path.join(cwd, filename)):
934 return open(os.path.join(cwd, filename))
935 if cwd == root:
936 break
937 cwd = os.path.dirname(cwd)
938
939
940def LoadCodereviewSettingsFromFile(fileobj):
941 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000942 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000943
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000944 def SetProperty(name, setting, unset_error_ok=False):
945 fullname = 'rietveld.' + name
946 if setting in keyvals:
947 RunGit(['config', fullname, keyvals[setting]])
948 else:
949 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
950
951 SetProperty('server', 'CODE_REVIEW_SERVER')
952 # Only server setting is required. Other settings can be absent.
953 # In that case, we ignore errors raised during option deletion attempt.
954 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000955 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000956 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
957 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
958
ukai@chromium.orge8077812012-02-03 03:41:46 +0000959 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
960 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
961 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000962
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000963 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
964 #should be of the form
965 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
966 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
967 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
968 keyvals['ORIGIN_URL_CONFIG']])
969
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000970
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000971def urlretrieve(source, destination):
972 """urllib is broken for SSL connections via a proxy therefore we
973 can't use urllib.urlretrieve()."""
974 with open(destination, 'w') as f:
975 f.write(urllib2.urlopen(source).read())
976
977
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000978def DownloadHooks(force):
979 """downloads hooks
980
981 Args:
982 force: True to update hooks. False to install hooks if not present.
983 """
984 if not settings.GetIsGerrit():
985 return
986 server_url = settings.GetDefaultServerUrl()
987 src = '%s/tools/hooks/commit-msg' % server_url
988 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
989 if not os.access(dst, os.X_OK):
990 if os.path.exists(dst):
991 if not force:
992 return
993 os.remove(dst)
994 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000995 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000996 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
997 except Exception:
998 if os.path.exists(dst):
999 os.remove(dst)
1000 DieWithError('\nFailed to download hooks from %s' % src)
1001
1002
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001003@usage('[repo root containing codereview.settings]')
1004def CMDconfig(parser, args):
1005 """edit configuration for this tree"""
1006
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001007 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008 if len(args) == 0:
1009 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001010 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001011 return 0
1012
1013 url = args[0]
1014 if not url.endswith('codereview.settings'):
1015 url = os.path.join(url, 'codereview.settings')
1016
1017 # Load code review settings and download hooks (if available).
1018 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001019 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001020 return 0
1021
1022
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001023def CMDbaseurl(parser, args):
1024 """get or set base-url for this branch"""
1025 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1026 branch = ShortBranchName(branchref)
1027 _, args = parser.parse_args(args)
1028 if not args:
1029 print("Current base-url:")
1030 return RunGit(['config', 'branch.%s.base-url' % branch],
1031 error_ok=False).strip()
1032 else:
1033 print("Setting base-url to %s" % args[0])
1034 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1035 error_ok=False).strip()
1036
1037
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001038def CMDstatus(parser, args):
1039 """show status of changelists"""
1040 parser.add_option('--field',
1041 help='print only specific field (desc|id|patch|url)')
1042 (options, args) = parser.parse_args(args)
1043
1044 # TODO: maybe make show_branches a flag if necessary.
1045 show_branches = not options.field
1046
1047 if show_branches:
1048 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1049 if branches:
1050 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +00001051 changes = (Changelist(branchref=b) for b in branches.splitlines())
1052 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
1053 alignment = max(5, max(len(b) for b in branches))
1054 for branch in sorted(branches):
1055 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001056
1057 cl = Changelist()
1058 if options.field:
1059 if options.field.startswith('desc'):
1060 print cl.GetDescription()
1061 elif options.field == 'id':
1062 issueid = cl.GetIssue()
1063 if issueid:
1064 print issueid
1065 elif options.field == 'patch':
1066 patchset = cl.GetPatchset()
1067 if patchset:
1068 print patchset
1069 elif options.field == 'url':
1070 url = cl.GetIssueURL()
1071 if url:
1072 print url
1073 else:
1074 print
1075 print 'Current branch:',
1076 if not cl.GetIssue():
1077 print 'no issue assigned.'
1078 return 0
1079 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +00001080 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081 print 'Issue description:'
1082 print cl.GetDescription(pretty=True)
1083 return 0
1084
1085
1086@usage('[issue_number]')
1087def CMDissue(parser, args):
1088 """Set or display the current code review issue number.
1089
1090 Pass issue number 0 to clear the current issue.
1091"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001092 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001093
1094 cl = Changelist()
1095 if len(args) > 0:
1096 try:
1097 issue = int(args[0])
1098 except ValueError:
1099 DieWithError('Pass a number to set the issue or none to list it.\n'
1100 'Maybe you want to run git cl status?')
1101 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001102 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001103 return 0
1104
1105
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001106def CMDcomments(parser, args):
1107 """show review comments of the current changelist"""
1108 (_, args) = parser.parse_args(args)
1109 if args:
1110 parser.error('Unsupported argument: %s' % args)
1111
1112 cl = Changelist()
1113 if cl.GetIssue():
1114 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
1115 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001116 if message['disapproval']:
1117 color = Fore.RED
1118 elif message['approval']:
1119 color = Fore.GREEN
1120 elif message['sender'] == data['owner_email']:
1121 color = Fore.MAGENTA
1122 else:
1123 color = Fore.BLUE
1124 print '\n%s%s %s%s' % (
1125 color, message['date'].split('.', 1)[0], message['sender'],
1126 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001127 if message['text'].strip():
1128 print '\n'.join(' ' + l for l in message['text'].splitlines())
1129 return 0
1130
1131
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001132def CMDdescription(parser, args):
1133 """brings up the editor for the current CL's description."""
1134 cl = Changelist()
1135 if not cl.GetIssue():
1136 DieWithError('This branch has no associated changelist.')
1137 description = ChangeDescription(cl.GetDescription())
1138 description.prompt()
1139 cl.UpdateDescription(description.description)
1140 return 0
1141
1142
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143def CreateDescriptionFromLog(args):
1144 """Pulls out the commit log to use as a base for the CL description."""
1145 log_args = []
1146 if len(args) == 1 and not args[0].endswith('.'):
1147 log_args = [args[0] + '..']
1148 elif len(args) == 1 and args[0].endswith('...'):
1149 log_args = [args[0][:-1]]
1150 elif len(args) == 2:
1151 log_args = [args[0] + '..' + args[1]]
1152 else:
1153 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001154 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001155
1156
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001157def CMDpresubmit(parser, args):
1158 """run presubmit tests on the current changelist"""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001159 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001161 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001162 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001163 (options, args) = parser.parse_args(args)
1164
ukai@chromium.org259e4682012-10-25 07:36:33 +00001165 if not options.force and is_dirty_git_tree('presubmit'):
1166 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001167 return 1
1168
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001169 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170 if args:
1171 base_branch = args[0]
1172 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001173 # Default to diffing against the common ancestor of the upstream branch.
1174 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001175
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001176 cl.RunHook(
1177 committing=not options.upload,
1178 may_prompt=False,
1179 verbose=options.verbose,
1180 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001181 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001182
1183
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001184def AddChangeIdToCommitMessage(options, args):
1185 """Re-commits using the current message, assumes the commit hook is in
1186 place.
1187 """
1188 log_desc = options.message or CreateDescriptionFromLog(args)
1189 git_command = ['commit', '--amend', '-m', log_desc]
1190 RunGit(git_command)
1191 new_log_desc = CreateDescriptionFromLog(args)
1192 if CHANGE_ID in new_log_desc:
1193 print 'git-cl: Added Change-Id to commit message.'
1194 else:
1195 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1196
1197
ukai@chromium.orge8077812012-02-03 03:41:46 +00001198def GerritUpload(options, args, cl):
1199 """upload the current branch to gerrit."""
1200 # We assume the remote called "origin" is the one we want.
1201 # It is probably not worthwhile to support different workflows.
1202 remote = 'origin'
1203 branch = 'master'
1204 if options.target_branch:
1205 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001206
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001207 change_desc = ChangeDescription(
1208 options.message or CreateDescriptionFromLog(args))
1209 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001210 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001212 if CHANGE_ID not in change_desc.description:
1213 AddChangeIdToCommitMessage(options, args)
1214 if options.reviewers:
1215 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001216
ukai@chromium.orge8077812012-02-03 03:41:46 +00001217 receive_options = []
1218 cc = cl.GetCCList().split(',')
1219 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001220 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001221 cc = filter(None, cc)
1222 if cc:
1223 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001224 if change_desc.get_reviewers():
1225 receive_options.extend(
1226 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227
ukai@chromium.orge8077812012-02-03 03:41:46 +00001228 git_command = ['push']
1229 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001230 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001231 ' '.join(receive_options))
1232 git_command += [remote, 'HEAD:refs/for/' + branch]
1233 RunGit(git_command)
1234 # TODO(ukai): parse Change-Id: and set issue number?
1235 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001236
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001237
ukai@chromium.orge8077812012-02-03 03:41:46 +00001238def RietveldUpload(options, args, cl):
1239 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001240 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1241 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001242 if options.emulate_svn_auto_props:
1243 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001244
1245 change_desc = None
1246
1247 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001248 if options.title:
1249 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001250 if options.message:
1251 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001252 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001253 print ("This branch is associated with issue %s. "
1254 "Adding patch to that issue." % cl.GetIssue())
1255 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001256 if options.title:
1257 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001258 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001259 change_desc = ChangeDescription(message)
1260 if options.reviewers:
1261 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001262 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001263 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001264
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001265 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001266 print "Description is empty; aborting."
1267 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001268
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001269 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001270 if change_desc.get_reviewers():
1271 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001272 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001273 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001274 DieWithError("Must specify reviewers to send email.")
1275 upload_args.append('--send_mail')
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001276 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001277 if cc:
1278 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001280 if options.private or settings.GetDefaultPrivateFlag() == "True":
1281 upload_args.append('--private')
1282
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001283 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001284 if not options.find_copies:
1285 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001286
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001287 # Include the upstream repo's URL in the change -- this is useful for
1288 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001289 remote_url = cl.GetGitBaseUrlFromConfig()
1290 if not remote_url:
1291 if settings.GetIsGitSvn():
1292 # URL is dependent on the current directory.
1293 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1294 if data:
1295 keys = dict(line.split(': ', 1) for line in data.splitlines()
1296 if ': ' in line)
1297 remote_url = keys.get('URL', None)
1298 else:
1299 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1300 remote_url = (cl.GetRemoteUrl() + '@'
1301 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001302 if remote_url:
1303 upload_args.extend(['--base_url', remote_url])
1304
1305 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001306 upload_args = ['upload'] + upload_args + args
1307 logging.info('upload.RealMain(%s)', upload_args)
1308 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001309 except KeyboardInterrupt:
1310 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001311 except:
1312 # If we got an exception after the user typed a description for their
1313 # change, back up the description before re-raising.
1314 if change_desc:
1315 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1316 print '\nGot exception while uploading -- saving description to %s\n' \
1317 % backup_path
1318 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001319 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001320 backup_file.close()
1321 raise
1322
1323 if not cl.GetIssue():
1324 cl.SetIssue(issue)
1325 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001326
1327 if options.use_commit_queue:
1328 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001329 return 0
1330
1331
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001332def cleanup_list(l):
1333 """Fixes a list so that comma separated items are put as individual items.
1334
1335 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1336 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1337 """
1338 items = sum((i.split(',') for i in l), [])
1339 stripped_items = (i.strip() for i in items)
1340 return sorted(filter(None, stripped_items))
1341
1342
ukai@chromium.orge8077812012-02-03 03:41:46 +00001343@usage('[args to "git diff"]')
1344def CMDupload(parser, args):
1345 """upload the current changelist to codereview"""
1346 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1347 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001348 parser.add_option('--bypass-watchlists', action='store_true',
1349 dest='bypass_watchlists',
1350 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001351 parser.add_option('-f', action='store_true', dest='force',
1352 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001353 parser.add_option('-m', dest='message', help='message for patchset')
1354 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001355 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001356 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001357 help='reviewer email addresses')
1358 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001359 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001360 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001361 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001362 help='send email to reviewer immediately')
1363 parser.add_option("--emulate_svn_auto_props", action="store_true",
1364 dest="emulate_svn_auto_props",
1365 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001366 parser.add_option('-c', '--use-commit-queue', action='store_true',
1367 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001368 parser.add_option('--private', action='store_true',
1369 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001370 parser.add_option('--target_branch',
1371 help='When uploading to gerrit, remote branch to '
1372 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001373 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001374 (options, args) = parser.parse_args(args)
1375
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001376 if options.target_branch and not settings.GetIsGerrit():
1377 parser.error('Use --target_branch for non gerrit repository.')
1378
ukai@chromium.org259e4682012-10-25 07:36:33 +00001379 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001380 return 1
1381
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001382 options.reviewers = cleanup_list(options.reviewers)
1383 options.cc = cleanup_list(options.cc)
1384
ukai@chromium.orge8077812012-02-03 03:41:46 +00001385 cl = Changelist()
1386 if args:
1387 # TODO(ukai): is it ok for gerrit case?
1388 base_branch = args[0]
1389 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001390 # Default to diffing against common ancestor of upstream branch
1391 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001392 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001393
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001394 # Apply watchlists on upload.
1395 change = cl.GetChange(base_branch, None)
1396 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1397 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001398 if not options.bypass_watchlists:
1399 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001400
ukai@chromium.orge8077812012-02-03 03:41:46 +00001401 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001402 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001403 may_prompt=not options.force,
1404 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001405 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001406 if not hook_results.should_continue():
1407 return 1
1408 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001409 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001410
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001411 if cl.GetIssue():
1412 latest_patchset = cl.GetMostRecentPatchset(cl.GetIssue())
1413 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001414 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001415 print ('The last upload made from this repository was patchset #%d but '
1416 'the most recent patchset on the server is #%d.'
1417 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001418 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1419 'from another machine or branch the patch you\'re uploading now '
1420 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001421 ask_for_data('About to upload; enter to confirm.')
1422
iannucci@chromium.org79540052012-10-19 23:15:26 +00001423 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001424 if settings.GetIsGerrit():
1425 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001426 ret = RietveldUpload(options, args, cl)
1427 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001428 git_set_branch_value('last-upload-hash',
1429 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001430
1431 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001432
1433
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001434def IsSubmoduleMergeCommit(ref):
1435 # When submodules are added to the repo, we expect there to be a single
1436 # non-git-svn merge commit at remote HEAD with a signature comment.
1437 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001438 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001439 return RunGit(cmd) != ''
1440
1441
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001442def SendUpstream(parser, args, cmd):
1443 """Common code for CmdPush and CmdDCommit
1444
1445 Squashed commit into a single.
1446 Updates changelog with metadata (e.g. pointer to review).
1447 Pushes/dcommits the code upstream.
1448 Updates review and closes.
1449 """
1450 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1451 help='bypass upload presubmit hook')
1452 parser.add_option('-m', dest='message',
1453 help="override review description")
1454 parser.add_option('-f', action='store_true', dest='force',
1455 help="force yes to questions (don't prompt)")
1456 parser.add_option('-c', dest='contributor',
1457 help="external contributor for patch (appended to " +
1458 "description and used as author for git). Should be " +
1459 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001460 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001461 (options, args) = parser.parse_args(args)
1462 cl = Changelist()
1463
1464 if not args or cmd == 'push':
1465 # Default to merging against our best guess of the upstream branch.
1466 args = [cl.GetUpstreamBranch()]
1467
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001468 if options.contributor:
1469 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1470 print "Please provide contibutor as 'First Last <email@example.com>'"
1471 return 1
1472
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001473 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001474 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475
ukai@chromium.org259e4682012-10-25 07:36:33 +00001476 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001477 return 1
1478
1479 # This rev-list syntax means "show all commits not in my branch that
1480 # are in base_branch".
1481 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1482 base_branch]).splitlines()
1483 if upstream_commits:
1484 print ('Base branch "%s" has %d commits '
1485 'not in this branch.' % (base_branch, len(upstream_commits)))
1486 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1487 return 1
1488
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001489 # This is the revision `svn dcommit` will commit on top of.
1490 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1491 '--pretty=format:%H'])
1492
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001493 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001494 # If the base_head is a submodule merge commit, the first parent of the
1495 # base_head should be a git-svn commit, which is what we're interested in.
1496 base_svn_head = base_branch
1497 if base_has_submodules:
1498 base_svn_head += '^1'
1499
1500 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001501 if extra_commits:
1502 print ('This branch has %d additional commits not upstreamed yet.'
1503 % len(extra_commits.splitlines()))
1504 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1505 'before attempting to %s.' % (base_branch, cmd))
1506 return 1
1507
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001508 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001509 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001510 author = None
1511 if options.contributor:
1512 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001513 hook_results = cl.RunHook(
1514 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001515 may_prompt=not options.force,
1516 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001517 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001518 if not hook_results.should_continue():
1519 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001520
1521 if cmd == 'dcommit':
1522 # Check the tree status if the tree status URL is set.
1523 status = GetTreeStatus()
1524 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001525 print('The tree is closed. Please wait for it to reopen. Use '
1526 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001527 return 1
1528 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001529 print('Unable to determine tree status. Please verify manually and '
1530 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001531 else:
1532 breakpad.SendStack(
1533 'GitClHooksBypassedCommit',
1534 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001535 (cl.GetRietveldServer(), cl.GetIssue()),
1536 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001537
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001538 change_desc = ChangeDescription(options.message)
1539 if not change_desc.description and cl.GetIssue():
1540 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001541
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001542 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001543 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001544 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001545 else:
1546 print 'No description set.'
1547 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1548 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001549
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001550 # Keep a separate copy for the commit message, because the commit message
1551 # contains the link to the Rietveld issue, while the Rietveld message contains
1552 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001553 # Keep a separate copy for the commit message.
1554 if cl.GetIssue():
1555 change_desc.update_reviewers(cl.GetApprovingReviewers(cl.GetIssue()))
1556
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001557 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001558 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001559 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001560 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001561 commit_desc.append_footer('Patch from %s.' % options.contributor)
1562
1563 print 'Description:', repr(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001564
1565 branches = [base_branch, cl.GetBranchRef()]
1566 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001567 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001568 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001569
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001570 # We want to squash all this branch's commits into one commit with the proper
1571 # description. We do this by doing a "reset --soft" to the base branch (which
1572 # keeps the working copy the same), then dcommitting that. If origin/master
1573 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1574 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001575 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001576 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1577 # Delete the branches if they exist.
1578 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1579 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1580 result = RunGitWithCode(showref_cmd)
1581 if result[0] == 0:
1582 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001583
1584 # We might be in a directory that's present in this branch but not in the
1585 # trunk. Move up to the top of the tree so that git commands that expect a
1586 # valid CWD won't fail after we check out the merge branch.
1587 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1588 if rel_base_path:
1589 os.chdir(rel_base_path)
1590
1591 # Stuff our change into the merge branch.
1592 # We wrap in a try...finally block so if anything goes wrong,
1593 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001594 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001595 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001596 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1597 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001598 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001599 RunGit(
1600 [
1601 'commit', '--author', options.contributor,
1602 '-m', commit_desc.description,
1603 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001604 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001605 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001606 if base_has_submodules:
1607 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1608 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1609 RunGit(['checkout', CHERRY_PICK_BRANCH])
1610 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001611 if cmd == 'push':
1612 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001613 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001614 retcode, output = RunGitWithCode(
1615 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1616 logging.debug(output)
1617 else:
1618 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001619 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001620 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001621 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001622 finally:
1623 # And then swap back to the original branch and clean up.
1624 RunGit(['checkout', '-q', cl.GetBranch()])
1625 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001626 if base_has_submodules:
1627 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001628
1629 if cl.GetIssue():
1630 if cmd == 'dcommit' and 'Committed r' in output:
1631 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1632 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001633 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1634 for l in output.splitlines(False))
1635 match = filter(None, match)
1636 if len(match) != 1:
1637 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1638 output)
1639 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001640 else:
1641 return 1
1642 viewvc_url = settings.GetViewVCUrl()
1643 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001644 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001645 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001646 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001647 print ('Closing issue '
1648 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001649 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001650 cl.CloseIssue()
iannucci@chromium.org16b51402013-02-17 05:33:36 +00001651 props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False)
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001652 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001653 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001654 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1655 cl.RpcServer().add_comment(cl.GetIssue(), comment)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001656 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001657
1658 if retcode == 0:
1659 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1660 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001661 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001662
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001663 return 0
1664
1665
1666@usage('[upstream branch to apply against]')
1667def CMDdcommit(parser, args):
1668 """commit the current changelist via git-svn"""
1669 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001670 message = """This doesn't appear to be an SVN repository.
1671If your project has a git mirror with an upstream SVN master, you probably need
1672to run 'git svn init', see your project's git mirror documentation.
1673If your project has a true writeable upstream repository, you probably want
1674to run 'git cl push' instead.
1675Choose wisely, if you get this wrong, your commit might appear to succeed but
1676will instead be silently ignored."""
1677 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001678 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001679 return SendUpstream(parser, args, 'dcommit')
1680
1681
1682@usage('[upstream branch to apply against]')
1683def CMDpush(parser, args):
1684 """commit the current changelist via git"""
1685 if settings.GetIsGitSvn():
1686 print('This appears to be an SVN repository.')
1687 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001688 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001689 return SendUpstream(parser, args, 'push')
1690
1691
1692@usage('<patch url or issue id>')
1693def CMDpatch(parser, args):
1694 """patch in a code review"""
1695 parser.add_option('-b', dest='newbranch',
1696 help='create a new branch off trunk for the patch')
1697 parser.add_option('-f', action='store_true', dest='force',
1698 help='with -b, clobber any existing branch')
1699 parser.add_option('--reject', action='store_true', dest='reject',
tapted@chromium.org0bdc2652013-06-07 23:47:05 +00001700 help='allow failed patches and spew .rej files')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001701 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1702 help="don't commit after patch applies")
1703 (options, args) = parser.parse_args(args)
1704 if len(args) != 1:
1705 parser.print_help()
1706 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001707 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001708
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001709 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001710 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001711
maruel@chromium.org52424302012-08-29 15:14:30 +00001712 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001713 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001714 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001715 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001716 patchset = cl.GetMostRecentPatchset(issue)
1717 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001718 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001719 # Assume it's a URL to the patch. Default to https.
1720 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001721 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001722 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001723 DieWithError('Must pass an issue ID or full URL for '
1724 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001725 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001726 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001727 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001728
1729 if options.newbranch:
1730 if options.force:
1731 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001732 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001733 RunGit(['checkout', '-b', options.newbranch,
1734 Changelist().GetUpstreamBranch()])
1735
1736 # Switch up to the top-level directory, if necessary, in preparation for
1737 # applying the patch.
1738 top = RunGit(['rev-parse', '--show-cdup']).strip()
1739 if top:
1740 os.chdir(top)
1741
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001742 # Git patches have a/ at the beginning of source paths. We strip that out
1743 # with a sed script rather than the -p flag to patch so we can feed either
1744 # Git or svn-style patches into the same apply command.
1745 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001746 try:
1747 patch_data = subprocess2.check_output(
1748 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1749 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001750 DieWithError('Git patch mungling failed.')
1751 logging.info(patch_data)
1752 # We use "git apply" to apply the patch instead of "patch" so that we can
1753 # pick up file adds.
1754 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001755 cmd = ['git', '--no-pager', 'apply', '--index', '-p0']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001756 if options.reject:
1757 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001758 try:
1759 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1760 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001761 DieWithError('Failed to apply the patch')
1762
1763 # If we had an issue, commit the current state and register the issue.
1764 if not options.nocommit:
1765 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1766 cl = Changelist()
1767 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001768 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001769 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001770 else:
1771 print "Patch applied to index."
1772 return 0
1773
1774
1775def CMDrebase(parser, args):
1776 """rebase current branch on top of svn repo"""
1777 # Provide a wrapper for git svn rebase to help avoid accidental
1778 # git svn dcommit.
1779 # It's the only command that doesn't use parser at all since we just defer
1780 # execution to git-svn.
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001781 return subprocess2.call(['git', '--no-pager', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001782
1783
1784def GetTreeStatus():
1785 """Fetches the tree status and returns either 'open', 'closed',
1786 'unknown' or 'unset'."""
1787 url = settings.GetTreeStatusUrl(error_ok=True)
1788 if url:
1789 status = urllib2.urlopen(url).read().lower()
1790 if status.find('closed') != -1 or status == '0':
1791 return 'closed'
1792 elif status.find('open') != -1 or status == '1':
1793 return 'open'
1794 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001795 return 'unset'
1796
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001797
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001798def GetTreeStatusReason():
1799 """Fetches the tree status from a json url and returns the message
1800 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001801 url = settings.GetTreeStatusUrl()
1802 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001803 connection = urllib2.urlopen(json_url)
1804 status = json.loads(connection.read())
1805 connection.close()
1806 return status['message']
1807
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001808
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001809def CMDtree(parser, args):
1810 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001811 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001812 status = GetTreeStatus()
1813 if 'unset' == status:
1814 print 'You must configure your tree status URL by running "git cl config".'
1815 return 2
1816
1817 print "The tree is %s" % status
1818 print
1819 print GetTreeStatusReason()
1820 if status != 'open':
1821 return 1
1822 return 0
1823
1824
maruel@chromium.org15192402012-09-06 12:38:29 +00001825def CMDtry(parser, args):
1826 """Triggers a try job through Rietveld."""
1827 group = optparse.OptionGroup(parser, "Try job options")
1828 group.add_option(
1829 "-b", "--bot", action="append",
1830 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1831 "times to specify multiple builders. ex: "
1832 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1833 "the try server waterfall for the builders name and the tests "
1834 "available. Can also be used to specify gtest_filter, e.g. "
1835 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1836 group.add_option(
1837 "-r", "--revision",
1838 help="Revision to use for the try job; default: the "
1839 "revision will be determined by the try server; see "
1840 "its waterfall for more info")
1841 group.add_option(
1842 "-c", "--clobber", action="store_true", default=False,
1843 help="Force a clobber before building; e.g. don't do an "
1844 "incremental build")
1845 group.add_option(
1846 "--project",
1847 help="Override which project to use. Projects are defined "
1848 "server-side to define what default bot set to use")
1849 group.add_option(
1850 "-t", "--testfilter", action="append", default=[],
1851 help=("Apply a testfilter to all the selected builders. Unless the "
1852 "builders configurations are similar, use multiple "
1853 "--bot <builder>:<test> arguments."))
1854 group.add_option(
1855 "-n", "--name", help="Try job name; default to current branch name")
1856 parser.add_option_group(group)
1857 options, args = parser.parse_args(args)
1858
1859 if args:
1860 parser.error('Unknown arguments: %s' % args)
1861
1862 cl = Changelist()
1863 if not cl.GetIssue():
1864 parser.error('Need to upload first')
1865
1866 if not options.name:
1867 options.name = cl.GetBranch()
1868
1869 # Process --bot and --testfilter.
1870 if not options.bot:
1871 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001872 change = cl.GetChange(
1873 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1874 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001875 options.bot = presubmit_support.DoGetTrySlaves(
1876 change,
1877 change.LocalPaths(),
1878 settings.GetRoot(),
1879 None,
1880 None,
1881 options.verbose,
1882 sys.stdout)
1883 if not options.bot:
1884 parser.error('No default try builder to try, use --bot')
1885
1886 builders_and_tests = {}
1887 for bot in options.bot:
1888 if ':' in bot:
1889 builder, tests = bot.split(':', 1)
1890 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1891 elif ',' in bot:
1892 parser.error('Specify one bot per --bot flag')
1893 else:
1894 builders_and_tests.setdefault(bot, []).append('defaulttests')
1895
1896 if options.testfilter:
1897 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1898 builders_and_tests = dict(
1899 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1900 if t != ['compile'])
1901
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001902 if any('triggered' in b for b in builders_and_tests):
1903 print >> sys.stderr, (
1904 'ERROR You are trying to send a job to a triggered bot. This type of'
1905 ' bot requires an\ninitial job from a parent (usually a builder). '
1906 'Instead send your job to the parent.\n'
1907 'Bot list: %s' % builders_and_tests)
1908 return 1
1909
maruel@chromium.org15192402012-09-06 12:38:29 +00001910 patchset = cl.GetPatchset()
1911 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001912 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001913
1914 cl.RpcServer().trigger_try_jobs(
1915 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1916 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001917 print('Tried jobs on:')
1918 length = max(len(builder) for builder in builders_and_tests)
1919 for builder in sorted(builders_and_tests):
1920 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001921 return 0
1922
1923
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001924@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001925def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001926 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001927 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001928 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001929 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001930 return 0
1931
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001932 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001933 if args:
1934 # One arg means set upstream branch.
1935 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1936 cl = Changelist()
1937 print "Upstream branch set to " + cl.GetUpstreamBranch()
1938 else:
1939 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001940 return 0
1941
1942
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001943def CMDset_commit(parser, args):
1944 """set the commit bit"""
1945 _, args = parser.parse_args(args)
1946 if args:
1947 parser.error('Unrecognized args: %s' % ' '.join(args))
1948 cl = Changelist()
1949 cl.SetFlag('commit', '1')
1950 return 0
1951
1952
groby@chromium.org411034a2013-02-26 15:12:01 +00001953def CMDset_close(parser, args):
1954 """close the issue"""
1955 _, args = parser.parse_args(args)
1956 if args:
1957 parser.error('Unrecognized args: %s' % ' '.join(args))
1958 cl = Changelist()
1959 # Ensure there actually is an issue to close.
1960 cl.GetDescription()
1961 cl.CloseIssue()
1962 return 0
1963
1964
agable@chromium.orgfab8f822013-05-06 17:43:09 +00001965def CMDformat(parser, args):
1966 """run clang-format on the diff"""
1967 CLANG_EXTS = ['.cc', '.cpp', '.h']
1968 parser.add_option('--full', action='store_true', default=False)
1969 opts, args = parser.parse_args(args)
1970 if args:
1971 parser.error('Unrecognized args: %s' % ' '.join(args))
1972
digit@chromium.org29e47272013-05-17 17:01:46 +00001973 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00001974 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00001975 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00001976 # Only list the names of modified files.
1977 diff_cmd.append('--name-only')
1978 else:
1979 # Only generate context-less patches.
1980 diff_cmd.append('-U0')
1981
1982 # Grab the merge-base commit, i.e. the upstream commit of the current
1983 # branch when it was created or the last time it was rebased. This is
1984 # to cover the case where the user may have called "git fetch origin",
1985 # moving the origin branch to a newer commit, but hasn't rebased yet.
1986 upstream_commit = None
1987 cl = Changelist()
1988 upstream_branch = cl.GetUpstreamBranch()
1989 if upstream_branch:
1990 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
1991 upstream_commit = upstream_commit.strip()
1992
1993 if not upstream_commit:
1994 DieWithError('Could not find base commit for this branch. '
1995 'Are you in detached state?')
1996
1997 diff_cmd.append(upstream_commit)
1998
1999 # Handle source file filtering.
2000 diff_cmd.append('--')
2001 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2002 diff_output = RunGit(diff_cmd)
2003
2004 if opts.full:
2005 # diff_output is a list of files to send to clang-format.
2006 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002007 if not files:
2008 print "Nothing to format."
2009 return 0
digit@chromium.org29e47272013-05-17 17:01:46 +00002010 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002011 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002012 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002013 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2014 'clang-format-diff.py')
2015 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002016 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
2017 cmd = [sys.executable, cfd_path, '-style', 'Chromium']
2018 RunCommand(cmd, stdin=diff_output)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002019
2020 return 0
2021
2022
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002023### Glue code for subcommand handling.
2024
2025
2026def Commands():
2027 """Returns a dict of command and their handling function."""
2028 module = sys.modules[__name__]
2029 cmds = (fn[3:] for fn in dir(module) if fn.startswith('CMD'))
2030 return dict((cmd, getattr(module, 'CMD' + cmd)) for cmd in cmds)
2031
2032
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002033def Command(name):
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002034 """Retrieves the function to handle a command."""
2035 commands = Commands()
2036 if name in commands:
2037 return commands[name]
2038
2039 # Try to be smart and look if there's something similar.
2040 commands_with_prefix = [c for c in commands if c.startswith(name)]
2041 if len(commands_with_prefix) == 1:
2042 return commands[commands_with_prefix[0]]
2043
2044 # A #closeenough approximation of levenshtein distance.
2045 def close_enough(a, b):
2046 return difflib.SequenceMatcher(a=a, b=b).ratio()
2047
2048 hamming_commands = sorted(
2049 ((close_enough(c, name), c) for c in commands),
2050 reverse=True)
2051 if (hamming_commands[0][0] - hamming_commands[1][0]) < 0.3:
2052 # Too ambiguous.
2053 return
2054
2055 if hamming_commands[0][0] < 0.8:
2056 # Not similar enough. Don't be a fool and run a random command.
2057 return
2058
2059 return commands[hamming_commands[0][1]]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002060
2061
2062def CMDhelp(parser, args):
2063 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002064 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002065 if len(args) == 1:
2066 return main(args + ['--help'])
2067 parser.print_help()
2068 return 0
2069
2070
2071def GenUsage(parser, command):
2072 """Modify an OptParse object with the function's documentation."""
2073 obj = Command(command)
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002074 # Get back the real command name in case Command() guess the actual command
2075 # name.
2076 command = obj.__name__[3:]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002077 more = getattr(obj, 'usage_more', '')
2078 if command == 'help':
2079 command = '<command>'
2080 else:
2081 # OptParser.description prefer nicely non-formatted strings.
2082 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
2083 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
2084
2085
2086def main(argv):
2087 """Doesn't parse the arguments here, just find the right subcommand to
2088 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002089 if sys.hexversion < 0x02060000:
2090 print >> sys.stderr, (
2091 '\nYour python version %s is unsupported, please upgrade.\n' %
2092 sys.version.split(' ', 1)[0])
2093 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002094
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002095 # Reload settings.
2096 global settings
2097 settings = Settings()
2098
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002099 # Do it late so all commands are listed.
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002100 commands = Commands()
2101 length = max(len(c) for c in commands)
2102 docs = sorted(
2103 (name, handler.__doc__.split('\n')[0].strip())
2104 for name, handler in commands.iteritems())
2105 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join(
2106 ' %-*s %s' % (length, name, doc) for name, doc in docs))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002107
2108 # Create the option parse and add --verbose support.
2109 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002110 parser.add_option(
2111 '-v', '--verbose', action='count', default=0,
2112 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002113 old_parser_args = parser.parse_args
2114 def Parse(args):
2115 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002116 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002117 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002118 elif options.verbose:
2119 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002120 else:
2121 logging.basicConfig(level=logging.WARNING)
2122 return options, args
2123 parser.parse_args = Parse
2124
2125 if argv:
2126 command = Command(argv[0])
2127 if command:
2128 # "fix" the usage and the description now that we know the subcommand.
2129 GenUsage(parser, argv[0])
2130 try:
2131 return command(parser, argv[1:])
2132 except urllib2.HTTPError, e:
2133 if e.code != 500:
2134 raise
2135 DieWithError(
2136 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2137 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
2138
2139 # Not a known command. Default to help.
2140 GenUsage(parser, 'help')
2141 return CMDhelp(parser, argv)
2142
2143
2144if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002145 # These affect sys.stdout so do it outside of main() to simplify mocks in
2146 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002147 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002148 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002149 sys.exit(main(sys.argv[1:]))