blob: 07ecd32ee2654bf8e1bc8b71435745dc211346eb [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
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000011import glob
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000012import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000013import logging
14import optparse
15import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000016import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000018import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import textwrap
maruel@chromium.org1033efd2013-07-23 23:25:09 +000021import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000023import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000024import webbrowser
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025
26try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000027 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028except ImportError:
29 pass
30
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000032from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033from third_party import upload
34import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000035import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000036import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000037import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000038import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000039import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000040import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000041import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042import watchlists
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000043import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000044
maruel@chromium.org0633fb42013-08-16 20:06:14 +000045__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000046
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000047DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000048POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000049DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000050GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000051CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000052
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000053# Shortcut since it quickly becomes redundant.
54Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000055
maruel@chromium.orgddd59412011-11-30 14:20:38 +000056# Initialized in main()
57settings = None
58
59
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000060def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000061 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000062 sys.exit(1)
63
64
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000065def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000066 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000067 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000068 except subprocess2.CalledProcessError as e:
69 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000070 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000071 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000072 'Command "%s" failed.\n%s' % (
73 ' '.join(args), error_message or e.stdout or ''))
74 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000075
76
77def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000078 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000079 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000080
81
82def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000083 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000084 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +000085 env = os.environ.copy()
86 # 'cat' is a magical git string that disables pagers on all platforms.
87 env['GIT_PAGER'] = 'cat'
88 out, code = subprocess2.communicate(['git'] + args,
89 env=env,
bratell@opera.comf267b0e2013-05-02 09:11:43 +000090 stdout=subprocess2.PIPE)
szager@chromium.org9bb85e22012-06-13 20:28:23 +000091 return code, out[0]
92 except ValueError:
93 # When the subprocess fails, it returns None. That triggers a ValueError
94 # when trying to unpack the return value into (out, code).
95 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000096
97
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000098def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +000099 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000100 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000101 return (version.startswith(prefix) and
102 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000103
104
maruel@chromium.org90541732011-04-01 17:54:18 +0000105def ask_for_data(prompt):
106 try:
107 return raw_input(prompt)
108 except KeyboardInterrupt:
109 # Hide the exception.
110 sys.exit(1)
111
112
iannucci@chromium.org79540052012-10-19 23:15:26 +0000113def git_set_branch_value(key, value):
114 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000115 if not branch:
116 return
117
118 cmd = ['config']
119 if isinstance(value, int):
120 cmd.append('--int')
121 git_key = 'branch.%s.%s' % (branch, key)
122 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000123
124
125def git_get_branch_default(key, default):
126 branch = Changelist().GetBranch()
127 if branch:
128 git_key = 'branch.%s.%s' % (branch, key)
129 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
130 try:
131 return int(stdout.strip())
132 except ValueError:
133 pass
134 return default
135
136
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000137def add_git_similarity(parser):
138 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000139 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000140 help='Sets the percentage that a pair of files need to match in order to'
141 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000142 parser.add_option(
143 '--find-copies', action='store_true',
144 help='Allows git to look for copies.')
145 parser.add_option(
146 '--no-find-copies', action='store_false', dest='find_copies',
147 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000148
149 old_parser_args = parser.parse_args
150 def Parse(args):
151 options, args = old_parser_args(args)
152
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000153 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000154 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000155 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000156 print('Note: Saving similarity of %d%% in git config.'
157 % options.similarity)
158 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000159
iannucci@chromium.org79540052012-10-19 23:15:26 +0000160 options.similarity = max(0, min(options.similarity, 100))
161
162 if options.find_copies is None:
163 options.find_copies = bool(
164 git_get_branch_default('git-find-copies', True))
165 else:
166 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000167
168 print('Using %d%% similarity for rename/copy detection. '
169 'Override with --similarity.' % options.similarity)
170
171 return options, args
172 parser.parse_args = Parse
173
174
ukai@chromium.org259e4682012-10-25 07:36:33 +0000175def is_dirty_git_tree(cmd):
176 # Make sure index is up-to-date before running diff-index.
177 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
178 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
179 if dirty:
180 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
181 print 'Uncommitted files: (git diff-index --name-status HEAD)'
182 print dirty[:4096]
183 if len(dirty) > 4096:
184 print '... (run "git diff-index --name-status HEAD" to see full output).'
185 return True
186 return False
187
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000188
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000189def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
190 """Return the corresponding git ref if |base_url| together with |glob_spec|
191 matches the full |url|.
192
193 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
194 """
195 fetch_suburl, as_ref = glob_spec.split(':')
196 if allow_wildcards:
197 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
198 if glob_match:
199 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
200 # "branches/{472,597,648}/src:refs/remotes/svn/*".
201 branch_re = re.escape(base_url)
202 if glob_match.group(1):
203 branch_re += '/' + re.escape(glob_match.group(1))
204 wildcard = glob_match.group(2)
205 if wildcard == '*':
206 branch_re += '([^/]*)'
207 else:
208 # Escape and replace surrounding braces with parentheses and commas
209 # with pipe symbols.
210 wildcard = re.escape(wildcard)
211 wildcard = re.sub('^\\\\{', '(', wildcard)
212 wildcard = re.sub('\\\\,', '|', wildcard)
213 wildcard = re.sub('\\\\}$', ')', wildcard)
214 branch_re += wildcard
215 if glob_match.group(3):
216 branch_re += re.escape(glob_match.group(3))
217 match = re.match(branch_re, url)
218 if match:
219 return re.sub('\*$', match.group(1), as_ref)
220
221 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
222 if fetch_suburl:
223 full_url = base_url + '/' + fetch_suburl
224 else:
225 full_url = base_url
226 if full_url == url:
227 return as_ref
228 return None
229
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000230
iannucci@chromium.org79540052012-10-19 23:15:26 +0000231def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000232 """Prints statistics about the change to the user."""
233 # --no-ext-diff is broken in some versions of Git, so try to work around
234 # this by overriding the environment (but there is still a problem if the
235 # git config key "diff.external" is used).
236 env = os.environ.copy()
237 if 'GIT_EXTERNAL_DIFF' in env:
238 del env['GIT_EXTERNAL_DIFF']
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000239 # 'cat' is a magical git string that disables pagers on all platforms.
240 env['GIT_PAGER'] = 'cat'
iannucci@chromium.org79540052012-10-19 23:15:26 +0000241
242 if find_copies:
243 similarity_options = ['--find-copies-harder', '-l100000',
244 '-C%s' % similarity]
245 else:
246 similarity_options = ['-M%s' % similarity]
247
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000248 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000249 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000250 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000251 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000252
253
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000254class Settings(object):
255 def __init__(self):
256 self.default_server = None
257 self.cc = None
258 self.root = None
259 self.is_git_svn = None
260 self.svn_branch = None
261 self.tree_status_url = None
262 self.viewvc_url = None
263 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000264 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000265 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000266
267 def LazyUpdateIfNeeded(self):
268 """Updates the settings from a codereview.settings file, if available."""
269 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000270 # The only value that actually changes the behavior is
271 # autoupdate = "false". Everything else means "true".
272 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
273 error_ok=True
274 ).strip().lower()
275
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000276 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000277 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000278 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000279 # set updated to True to avoid infinite calling loop
280 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000281 self.updated = True
282 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000283 self.updated = True
284
285 def GetDefaultServerUrl(self, error_ok=False):
286 if not self.default_server:
287 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000288 self.default_server = gclient_utils.UpgradeToHttps(
289 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000290 if error_ok:
291 return self.default_server
292 if not self.default_server:
293 error_message = ('Could not find settings file. You must configure '
294 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000295 self.default_server = gclient_utils.UpgradeToHttps(
296 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000297 return self.default_server
298
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000299 def GetRoot(self):
300 if not self.root:
301 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
302 return self.root
303
304 def GetIsGitSvn(self):
305 """Return true if this repo looks like it's using git-svn."""
306 if self.is_git_svn is None:
307 # If you have any "svn-remote.*" config keys, we think you're using svn.
308 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000309 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000310 return self.is_git_svn
311
312 def GetSVNBranch(self):
313 if self.svn_branch is None:
314 if not self.GetIsGitSvn():
315 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
316
317 # Try to figure out which remote branch we're based on.
318 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000319 # 1) iterate through our branch history and find the svn URL.
320 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000321
322 # regexp matching the git-svn line that contains the URL.
323 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
324
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000325 env = os.environ.copy()
326 # 'cat' is a magical git string that disables pagers on all platforms.
327 env['GIT_PAGER'] = 'cat'
328
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000329 # We don't want to go through all of history, so read a line from the
330 # pipe at a time.
331 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000332 cmd = ['git', 'log', '-100', '--pretty=medium']
333 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE, env=env)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000334 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000335 for line in proc.stdout:
336 match = git_svn_re.match(line)
337 if match:
338 url = match.group(1)
339 proc.stdout.close() # Cut pipe.
340 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000341
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000342 if url:
343 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
344 remotes = RunGit(['config', '--get-regexp',
345 r'^svn-remote\..*\.url']).splitlines()
346 for remote in remotes:
347 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000348 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000349 remote = match.group(1)
350 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000351 rewrite_root = RunGit(
352 ['config', 'svn-remote.%s.rewriteRoot' % remote],
353 error_ok=True).strip()
354 if rewrite_root:
355 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000356 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000357 ['config', 'svn-remote.%s.fetch' % remote],
358 error_ok=True).strip()
359 if fetch_spec:
360 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
361 if self.svn_branch:
362 break
363 branch_spec = RunGit(
364 ['config', 'svn-remote.%s.branches' % remote],
365 error_ok=True).strip()
366 if branch_spec:
367 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
368 if self.svn_branch:
369 break
370 tag_spec = RunGit(
371 ['config', 'svn-remote.%s.tags' % remote],
372 error_ok=True).strip()
373 if tag_spec:
374 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
375 if self.svn_branch:
376 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000377
378 if not self.svn_branch:
379 DieWithError('Can\'t guess svn branch -- try specifying it on the '
380 'command line')
381
382 return self.svn_branch
383
384 def GetTreeStatusUrl(self, error_ok=False):
385 if not self.tree_status_url:
386 error_message = ('You must configure your tree status URL by running '
387 '"git cl config".')
388 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
389 error_ok=error_ok,
390 error_message=error_message)
391 return self.tree_status_url
392
393 def GetViewVCUrl(self):
394 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000395 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000396 return self.viewvc_url
397
rmistry@google.com90752582014-01-14 21:04:50 +0000398 def GetBugPrefix(self):
399 return self._GetConfig('rietveld.bug-prefix', error_ok=True)
400
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000401 def GetDefaultCCList(self):
402 return self._GetConfig('rietveld.cc', error_ok=True)
403
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000404 def GetDefaultPrivateFlag(self):
405 return self._GetConfig('rietveld.private', error_ok=True)
406
ukai@chromium.orge8077812012-02-03 03:41:46 +0000407 def GetIsGerrit(self):
408 """Return true if this repo is assosiated with gerrit code review system."""
409 if self.is_gerrit is None:
410 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
411 return self.is_gerrit
412
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000413 def GetGitEditor(self):
414 """Return the editor specified in the git config, or None if none is."""
415 if self.git_editor is None:
416 self.git_editor = self._GetConfig('core.editor', error_ok=True)
417 return self.git_editor or None
418
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000419 def _GetConfig(self, param, **kwargs):
420 self.LazyUpdateIfNeeded()
421 return RunGit(['config', param], **kwargs).strip()
422
423
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000424def ShortBranchName(branch):
425 """Convert a name like 'refs/heads/foo' to just 'foo'."""
426 return branch.replace('refs/heads/', '')
427
428
429class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000430 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000431 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000432 global settings
433 if not settings:
434 # Happens when git_cl.py is used as a utility library.
435 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000436 settings.GetDefaultServerUrl()
437 self.branchref = branchref
438 if self.branchref:
439 self.branch = ShortBranchName(self.branchref)
440 else:
441 self.branch = None
442 self.rietveld_server = None
443 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000444 self.lookedup_issue = False
445 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000446 self.has_description = False
447 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000448 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000449 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000450 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000451 self.cc = None
452 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000453 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000454 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000455
456 def GetCCList(self):
457 """Return the users cc'd on this CL.
458
459 Return is a string suitable for passing to gcl with the --cc flag.
460 """
461 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000462 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000463 more_cc = ','.join(self.watchers)
464 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
465 return self.cc
466
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000467 def GetCCListWithoutDefault(self):
468 """Return the users cc'd on this CL excluding default ones."""
469 if self.cc is None:
470 self.cc = ','.join(self.watchers)
471 return self.cc
472
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000473 def SetWatchers(self, watchers):
474 """Set the list of email addresses that should be cc'd based on the changed
475 files in this CL.
476 """
477 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000478
479 def GetBranch(self):
480 """Returns the short branch name, e.g. 'master'."""
481 if not self.branch:
482 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
483 self.branch = ShortBranchName(self.branchref)
484 return self.branch
485
486 def GetBranchRef(self):
487 """Returns the full branch name, e.g. 'refs/heads/master'."""
488 self.GetBranch() # Poke the lazy loader.
489 return self.branchref
490
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000491 @staticmethod
492 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000493 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000494 e.g. 'origin', 'refs/heads/master'
495 """
496 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000497 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
498 error_ok=True).strip()
499 if upstream_branch:
500 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
501 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000502 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
503 error_ok=True).strip()
504 if upstream_branch:
505 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000506 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000507 # Fall back on trying a git-svn upstream branch.
508 if settings.GetIsGitSvn():
509 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000510 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000511 # Else, try to guess the origin remote.
512 remote_branches = RunGit(['branch', '-r']).split()
513 if 'origin/master' in remote_branches:
514 # Fall back on origin/master if it exits.
515 remote = 'origin'
516 upstream_branch = 'refs/heads/master'
517 elif 'origin/trunk' in remote_branches:
518 # Fall back on origin/trunk if it exists. Generally a shared
519 # git-svn clone
520 remote = 'origin'
521 upstream_branch = 'refs/heads/trunk'
522 else:
523 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000524Either pass complete "git diff"-style arguments, like
525 git cl upload origin/master
526or verify this branch is set up to track another (via the --track argument to
527"git checkout -b ...").""")
528
529 return remote, upstream_branch
530
531 def GetUpstreamBranch(self):
532 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000533 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000534 if remote is not '.':
535 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
536 self.upstream_branch = upstream_branch
537 return self.upstream_branch
538
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000539 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000540 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000541 remote, branch = None, self.GetBranch()
542 seen_branches = set()
543 while branch not in seen_branches:
544 seen_branches.add(branch)
545 remote, branch = self.FetchUpstreamTuple(branch)
546 branch = ShortBranchName(branch)
547 if remote != '.' or branch.startswith('refs/remotes'):
548 break
549 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000550 remotes = RunGit(['remote'], error_ok=True).split()
551 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000552 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000553 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000554 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000555 logging.warning('Could not determine which remote this change is '
556 'associated with, so defaulting to "%s". This may '
557 'not be what you want. You may prevent this message '
558 'by running "git svn info" as documented here: %s',
559 self._remote,
560 GIT_INSTRUCTIONS_URL)
561 else:
562 logging.warn('Could not determine which remote this change is '
563 'associated with. You may prevent this message by '
564 'running "git svn info" as documented here: %s',
565 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000566 branch = 'HEAD'
567 if branch.startswith('refs/remotes'):
568 self._remote = (remote, branch)
569 else:
570 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000571 return self._remote
572
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000573 def GitSanityChecks(self, upstream_git_obj):
574 """Checks git repo status and ensures diff is from local commits."""
575
576 # Verify the commit we're diffing against is in our current branch.
577 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
578 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
579 if upstream_sha != common_ancestor:
580 print >> sys.stderr, (
581 'ERROR: %s is not in the current branch. You may need to rebase '
582 'your tracking branch' % upstream_sha)
583 return False
584
585 # List the commits inside the diff, and verify they are all local.
586 commits_in_diff = RunGit(
587 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
588 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
589 remote_branch = remote_branch.strip()
590 if code != 0:
591 _, remote_branch = self.GetRemoteBranch()
592
593 commits_in_remote = RunGit(
594 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
595
596 common_commits = set(commits_in_diff) & set(commits_in_remote)
597 if common_commits:
598 print >> sys.stderr, (
599 'ERROR: Your diff contains %d commits already in %s.\n'
600 'Run "git log --oneline %s..HEAD" to get a list of commits in '
601 'the diff. If you are using a custom git flow, you can override'
602 ' the reference used for this check with "git config '
603 'gitcl.remotebranch <git-ref>".' % (
604 len(common_commits), remote_branch, upstream_git_obj))
605 return False
606 return True
607
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000608 def GetGitBaseUrlFromConfig(self):
609 """Return the configured base URL from branch.<branchname>.baseurl.
610
611 Returns None if it is not set.
612 """
613 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
614 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000615
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000616 def GetRemoteUrl(self):
617 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
618
619 Returns None if there is no remote.
620 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000621 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000622 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
623
624 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000625 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000626 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000627 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000628 self.issue = int(issue) or None if issue else None
629 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000630 return self.issue
631
632 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000633 if not self.rietveld_server:
634 # If we're on a branch then get the server potentially associated
635 # with that branch.
636 if self.GetIssue():
637 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
638 ['config', self._RietveldServer()], error_ok=True).strip())
639 if not self.rietveld_server:
640 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000641 return self.rietveld_server
642
643 def GetIssueURL(self):
644 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000645 if not self.GetIssue():
646 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000647 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
648
649 def GetDescription(self, pretty=False):
650 if not self.has_description:
651 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000652 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000653 try:
654 self.description = self.RpcServer().get_description(issue).strip()
655 except urllib2.HTTPError, e:
656 if e.code == 404:
657 DieWithError(
658 ('\nWhile fetching the description for issue %d, received a '
659 '404 (not found)\n'
660 'error. It is likely that you deleted this '
661 'issue on the server. If this is the\n'
662 'case, please run\n\n'
663 ' git cl issue 0\n\n'
664 'to clear the association with the deleted issue. Then run '
665 'this command again.') % issue)
666 else:
667 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000668 '\nFailed to fetch issue description. HTTP error %d' % e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000669 self.has_description = True
670 if pretty:
671 wrapper = textwrap.TextWrapper()
672 wrapper.initial_indent = wrapper.subsequent_indent = ' '
673 return wrapper.fill(self.description)
674 return self.description
675
676 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000677 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000678 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000679 patchset = RunGit(['config', self._PatchsetSetting()],
680 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000681 self.patchset = int(patchset) or None if patchset else None
682 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000683 return self.patchset
684
685 def SetPatchset(self, patchset):
686 """Set this branch's patchset. If patchset=0, clears the patchset."""
687 if patchset:
688 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000689 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000690 else:
691 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000692 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000693 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000694
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000695 def GetMostRecentPatchset(self):
696 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000697
698 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000699 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000700 '/download/issue%s_%s.diff' % (issue, patchset))
701
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000702 def GetIssueProperties(self):
703 if self._props is None:
704 issue = self.GetIssue()
705 if not issue:
706 self._props = {}
707 else:
708 self._props = self.RpcServer().get_issue_properties(issue, True)
709 return self._props
710
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000711 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000712 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000713
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000714 def SetIssue(self, issue):
715 """Set this branch's issue. If issue=0, clears the issue."""
716 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000717 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000718 RunGit(['config', self._IssueSetting(), str(issue)])
719 if self.rietveld_server:
720 RunGit(['config', self._RietveldServer(), self.rietveld_server])
721 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000722 current_issue = self.GetIssue()
723 if current_issue:
724 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000725 self.issue = None
726 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000727
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000728 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000729 if not self.GitSanityChecks(upstream_branch):
730 DieWithError('\nGit sanity check failure')
731
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000732 env = os.environ.copy()
733 # 'cat' is a magical git string that disables pagers on all platforms.
734 env['GIT_PAGER'] = 'cat'
735
736 root = RunCommand(['git', 'rev-parse', '--show-cdup'], env=env).strip()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000737 if not root:
738 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000739 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000740
741 # We use the sha1 of HEAD as a name of this change.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000742 name = RunCommand(['git', 'rev-parse', 'HEAD'], env=env).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000743 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000744 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000745 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000746 except subprocess2.CalledProcessError:
747 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000748 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000749 'This branch probably doesn\'t exist anymore. To reset the\n'
750 'tracking branch, please run\n'
751 ' git branch --set-upstream %s trunk\n'
752 'replacing trunk with origin/master or the relevant branch') %
753 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000754
maruel@chromium.org52424302012-08-29 15:14:30 +0000755 issue = self.GetIssue()
756 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000757 if issue:
758 description = self.GetDescription()
759 else:
760 # If the change was never uploaded, use the log messages of all commits
761 # up to the branch point, as git cl upload will prefill the description
762 # with these log messages.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000763 description = RunCommand(['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000764 'log', '--pretty=format:%s%n%n%b',
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000765 '%s...' % (upstream_branch)],
766 env=env).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000767
768 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000769 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000770 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000771 name,
772 description,
773 absroot,
774 files,
775 issue,
776 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000777 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000778
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000779 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000780 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000781
782 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000783 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000784 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000785 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000786 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000787 except presubmit_support.PresubmitFailure, e:
788 DieWithError(
789 ('%s\nMaybe your depot_tools is out of date?\n'
790 'If all fails, contact maruel@') % e)
791
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000792 def UpdateDescription(self, description):
793 self.description = description
794 return self.RpcServer().update_description(
795 self.GetIssue(), self.description)
796
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000798 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000799 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000800
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000801 def SetFlag(self, flag, value):
802 """Patchset must match."""
803 if not self.GetPatchset():
804 DieWithError('The patchset needs to match. Send another patchset.')
805 try:
806 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000807 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000808 except urllib2.HTTPError, e:
809 if e.code == 404:
810 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
811 if e.code == 403:
812 DieWithError(
813 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
814 'match?') % (self.GetIssue(), self.GetPatchset()))
815 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000816
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000817 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000818 """Returns an upload.RpcServer() to access this review's rietveld instance.
819 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000820 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000821 self._rpc_server = rietveld.CachingRietveld(
822 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000823 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824
825 def _IssueSetting(self):
826 """Return the git setting that stores this change's issue."""
827 return 'branch.%s.rietveldissue' % self.GetBranch()
828
829 def _PatchsetSetting(self):
830 """Return the git setting that stores this change's most recent patchset."""
831 return 'branch.%s.rietveldpatchset' % self.GetBranch()
832
833 def _RietveldServer(self):
834 """Returns the git setting that stores this change's rietveld server."""
835 return 'branch.%s.rietveldserver' % self.GetBranch()
836
837
838def GetCodereviewSettingsInteractively():
839 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000840 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841 server = settings.GetDefaultServerUrl(error_ok=True)
842 prompt = 'Rietveld server (host[:port])'
843 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000844 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000845 if not server and not newserver:
846 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000847 if newserver:
848 newserver = gclient_utils.UpgradeToHttps(newserver)
849 if newserver != server:
850 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000851
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000852 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000853 prompt = caption
854 if initial:
855 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000856 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000857 if new_val == 'x':
858 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000859 elif new_val:
860 if is_url:
861 new_val = gclient_utils.UpgradeToHttps(new_val)
862 if new_val != initial:
863 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000864
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000865 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000866 SetProperty(settings.GetDefaultPrivateFlag(),
867 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000868 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000869 'tree-status-url', False)
870 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000871 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000872
873 # TODO: configure a default branch to diff against, rather than this
874 # svn-based hackery.
875
876
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000877class ChangeDescription(object):
878 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000879 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000880 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000881
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000882 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000883 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000884
agable@chromium.org42c20792013-09-12 17:34:49 +0000885 @property # www.logilab.org/ticket/89786
886 def description(self): # pylint: disable=E0202
887 return '\n'.join(self._description_lines)
888
889 def set_description(self, desc):
890 if isinstance(desc, basestring):
891 lines = desc.splitlines()
892 else:
893 lines = [line.rstrip() for line in desc]
894 while lines and not lines[0]:
895 lines.pop(0)
896 while lines and not lines[-1]:
897 lines.pop(-1)
898 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000899
900 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000901 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000902 assert isinstance(reviewers, list), reviewers
903 if not reviewers:
904 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000905 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000906
agable@chromium.org42c20792013-09-12 17:34:49 +0000907 # Get the set of R= and TBR= lines and remove them from the desciption.
908 regexp = re.compile(self.R_LINE)
909 matches = [regexp.match(line) for line in self._description_lines]
910 new_desc = [l for i, l in enumerate(self._description_lines)
911 if not matches[i]]
912 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000913
agable@chromium.org42c20792013-09-12 17:34:49 +0000914 # Construct new unified R= and TBR= lines.
915 r_names = []
916 tbr_names = []
917 for match in matches:
918 if not match:
919 continue
920 people = cleanup_list([match.group(2).strip()])
921 if match.group(1) == 'TBR':
922 tbr_names.extend(people)
923 else:
924 r_names.extend(people)
925 for name in r_names:
926 if name not in reviewers:
927 reviewers.append(name)
928 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
929 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
930
931 # Put the new lines in the description where the old first R= line was.
932 line_loc = next((i for i, match in enumerate(matches) if match), -1)
933 if 0 <= line_loc < len(self._description_lines):
934 if new_tbr_line:
935 self._description_lines.insert(line_loc, new_tbr_line)
936 if new_r_line:
937 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000938 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000939 if new_r_line:
940 self.append_footer(new_r_line)
941 if new_tbr_line:
942 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000943
944 def prompt(self):
945 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000946 self.set_description([
947 '# Enter a description of the change.',
948 '# This will be displayed on the codereview site.',
949 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000950 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000951 '--------------------',
952 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000953
agable@chromium.org42c20792013-09-12 17:34:49 +0000954 regexp = re.compile(self.BUG_LINE)
955 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +0000956 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +0000957 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000958 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000959 if not content:
960 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000961 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000962
963 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000964 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
965 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000966 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000967 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000968
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000969 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000970 if self._description_lines:
971 # Add an empty line if either the last line or the new line isn't a tag.
972 last_line = self._description_lines[-1]
973 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
974 not presubmit_support.Change.TAG_LINE_RE.match(line)):
975 self._description_lines.append('')
976 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000977
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000978 def get_reviewers(self):
979 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000980 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
981 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000982 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000983
984
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000985def get_approving_reviewers(props):
986 """Retrieves the reviewers that approved a CL from the issue properties with
987 messages.
988
989 Note that the list may contain reviewers that are not committer, thus are not
990 considered by the CQ.
991 """
992 return sorted(
993 set(
994 message['sender']
995 for message in props['messages']
996 if message['approval'] and message['sender'] in props['reviewers']
997 )
998 )
999
1000
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001001def FindCodereviewSettingsFile(filename='codereview.settings'):
1002 """Finds the given file starting in the cwd and going up.
1003
1004 Only looks up to the top of the repository unless an
1005 'inherit-review-settings-ok' file exists in the root of the repository.
1006 """
1007 inherit_ok_file = 'inherit-review-settings-ok'
1008 cwd = os.getcwd()
1009 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
1010 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1011 root = '/'
1012 while True:
1013 if filename in os.listdir(cwd):
1014 if os.path.isfile(os.path.join(cwd, filename)):
1015 return open(os.path.join(cwd, filename))
1016 if cwd == root:
1017 break
1018 cwd = os.path.dirname(cwd)
1019
1020
1021def LoadCodereviewSettingsFromFile(fileobj):
1022 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001023 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001024
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001025 def SetProperty(name, setting, unset_error_ok=False):
1026 fullname = 'rietveld.' + name
1027 if setting in keyvals:
1028 RunGit(['config', fullname, keyvals[setting]])
1029 else:
1030 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1031
1032 SetProperty('server', 'CODE_REVIEW_SERVER')
1033 # Only server setting is required. Other settings can be absent.
1034 # In that case, we ignore errors raised during option deletion attempt.
1035 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001036 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001037 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1038 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001039 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001040
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001041 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001042 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001043
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001044 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1045 #should be of the form
1046 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1047 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1048 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1049 keyvals['ORIGIN_URL_CONFIG']])
1050
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001051
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001052def urlretrieve(source, destination):
1053 """urllib is broken for SSL connections via a proxy therefore we
1054 can't use urllib.urlretrieve()."""
1055 with open(destination, 'w') as f:
1056 f.write(urllib2.urlopen(source).read())
1057
1058
ukai@chromium.org712d6102013-11-27 00:52:58 +00001059def hasSheBang(fname):
1060 """Checks fname is a #! script."""
1061 with open(fname) as f:
1062 return f.read(2).startswith('#!')
1063
1064
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001065def DownloadHooks(force):
1066 """downloads hooks
1067
1068 Args:
1069 force: True to update hooks. False to install hooks if not present.
1070 """
1071 if not settings.GetIsGerrit():
1072 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001073 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001074 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1075 if not os.access(dst, os.X_OK):
1076 if os.path.exists(dst):
1077 if not force:
1078 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001079 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001080 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001081 if not hasSheBang(dst):
1082 DieWithError('Not a script: %s\n'
1083 'You need to download from\n%s\n'
1084 'into .git/hooks/commit-msg and '
1085 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001086 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1087 except Exception:
1088 if os.path.exists(dst):
1089 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001090 DieWithError('\nFailed to download hooks.\n'
1091 'You need to download from\n%s\n'
1092 'into .git/hooks/commit-msg and '
1093 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001094
1095
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001096@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001097def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001098 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001100 parser.add_option('--activate-update', action='store_true',
1101 help='activate auto-updating [rietveld] section in '
1102 '.git/config')
1103 parser.add_option('--deactivate-update', action='store_true',
1104 help='deactivate auto-updating [rietveld] section in '
1105 '.git/config')
1106 options, args = parser.parse_args(args)
1107
1108 if options.deactivate_update:
1109 RunGit(['config', 'rietveld.autoupdate', 'false'])
1110 return
1111
1112 if options.activate_update:
1113 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1114 return
1115
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001116 if len(args) == 0:
1117 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001118 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001119 return 0
1120
1121 url = args[0]
1122 if not url.endswith('codereview.settings'):
1123 url = os.path.join(url, 'codereview.settings')
1124
1125 # Load code review settings and download hooks (if available).
1126 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001127 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128 return 0
1129
1130
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001131def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001132 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001133 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1134 branch = ShortBranchName(branchref)
1135 _, args = parser.parse_args(args)
1136 if not args:
1137 print("Current base-url:")
1138 return RunGit(['config', 'branch.%s.base-url' % branch],
1139 error_ok=False).strip()
1140 else:
1141 print("Setting base-url to %s" % args[0])
1142 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1143 error_ok=False).strip()
1144
1145
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001146def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001147 """Show status of changelists.
1148
1149 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001150 - Red not sent for review or broken
1151 - Blue waiting for review
1152 - Yellow waiting for you to reply to review
1153 - Green LGTM'ed
1154 - Magenta in the commit queue
1155 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001156
1157 Also see 'git cl comments'.
1158 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001159 parser.add_option('--field',
1160 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001161 parser.add_option('-f', '--fast', action='store_true',
1162 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001163 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001164 if args:
1165 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001166
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001167 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001168 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169 if options.field.startswith('desc'):
1170 print cl.GetDescription()
1171 elif options.field == 'id':
1172 issueid = cl.GetIssue()
1173 if issueid:
1174 print issueid
1175 elif options.field == 'patch':
1176 patchset = cl.GetPatchset()
1177 if patchset:
1178 print patchset
1179 elif options.field == 'url':
1180 url = cl.GetIssueURL()
1181 if url:
1182 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001183 return 0
1184
1185 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1186 if not branches:
1187 print('No local branch found.')
1188 return 0
1189
1190 changes = (Changelist(branchref=b) for b in branches.splitlines())
1191 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1192 alignment = max(5, max(len(b) for b in branches))
1193 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001194 # Adhoc thread pool to request data concurrently.
1195 output = Queue.Queue()
1196
1197 # Silence upload.py otherwise it becomes unweldly.
1198 upload.verbosity = 0
1199
1200 if not options.fast:
1201 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001202 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001203 c = Changelist(branchref=b)
1204 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001205 props = {}
1206 r = None
1207 if i:
1208 try:
1209 props = c.GetIssueProperties()
1210 r = c.GetApprovingReviewers() if i else None
1211 except urllib2.HTTPError:
1212 # The issue probably doesn't exist anymore.
1213 i += ' (broken)'
1214
1215 msgs = props.get('messages') or []
1216
1217 if not i:
1218 color = Fore.WHITE
1219 elif props.get('closed'):
1220 # Issue is closed.
1221 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001222 elif props.get('commit'):
1223 # Issue is in the commit queue.
1224 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001225 elif r:
1226 # Was LGTM'ed.
1227 color = Fore.GREEN
1228 elif not msgs:
1229 # No message was sent.
1230 color = Fore.RED
1231 elif msgs[-1]['sender'] != props.get('owner_email'):
1232 color = Fore.YELLOW
1233 else:
1234 color = Fore.BLUE
1235 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001236
1237 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1238 for t in threads:
1239 t.daemon = True
1240 t.start()
1241 else:
1242 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1243 for b in branches:
1244 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001245 url = c.GetIssueURL()
1246 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001247
1248 tmp = {}
1249 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001250 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001251 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001252 b, i, color = output.get()
1253 tmp[b] = (i, color)
1254 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001255 reset = Fore.RESET
1256 if not sys.stdout.isatty():
1257 color = ''
1258 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001259 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001260 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001261
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001262 cl = Changelist()
1263 print
1264 print 'Current branch:',
1265 if not cl.GetIssue():
1266 print 'no issue assigned.'
1267 return 0
1268 print cl.GetBranch()
1269 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1270 print 'Issue description:'
1271 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001272 return 0
1273
1274
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001275def colorize_CMDstatus_doc():
1276 """To be called once in main() to add colors to git cl status help."""
1277 colors = [i for i in dir(Fore) if i[0].isupper()]
1278
1279 def colorize_line(line):
1280 for color in colors:
1281 if color in line.upper():
1282 # Extract whitespaces first and the leading '-'.
1283 indent = len(line) - len(line.lstrip(' ')) + 1
1284 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1285 return line
1286
1287 lines = CMDstatus.__doc__.splitlines()
1288 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1289
1290
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001291@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001292def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001293 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001294
1295 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001296 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001297 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298
1299 cl = Changelist()
1300 if len(args) > 0:
1301 try:
1302 issue = int(args[0])
1303 except ValueError:
1304 DieWithError('Pass a number to set the issue or none to list it.\n'
1305 'Maybe you want to run git cl status?')
1306 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001307 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001308 return 0
1309
1310
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001311def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001312 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001313 (_, args) = parser.parse_args(args)
1314 if args:
1315 parser.error('Unsupported argument: %s' % args)
1316
1317 cl = Changelist()
1318 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001319 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001320 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001321 if message['disapproval']:
1322 color = Fore.RED
1323 elif message['approval']:
1324 color = Fore.GREEN
1325 elif message['sender'] == data['owner_email']:
1326 color = Fore.MAGENTA
1327 else:
1328 color = Fore.BLUE
1329 print '\n%s%s %s%s' % (
1330 color, message['date'].split('.', 1)[0], message['sender'],
1331 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001332 if message['text'].strip():
1333 print '\n'.join(' ' + l for l in message['text'].splitlines())
1334 return 0
1335
1336
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001337def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001338 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001339 cl = Changelist()
1340 if not cl.GetIssue():
1341 DieWithError('This branch has no associated changelist.')
1342 description = ChangeDescription(cl.GetDescription())
1343 description.prompt()
1344 cl.UpdateDescription(description.description)
1345 return 0
1346
1347
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001348def CreateDescriptionFromLog(args):
1349 """Pulls out the commit log to use as a base for the CL description."""
1350 log_args = []
1351 if len(args) == 1 and not args[0].endswith('.'):
1352 log_args = [args[0] + '..']
1353 elif len(args) == 1 and args[0].endswith('...'):
1354 log_args = [args[0][:-1]]
1355 elif len(args) == 2:
1356 log_args = [args[0] + '..' + args[1]]
1357 else:
1358 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001359 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360
1361
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001363 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001364 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001365 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001366 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001367 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001368 (options, args) = parser.parse_args(args)
1369
ukai@chromium.org259e4682012-10-25 07:36:33 +00001370 if not options.force and is_dirty_git_tree('presubmit'):
1371 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001372 return 1
1373
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001374 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001375 if args:
1376 base_branch = args[0]
1377 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001378 # Default to diffing against the common ancestor of the upstream branch.
1379 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001380
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001381 cl.RunHook(
1382 committing=not options.upload,
1383 may_prompt=False,
1384 verbose=options.verbose,
1385 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001386 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001387
1388
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001389def AddChangeIdToCommitMessage(options, args):
1390 """Re-commits using the current message, assumes the commit hook is in
1391 place.
1392 """
1393 log_desc = options.message or CreateDescriptionFromLog(args)
1394 git_command = ['commit', '--amend', '-m', log_desc]
1395 RunGit(git_command)
1396 new_log_desc = CreateDescriptionFromLog(args)
1397 if CHANGE_ID in new_log_desc:
1398 print 'git-cl: Added Change-Id to commit message.'
1399 else:
1400 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1401
1402
ukai@chromium.orge8077812012-02-03 03:41:46 +00001403def GerritUpload(options, args, cl):
1404 """upload the current branch to gerrit."""
1405 # We assume the remote called "origin" is the one we want.
1406 # It is probably not worthwhile to support different workflows.
1407 remote = 'origin'
1408 branch = 'master'
1409 if options.target_branch:
1410 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001412 change_desc = ChangeDescription(
1413 options.message or CreateDescriptionFromLog(args))
1414 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001415 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001417 if CHANGE_ID not in change_desc.description:
1418 AddChangeIdToCommitMessage(options, args)
1419 if options.reviewers:
1420 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001421
ukai@chromium.orge8077812012-02-03 03:41:46 +00001422 receive_options = []
1423 cc = cl.GetCCList().split(',')
1424 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001425 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001426 cc = filter(None, cc)
1427 if cc:
1428 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001429 if change_desc.get_reviewers():
1430 receive_options.extend(
1431 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001432
ukai@chromium.orge8077812012-02-03 03:41:46 +00001433 git_command = ['push']
1434 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001435 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001436 ' '.join(receive_options))
1437 git_command += [remote, 'HEAD:refs/for/' + branch]
1438 RunGit(git_command)
1439 # TODO(ukai): parse Change-Id: and set issue number?
1440 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001441
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001442
ukai@chromium.orge8077812012-02-03 03:41:46 +00001443def RietveldUpload(options, args, cl):
1444 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001445 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1446 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001447 if options.emulate_svn_auto_props:
1448 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001449
1450 change_desc = None
1451
pgervais@chromium.org91141372014-01-09 23:27:20 +00001452 if options.email is not None:
1453 upload_args.extend(['--email', options.email])
1454
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001455 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001456 if options.title:
1457 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001458 if options.message:
1459 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001460 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001461 print ("This branch is associated with issue %s. "
1462 "Adding patch to that issue." % cl.GetIssue())
1463 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001464 if options.title:
1465 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001466 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001467 change_desc = ChangeDescription(message)
1468 if options.reviewers:
1469 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001470 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001471 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001472
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001473 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001474 print "Description is empty; aborting."
1475 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001476
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001477 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001478 if change_desc.get_reviewers():
1479 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001480 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001481 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001482 DieWithError("Must specify reviewers to send email.")
1483 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001484
1485 # We check this before applying rietveld.private assuming that in
1486 # rietveld.cc only addresses which we can send private CLs to are listed
1487 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1488 # --private is specified explicitly on the command line.
1489 if options.private:
1490 logging.warn('rietveld.cc is ignored since private flag is specified. '
1491 'You need to review and add them manually if necessary.')
1492 cc = cl.GetCCListWithoutDefault()
1493 else:
1494 cc = cl.GetCCList()
1495 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001496 if cc:
1497 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001498
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001499 if options.private or settings.GetDefaultPrivateFlag() == "True":
1500 upload_args.append('--private')
1501
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001502 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001503 if not options.find_copies:
1504 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001505
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001506 # Include the upstream repo's URL in the change -- this is useful for
1507 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001508 remote_url = cl.GetGitBaseUrlFromConfig()
1509 if not remote_url:
1510 if settings.GetIsGitSvn():
1511 # URL is dependent on the current directory.
1512 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1513 if data:
1514 keys = dict(line.split(': ', 1) for line in data.splitlines()
1515 if ': ' in line)
1516 remote_url = keys.get('URL', None)
1517 else:
1518 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1519 remote_url = (cl.GetRemoteUrl() + '@'
1520 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001521 if remote_url:
1522 upload_args.extend(['--base_url', remote_url])
1523
1524 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001525 upload_args = ['upload'] + upload_args + args
1526 logging.info('upload.RealMain(%s)', upload_args)
1527 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001528 issue = int(issue)
1529 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001530 except KeyboardInterrupt:
1531 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001532 except:
1533 # If we got an exception after the user typed a description for their
1534 # change, back up the description before re-raising.
1535 if change_desc:
1536 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1537 print '\nGot exception while uploading -- saving description to %s\n' \
1538 % backup_path
1539 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001540 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001541 backup_file.close()
1542 raise
1543
1544 if not cl.GetIssue():
1545 cl.SetIssue(issue)
1546 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001547
1548 if options.use_commit_queue:
1549 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001550 return 0
1551
1552
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001553def cleanup_list(l):
1554 """Fixes a list so that comma separated items are put as individual items.
1555
1556 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1557 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1558 """
1559 items = sum((i.split(',') for i in l), [])
1560 stripped_items = (i.strip() for i in items)
1561 return sorted(filter(None, stripped_items))
1562
1563
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001564@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001565def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001566 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001567 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1568 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001569 parser.add_option('--bypass-watchlists', action='store_true',
1570 dest='bypass_watchlists',
1571 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001572 parser.add_option('-f', action='store_true', dest='force',
1573 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001574 parser.add_option('-m', dest='message', help='message for patchset')
1575 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001576 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001577 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001578 help='reviewer email addresses')
1579 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001580 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001581 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001582 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001583 help='send email to reviewer immediately')
1584 parser.add_option("--emulate_svn_auto_props", action="store_true",
1585 dest="emulate_svn_auto_props",
1586 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001587 parser.add_option('-c', '--use-commit-queue', action='store_true',
1588 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001589 parser.add_option('--private', action='store_true',
1590 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001591 parser.add_option('--target_branch',
1592 help='When uploading to gerrit, remote branch to '
1593 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001594 parser.add_option('--email', default=None,
1595 help='email address to use to connect to Rietveld')
1596
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001597 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001598 (options, args) = parser.parse_args(args)
1599
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001600 if options.target_branch and not settings.GetIsGerrit():
1601 parser.error('Use --target_branch for non gerrit repository.')
1602
ukai@chromium.org259e4682012-10-25 07:36:33 +00001603 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001604 return 1
1605
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001606 options.reviewers = cleanup_list(options.reviewers)
1607 options.cc = cleanup_list(options.cc)
1608
ukai@chromium.orge8077812012-02-03 03:41:46 +00001609 cl = Changelist()
1610 if args:
1611 # TODO(ukai): is it ok for gerrit case?
1612 base_branch = args[0]
1613 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001614 # Default to diffing against common ancestor of upstream branch
1615 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001616 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001617
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001618 # Apply watchlists on upload.
1619 change = cl.GetChange(base_branch, None)
1620 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1621 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001622 if not options.bypass_watchlists:
1623 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001624
ukai@chromium.orge8077812012-02-03 03:41:46 +00001625 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001626 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001627 may_prompt=not options.force,
1628 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001629 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001630 if not hook_results.should_continue():
1631 return 1
1632 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001633 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001634
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001635 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001636 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001637 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001638 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001639 print ('The last upload made from this repository was patchset #%d but '
1640 'the most recent patchset on the server is #%d.'
1641 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001642 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1643 'from another machine or branch the patch you\'re uploading now '
1644 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001645 ask_for_data('About to upload; enter to confirm.')
1646
iannucci@chromium.org79540052012-10-19 23:15:26 +00001647 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001648 if settings.GetIsGerrit():
1649 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001650 ret = RietveldUpload(options, args, cl)
1651 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001652 git_set_branch_value('last-upload-hash',
1653 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001654
1655 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001656
1657
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001658def IsSubmoduleMergeCommit(ref):
1659 # When submodules are added to the repo, we expect there to be a single
1660 # non-git-svn merge commit at remote HEAD with a signature comment.
1661 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001662 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001663 return RunGit(cmd) != ''
1664
1665
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001666def SendUpstream(parser, args, cmd):
1667 """Common code for CmdPush and CmdDCommit
1668
1669 Squashed commit into a single.
1670 Updates changelog with metadata (e.g. pointer to review).
1671 Pushes/dcommits the code upstream.
1672 Updates review and closes.
1673 """
1674 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1675 help='bypass upload presubmit hook')
1676 parser.add_option('-m', dest='message',
1677 help="override review description")
1678 parser.add_option('-f', action='store_true', dest='force',
1679 help="force yes to questions (don't prompt)")
1680 parser.add_option('-c', dest='contributor',
1681 help="external contributor for patch (appended to " +
1682 "description and used as author for git). Should be " +
1683 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001684 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001685 (options, args) = parser.parse_args(args)
1686 cl = Changelist()
1687
1688 if not args or cmd == 'push':
1689 # Default to merging against our best guess of the upstream branch.
1690 args = [cl.GetUpstreamBranch()]
1691
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001692 if options.contributor:
1693 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1694 print "Please provide contibutor as 'First Last <email@example.com>'"
1695 return 1
1696
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001697 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001698 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001699
ukai@chromium.org259e4682012-10-25 07:36:33 +00001700 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001701 return 1
1702
1703 # This rev-list syntax means "show all commits not in my branch that
1704 # are in base_branch".
1705 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1706 base_branch]).splitlines()
1707 if upstream_commits:
1708 print ('Base branch "%s" has %d commits '
1709 'not in this branch.' % (base_branch, len(upstream_commits)))
1710 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1711 return 1
1712
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001713 # This is the revision `svn dcommit` will commit on top of.
1714 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1715 '--pretty=format:%H'])
1716
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001717 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001718 # If the base_head is a submodule merge commit, the first parent of the
1719 # base_head should be a git-svn commit, which is what we're interested in.
1720 base_svn_head = base_branch
1721 if base_has_submodules:
1722 base_svn_head += '^1'
1723
1724 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001725 if extra_commits:
1726 print ('This branch has %d additional commits not upstreamed yet.'
1727 % len(extra_commits.splitlines()))
1728 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1729 'before attempting to %s.' % (base_branch, cmd))
1730 return 1
1731
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001732 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001733 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001734 author = None
1735 if options.contributor:
1736 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001737 hook_results = cl.RunHook(
1738 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001739 may_prompt=not options.force,
1740 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001741 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001742 if not hook_results.should_continue():
1743 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001744
1745 if cmd == 'dcommit':
1746 # Check the tree status if the tree status URL is set.
1747 status = GetTreeStatus()
1748 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001749 print('The tree is closed. Please wait for it to reopen. Use '
1750 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001751 return 1
1752 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001753 print('Unable to determine tree status. Please verify manually and '
1754 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001755 else:
1756 breakpad.SendStack(
1757 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001758 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1759 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001760 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001761
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001762 change_desc = ChangeDescription(options.message)
1763 if not change_desc.description and cl.GetIssue():
1764 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001765
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001766 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001767 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001768 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001769 else:
1770 print 'No description set.'
1771 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1772 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001773
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001774 # Keep a separate copy for the commit message, because the commit message
1775 # contains the link to the Rietveld issue, while the Rietveld message contains
1776 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001777 # Keep a separate copy for the commit message.
1778 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001779 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001780
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001781 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001782 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001783 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001784 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001785 commit_desc.append_footer('Patch from %s.' % options.contributor)
1786
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001787 print('Description:')
1788 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001789
1790 branches = [base_branch, cl.GetBranchRef()]
1791 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001792 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001793 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001794
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001795 # We want to squash all this branch's commits into one commit with the proper
1796 # description. We do this by doing a "reset --soft" to the base branch (which
1797 # keeps the working copy the same), then dcommitting that. If origin/master
1798 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1799 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001800 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001801 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1802 # Delete the branches if they exist.
1803 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1804 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1805 result = RunGitWithCode(showref_cmd)
1806 if result[0] == 0:
1807 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001808
1809 # We might be in a directory that's present in this branch but not in the
1810 # trunk. Move up to the top of the tree so that git commands that expect a
1811 # valid CWD won't fail after we check out the merge branch.
1812 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1813 if rel_base_path:
1814 os.chdir(rel_base_path)
1815
1816 # Stuff our change into the merge branch.
1817 # We wrap in a try...finally block so if anything goes wrong,
1818 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001819 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001820 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001821 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1822 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001823 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001824 RunGit(
1825 [
1826 'commit', '--author', options.contributor,
1827 '-m', commit_desc.description,
1828 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001829 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001830 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001831 if base_has_submodules:
1832 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1833 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1834 RunGit(['checkout', CHERRY_PICK_BRANCH])
1835 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001836 if cmd == 'push':
1837 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001838 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001839 retcode, output = RunGitWithCode(
1840 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1841 logging.debug(output)
1842 else:
1843 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001844 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001845 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001846 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001847 finally:
1848 # And then swap back to the original branch and clean up.
1849 RunGit(['checkout', '-q', cl.GetBranch()])
1850 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001851 if base_has_submodules:
1852 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001853
1854 if cl.GetIssue():
1855 if cmd == 'dcommit' and 'Committed r' in output:
1856 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1857 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001858 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1859 for l in output.splitlines(False))
1860 match = filter(None, match)
1861 if len(match) != 1:
1862 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1863 output)
1864 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001865 else:
1866 return 1
1867 viewvc_url = settings.GetViewVCUrl()
1868 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001869 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001870 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001871 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001872 print ('Closing issue '
1873 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001874 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001875 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001876 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001877 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001878 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001879 if options.bypass_hooks:
1880 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
1881 else:
1882 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001883 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001884 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001885
1886 if retcode == 0:
1887 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1888 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001889 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001890
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001891 return 0
1892
1893
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001894@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001895def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001896 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001897 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001898 message = """This doesn't appear to be an SVN repository.
1899If your project has a git mirror with an upstream SVN master, you probably need
1900to run 'git svn init', see your project's git mirror documentation.
1901If your project has a true writeable upstream repository, you probably want
1902to run 'git cl push' instead.
1903Choose wisely, if you get this wrong, your commit might appear to succeed but
1904will instead be silently ignored."""
1905 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001906 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001907 return SendUpstream(parser, args, 'dcommit')
1908
1909
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001910@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001911def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001912 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001913 if settings.GetIsGitSvn():
1914 print('This appears to be an SVN repository.')
1915 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001916 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001917 return SendUpstream(parser, args, 'push')
1918
1919
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001920@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001921def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00001922 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001923 parser.add_option('-b', dest='newbranch',
1924 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001925 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001926 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001927 parser.add_option('-d', '--directory', action='store', metavar='DIR',
1928 help='Change to the directory DIR immediately, '
1929 'before doing anything else.')
1930 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001931 help='failed patches spew .rej files rather than '
1932 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001933 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1934 help="don't commit after patch applies")
1935 (options, args) = parser.parse_args(args)
1936 if len(args) != 1:
1937 parser.print_help()
1938 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001939 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001940
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001941 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001942 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001943
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001944 if options.newbranch:
1945 if options.force:
1946 RunGit(['branch', '-D', options.newbranch],
1947 stderr=subprocess2.PIPE, error_ok=True)
1948 RunGit(['checkout', '-b', options.newbranch,
1949 Changelist().GetUpstreamBranch()])
1950
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001951 return PatchIssue(issue_arg, options.reject, options.nocommit,
1952 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001953
1954
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001955def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001956 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001957 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001958 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001959 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001960 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001961 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001962 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001963 # Assume it's a URL to the patch. Default to https.
1964 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001965 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001966 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001967 DieWithError('Must pass an issue ID or full URL for '
1968 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001969 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001970 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001971 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001972
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001973 # Switch up to the top-level directory, if necessary, in preparation for
1974 # applying the patch.
1975 top = RunGit(['rev-parse', '--show-cdup']).strip()
1976 if top:
1977 os.chdir(top)
1978
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001979 # Git patches have a/ at the beginning of source paths. We strip that out
1980 # with a sed script rather than the -p flag to patch so we can feed either
1981 # Git or svn-style patches into the same apply command.
1982 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001983 try:
1984 patch_data = subprocess2.check_output(
1985 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1986 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001987 DieWithError('Git patch mungling failed.')
1988 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001989 env = os.environ.copy()
1990 # 'cat' is a magical git string that disables pagers on all platforms.
1991 env['GIT_PAGER'] = 'cat'
1992
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001993 # We use "git apply" to apply the patch instead of "patch" so that we can
1994 # pick up file adds.
1995 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001996 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001997 if directory:
1998 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001999 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002000 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002001 elif IsGitVersionAtLeast('1.7.12'):
2002 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002003 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002004 subprocess2.check_call(cmd, env=env,
2005 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002006 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002007 DieWithError('Failed to apply the patch')
2008
2009 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002010 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002011 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2012 cl = Changelist()
2013 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002014 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002015 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002016 else:
2017 print "Patch applied to index."
2018 return 0
2019
2020
2021def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002022 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002023 # Provide a wrapper for git svn rebase to help avoid accidental
2024 # git svn dcommit.
2025 # It's the only command that doesn't use parser at all since we just defer
2026 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002027 env = os.environ.copy()
2028 # 'cat' is a magical git string that disables pagers on all platforms.
2029 env['GIT_PAGER'] = 'cat'
2030
2031 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002032
2033
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002034def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002035 """Fetches the tree status and returns either 'open', 'closed',
2036 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002037 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002038 if url:
2039 status = urllib2.urlopen(url).read().lower()
2040 if status.find('closed') != -1 or status == '0':
2041 return 'closed'
2042 elif status.find('open') != -1 or status == '1':
2043 return 'open'
2044 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002045 return 'unset'
2046
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002047
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002048def GetTreeStatusReason():
2049 """Fetches the tree status from a json url and returns the message
2050 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002051 url = settings.GetTreeStatusUrl()
2052 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002053 connection = urllib2.urlopen(json_url)
2054 status = json.loads(connection.read())
2055 connection.close()
2056 return status['message']
2057
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002058
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002059def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002060 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002061 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002062 status = GetTreeStatus()
2063 if 'unset' == status:
2064 print 'You must configure your tree status URL by running "git cl config".'
2065 return 2
2066
2067 print "The tree is %s" % status
2068 print
2069 print GetTreeStatusReason()
2070 if status != 'open':
2071 return 1
2072 return 0
2073
2074
maruel@chromium.org15192402012-09-06 12:38:29 +00002075def CMDtry(parser, args):
2076 """Triggers a try job through Rietveld."""
2077 group = optparse.OptionGroup(parser, "Try job options")
2078 group.add_option(
2079 "-b", "--bot", action="append",
2080 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2081 "times to specify multiple builders. ex: "
2082 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2083 "the try server waterfall for the builders name and the tests "
2084 "available. Can also be used to specify gtest_filter, e.g. "
2085 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2086 group.add_option(
2087 "-r", "--revision",
2088 help="Revision to use for the try job; default: the "
2089 "revision will be determined by the try server; see "
2090 "its waterfall for more info")
2091 group.add_option(
2092 "-c", "--clobber", action="store_true", default=False,
2093 help="Force a clobber before building; e.g. don't do an "
2094 "incremental build")
2095 group.add_option(
2096 "--project",
2097 help="Override which project to use. Projects are defined "
2098 "server-side to define what default bot set to use")
2099 group.add_option(
2100 "-t", "--testfilter", action="append", default=[],
2101 help=("Apply a testfilter to all the selected builders. Unless the "
2102 "builders configurations are similar, use multiple "
2103 "--bot <builder>:<test> arguments."))
2104 group.add_option(
2105 "-n", "--name", help="Try job name; default to current branch name")
2106 parser.add_option_group(group)
2107 options, args = parser.parse_args(args)
2108
2109 if args:
2110 parser.error('Unknown arguments: %s' % args)
2111
2112 cl = Changelist()
2113 if not cl.GetIssue():
2114 parser.error('Need to upload first')
2115
2116 if not options.name:
2117 options.name = cl.GetBranch()
2118
2119 # Process --bot and --testfilter.
2120 if not options.bot:
2121 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002122 change = cl.GetChange(
2123 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
2124 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002125 options.bot = presubmit_support.DoGetTrySlaves(
2126 change,
2127 change.LocalPaths(),
2128 settings.GetRoot(),
2129 None,
2130 None,
2131 options.verbose,
2132 sys.stdout)
2133 if not options.bot:
2134 parser.error('No default try builder to try, use --bot')
2135
2136 builders_and_tests = {}
stip@chromium.org43064fd2013-12-18 20:07:44 +00002137 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2138 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2139
2140 for bot in old_style:
maruel@chromium.org15192402012-09-06 12:38:29 +00002141 if ':' in bot:
2142 builder, tests = bot.split(':', 1)
2143 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2144 elif ',' in bot:
2145 parser.error('Specify one bot per --bot flag')
2146 else:
2147 builders_and_tests.setdefault(bot, []).append('defaulttests')
2148
stip@chromium.org43064fd2013-12-18 20:07:44 +00002149 for bot, tests in new_style:
2150 builders_and_tests.setdefault(bot, []).extend(tests)
2151
maruel@chromium.org15192402012-09-06 12:38:29 +00002152 if options.testfilter:
2153 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2154 builders_and_tests = dict(
2155 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2156 if t != ['compile'])
2157
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002158 if any('triggered' in b for b in builders_and_tests):
2159 print >> sys.stderr, (
2160 'ERROR You are trying to send a job to a triggered bot. This type of'
2161 ' bot requires an\ninitial job from a parent (usually a builder). '
2162 'Instead send your job to the parent.\n'
2163 'Bot list: %s' % builders_and_tests)
2164 return 1
2165
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002166 patchset = cl.GetMostRecentPatchset()
2167 if patchset and patchset != cl.GetPatchset():
2168 print(
2169 '\nWARNING Mismatch between local config and server. Did a previous '
2170 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2171 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002172 try:
2173 cl.RpcServer().trigger_try_jobs(
2174 cl.GetIssue(), patchset, options.name, options.clobber,
2175 options.revision, builders_and_tests)
2176 except urllib2.HTTPError, e:
2177 if e.code == 404:
2178 print('404 from rietveld; '
2179 'did you mean to use "git try" instead of "git cl try"?')
2180 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002181 print('Tried jobs on:')
2182 length = max(len(builder) for builder in builders_and_tests)
2183 for builder in sorted(builders_and_tests):
2184 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002185 return 0
2186
2187
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002188@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002189def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002190 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002191 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002192 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002193 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002194
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002195 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002196 if args:
2197 # One arg means set upstream branch.
2198 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2199 cl = Changelist()
2200 print "Upstream branch set to " + cl.GetUpstreamBranch()
2201 else:
2202 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002203 return 0
2204
2205
thestig@chromium.org00858c82013-12-02 23:08:03 +00002206def CMDweb(parser, args):
2207 """Opens the current CL in the web browser."""
2208 _, args = parser.parse_args(args)
2209 if args:
2210 parser.error('Unrecognized args: %s' % ' '.join(args))
2211
2212 issue_url = Changelist().GetIssueURL()
2213 if not issue_url:
2214 print >> sys.stderr, 'ERROR No issue to open'
2215 return 1
2216
2217 webbrowser.open(issue_url)
2218 return 0
2219
2220
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002221def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002222 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002223 _, args = parser.parse_args(args)
2224 if args:
2225 parser.error('Unrecognized args: %s' % ' '.join(args))
2226 cl = Changelist()
2227 cl.SetFlag('commit', '1')
2228 return 0
2229
2230
groby@chromium.org411034a2013-02-26 15:12:01 +00002231def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002232 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002233 _, args = parser.parse_args(args)
2234 if args:
2235 parser.error('Unrecognized args: %s' % ' '.join(args))
2236 cl = Changelist()
2237 # Ensure there actually is an issue to close.
2238 cl.GetDescription()
2239 cl.CloseIssue()
2240 return 0
2241
2242
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002243def CMDdiff(parser, args):
2244 """shows differences between local tree and last upload."""
2245 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002246 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002247 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002248 if not issue:
2249 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002250 TMP_BRANCH = 'git-cl-diff'
2251 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2252
2253 # Create a new branch based on the merge-base
2254 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2255 try:
2256 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002257 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002258 if rtn != 0:
2259 return rtn
2260
2261 # Switch back to starting brand and diff against the temporary
2262 # branch containing the latest rietveld patch.
2263 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2264 finally:
2265 RunGit(['checkout', '-q', branch])
2266 RunGit(['branch', '-D', TMP_BRANCH])
2267
2268 return 0
2269
2270
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002271def CMDowners(parser, args):
2272 """interactively find the owners for reviewing"""
2273 parser.add_option(
2274 '--no-color',
2275 action='store_true',
2276 help='Use this option to disable color output')
2277 options, args = parser.parse_args(args)
2278
2279 author = RunGit(['config', 'user.email']).strip() or None
2280
2281 cl = Changelist()
2282
2283 if args:
2284 if len(args) > 1:
2285 parser.error('Unknown args')
2286 base_branch = args[0]
2287 else:
2288 # Default to diffing against the common ancestor of the upstream branch.
2289 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2290
2291 change = cl.GetChange(base_branch, None)
2292 return owners_finder.OwnersFinder(
2293 [f.LocalPath() for f in
2294 cl.GetChange(base_branch, None).AffectedFiles()],
2295 change.RepositoryRoot(), author,
2296 fopen=file, os_path=os.path, glob=glob.glob,
2297 disable_color=options.no_color).run()
2298
2299
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002300def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002301 """Runs clang-format on the diff."""
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002302 CLANG_EXTS = ['.cc', '.cpp', '.h']
2303 parser.add_option('--full', action='store_true', default=False)
2304 opts, args = parser.parse_args(args)
2305 if args:
2306 parser.error('Unrecognized args: %s' % ' '.join(args))
2307
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002308 # git diff generates paths against the root of the repository. Change
2309 # to that directory so clang-format can find files even within subdirs.
2310 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
2311 if rel_base_path:
2312 os.chdir(rel_base_path)
2313
digit@chromium.org29e47272013-05-17 17:01:46 +00002314 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002315 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002316 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002317 # Only list the names of modified files.
2318 diff_cmd.append('--name-only')
2319 else:
2320 # Only generate context-less patches.
2321 diff_cmd.append('-U0')
2322
2323 # Grab the merge-base commit, i.e. the upstream commit of the current
2324 # branch when it was created or the last time it was rebased. This is
2325 # to cover the case where the user may have called "git fetch origin",
2326 # moving the origin branch to a newer commit, but hasn't rebased yet.
2327 upstream_commit = None
2328 cl = Changelist()
2329 upstream_branch = cl.GetUpstreamBranch()
2330 if upstream_branch:
2331 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2332 upstream_commit = upstream_commit.strip()
2333
2334 if not upstream_commit:
2335 DieWithError('Could not find base commit for this branch. '
2336 'Are you in detached state?')
2337
2338 diff_cmd.append(upstream_commit)
2339
2340 # Handle source file filtering.
2341 diff_cmd.append('--')
2342 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2343 diff_output = RunGit(diff_cmd)
2344
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002345 top_dir = RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n')
2346
digit@chromium.org29e47272013-05-17 17:01:46 +00002347 if opts.full:
2348 # diff_output is a list of files to send to clang-format.
2349 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002350 if not files:
2351 print "Nothing to format."
2352 return 0
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002353 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files,
2354 cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002355 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002356 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002357 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2358 'clang-format-diff.py')
2359 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002360 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
adam.treat@samsung.com16d2ad62013-09-27 14:54:04 +00002361 cmd = [sys.executable, cfd_path, '-p0', '-style', 'Chromium']
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002362
2363 # Newer versions of clang-format-diff.py require an explicit -i flag
2364 # to apply the edits to files, otherwise it just displays a diff.
2365 # Probe the usage string to verify if this is needed.
2366 help_text = RunCommand([sys.executable, cfd_path, '-h'])
2367 if '[-i]' in help_text:
2368 cmd.append('-i')
2369
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002370 RunCommand(cmd, stdin=diff_output, cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002371
2372 return 0
2373
2374
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002375class OptionParser(optparse.OptionParser):
2376 """Creates the option parse and add --verbose support."""
2377 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002378 optparse.OptionParser.__init__(
2379 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002380 self.add_option(
2381 '-v', '--verbose', action='count', default=0,
2382 help='Use 2 times for more debugging info')
2383
2384 def parse_args(self, args=None, values=None):
2385 options, args = optparse.OptionParser.parse_args(self, args, values)
2386 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2387 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2388 return options, args
2389
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002390
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002391def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002392 if sys.hexversion < 0x02060000:
2393 print >> sys.stderr, (
2394 '\nYour python version %s is unsupported, please upgrade.\n' %
2395 sys.version.split(' ', 1)[0])
2396 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002397
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002398 # Reload settings.
2399 global settings
2400 settings = Settings()
2401
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002402 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002403 dispatcher = subcommand.CommandDispatcher(__name__)
2404 try:
2405 return dispatcher.execute(OptionParser(), argv)
2406 except urllib2.HTTPError, e:
2407 if e.code != 500:
2408 raise
2409 DieWithError(
2410 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2411 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002412
2413
2414if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002415 # These affect sys.stdout so do it outside of main() to simplify mocks in
2416 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002417 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002418 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002419 sys.exit(main(sys.argv[1:]))