blob: f6c1ae7d7567f1b500676c1493d575b1ebcca583 [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
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000035import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000036import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000037import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000038import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000039import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000040import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000041import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000042import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043import watchlists
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000044import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000045
maruel@chromium.org0633fb42013-08-16 20:06:14 +000046__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000047
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000048DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000049POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000050DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000051GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000052CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000053
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000054# Shortcut since it quickly becomes redundant.
55Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000056
maruel@chromium.orgddd59412011-11-30 14:20:38 +000057# Initialized in main()
58settings = None
59
60
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000061def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000062 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000063 sys.exit(1)
64
65
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000068 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000069 except subprocess2.CalledProcessError as e:
70 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000071 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000072 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000073 'Command "%s" failed.\n%s' % (
74 ' '.join(args), error_message or e.stdout or ''))
75 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000076
77
78def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000079 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000080 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000081
82
83def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000084 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000085 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +000086 env = os.environ.copy()
87 # 'cat' is a magical git string that disables pagers on all platforms.
88 env['GIT_PAGER'] = 'cat'
89 out, code = subprocess2.communicate(['git'] + args,
90 env=env,
bratell@opera.comf267b0e2013-05-02 09:11:43 +000091 stdout=subprocess2.PIPE)
szager@chromium.org9bb85e22012-06-13 20:28:23 +000092 return code, out[0]
93 except ValueError:
94 # When the subprocess fails, it returns None. That triggers a ValueError
95 # when trying to unpack the return value into (out, code).
96 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000097
98
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000099def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000100 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000101 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000102 return (version.startswith(prefix) and
103 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000104
105
maruel@chromium.org90541732011-04-01 17:54:18 +0000106def ask_for_data(prompt):
107 try:
108 return raw_input(prompt)
109 except KeyboardInterrupt:
110 # Hide the exception.
111 sys.exit(1)
112
113
iannucci@chromium.org79540052012-10-19 23:15:26 +0000114def git_set_branch_value(key, value):
115 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000116 if not branch:
117 return
118
119 cmd = ['config']
120 if isinstance(value, int):
121 cmd.append('--int')
122 git_key = 'branch.%s.%s' % (branch, key)
123 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000124
125
126def git_get_branch_default(key, default):
127 branch = Changelist().GetBranch()
128 if branch:
129 git_key = 'branch.%s.%s' % (branch, key)
130 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
131 try:
132 return int(stdout.strip())
133 except ValueError:
134 pass
135 return default
136
137
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000138def add_git_similarity(parser):
139 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000140 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000141 help='Sets the percentage that a pair of files need to match in order to'
142 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000143 parser.add_option(
144 '--find-copies', action='store_true',
145 help='Allows git to look for copies.')
146 parser.add_option(
147 '--no-find-copies', action='store_false', dest='find_copies',
148 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000149
150 old_parser_args = parser.parse_args
151 def Parse(args):
152 options, args = old_parser_args(args)
153
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000154 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000155 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000156 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000157 print('Note: Saving similarity of %d%% in git config.'
158 % options.similarity)
159 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000160
iannucci@chromium.org79540052012-10-19 23:15:26 +0000161 options.similarity = max(0, min(options.similarity, 100))
162
163 if options.find_copies is None:
164 options.find_copies = bool(
165 git_get_branch_default('git-find-copies', True))
166 else:
167 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000168
169 print('Using %d%% similarity for rename/copy detection. '
170 'Override with --similarity.' % options.similarity)
171
172 return options, args
173 parser.parse_args = Parse
174
175
ukai@chromium.org259e4682012-10-25 07:36:33 +0000176def is_dirty_git_tree(cmd):
177 # Make sure index is up-to-date before running diff-index.
178 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
179 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
180 if dirty:
181 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
182 print 'Uncommitted files: (git diff-index --name-status HEAD)'
183 print dirty[:4096]
184 if len(dirty) > 4096:
185 print '... (run "git diff-index --name-status HEAD" to see full output).'
186 return True
187 return False
188
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000189
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000190def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
191 """Return the corresponding git ref if |base_url| together with |glob_spec|
192 matches the full |url|.
193
194 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
195 """
196 fetch_suburl, as_ref = glob_spec.split(':')
197 if allow_wildcards:
198 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
199 if glob_match:
200 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
201 # "branches/{472,597,648}/src:refs/remotes/svn/*".
202 branch_re = re.escape(base_url)
203 if glob_match.group(1):
204 branch_re += '/' + re.escape(glob_match.group(1))
205 wildcard = glob_match.group(2)
206 if wildcard == '*':
207 branch_re += '([^/]*)'
208 else:
209 # Escape and replace surrounding braces with parentheses and commas
210 # with pipe symbols.
211 wildcard = re.escape(wildcard)
212 wildcard = re.sub('^\\\\{', '(', wildcard)
213 wildcard = re.sub('\\\\,', '|', wildcard)
214 wildcard = re.sub('\\\\}$', ')', wildcard)
215 branch_re += wildcard
216 if glob_match.group(3):
217 branch_re += re.escape(glob_match.group(3))
218 match = re.match(branch_re, url)
219 if match:
220 return re.sub('\*$', match.group(1), as_ref)
221
222 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
223 if fetch_suburl:
224 full_url = base_url + '/' + fetch_suburl
225 else:
226 full_url = base_url
227 if full_url == url:
228 return as_ref
229 return None
230
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000231
iannucci@chromium.org79540052012-10-19 23:15:26 +0000232def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000233 """Prints statistics about the change to the user."""
234 # --no-ext-diff is broken in some versions of Git, so try to work around
235 # this by overriding the environment (but there is still a problem if the
236 # git config key "diff.external" is used).
237 env = os.environ.copy()
238 if 'GIT_EXTERNAL_DIFF' in env:
239 del env['GIT_EXTERNAL_DIFF']
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000240 # 'cat' is a magical git string that disables pagers on all platforms.
241 env['GIT_PAGER'] = 'cat'
iannucci@chromium.org79540052012-10-19 23:15:26 +0000242
243 if find_copies:
244 similarity_options = ['--find-copies-harder', '-l100000',
245 '-C%s' % similarity]
246 else:
247 similarity_options = ['-M%s' % similarity]
248
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000249 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000250 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000251 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000252 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000253
254
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000255class Settings(object):
256 def __init__(self):
257 self.default_server = None
258 self.cc = None
259 self.root = None
260 self.is_git_svn = None
261 self.svn_branch = None
262 self.tree_status_url = None
263 self.viewvc_url = None
264 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000265 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000266 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000267
268 def LazyUpdateIfNeeded(self):
269 """Updates the settings from a codereview.settings file, if available."""
270 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000271 # The only value that actually changes the behavior is
272 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000273 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000274 error_ok=True
275 ).strip().lower()
276
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000277 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000278 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000279 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000280 # set updated to True to avoid infinite calling loop
281 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000282 self.updated = True
283 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000284 self.updated = True
285
286 def GetDefaultServerUrl(self, error_ok=False):
287 if not self.default_server:
288 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000289 self.default_server = gclient_utils.UpgradeToHttps(
290 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000291 if error_ok:
292 return self.default_server
293 if not self.default_server:
294 error_message = ('Could not find settings file. You must configure '
295 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000296 self.default_server = gclient_utils.UpgradeToHttps(
297 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000298 return self.default_server
299
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000300 def GetRoot(self):
301 if not self.root:
302 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
303 return self.root
304
305 def GetIsGitSvn(self):
306 """Return true if this repo looks like it's using git-svn."""
307 if self.is_git_svn is None:
308 # If you have any "svn-remote.*" config keys, we think you're using svn.
309 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000310 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000311 return self.is_git_svn
312
313 def GetSVNBranch(self):
314 if self.svn_branch is None:
315 if not self.GetIsGitSvn():
316 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
317
318 # Try to figure out which remote branch we're based on.
319 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000320 # 1) iterate through our branch history and find the svn URL.
321 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000322
323 # regexp matching the git-svn line that contains the URL.
324 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
325
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000326 env = os.environ.copy()
327 # 'cat' is a magical git string that disables pagers on all platforms.
328 env['GIT_PAGER'] = 'cat'
329
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000330 # We don't want to go through all of history, so read a line from the
331 # pipe at a time.
332 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000333 cmd = ['git', 'log', '-100', '--pretty=medium']
334 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE, env=env)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000335 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000336 for line in proc.stdout:
337 match = git_svn_re.match(line)
338 if match:
339 url = match.group(1)
340 proc.stdout.close() # Cut pipe.
341 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000342
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000343 if url:
344 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
345 remotes = RunGit(['config', '--get-regexp',
346 r'^svn-remote\..*\.url']).splitlines()
347 for remote in remotes:
348 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000349 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000350 remote = match.group(1)
351 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000352 rewrite_root = RunGit(
353 ['config', 'svn-remote.%s.rewriteRoot' % remote],
354 error_ok=True).strip()
355 if rewrite_root:
356 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000357 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000358 ['config', 'svn-remote.%s.fetch' % remote],
359 error_ok=True).strip()
360 if fetch_spec:
361 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
362 if self.svn_branch:
363 break
364 branch_spec = RunGit(
365 ['config', 'svn-remote.%s.branches' % remote],
366 error_ok=True).strip()
367 if branch_spec:
368 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
369 if self.svn_branch:
370 break
371 tag_spec = RunGit(
372 ['config', 'svn-remote.%s.tags' % remote],
373 error_ok=True).strip()
374 if tag_spec:
375 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
376 if self.svn_branch:
377 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000378
379 if not self.svn_branch:
380 DieWithError('Can\'t guess svn branch -- try specifying it on the '
381 'command line')
382
383 return self.svn_branch
384
385 def GetTreeStatusUrl(self, error_ok=False):
386 if not self.tree_status_url:
387 error_message = ('You must configure your tree status URL by running '
388 '"git cl config".')
389 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
390 error_ok=error_ok,
391 error_message=error_message)
392 return self.tree_status_url
393
394 def GetViewVCUrl(self):
395 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000396 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000397 return self.viewvc_url
398
rmistry@google.com90752582014-01-14 21:04:50 +0000399 def GetBugPrefix(self):
400 return self._GetConfig('rietveld.bug-prefix', error_ok=True)
401
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000402 def GetDefaultCCList(self):
403 return self._GetConfig('rietveld.cc', error_ok=True)
404
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000405 def GetDefaultPrivateFlag(self):
406 return self._GetConfig('rietveld.private', error_ok=True)
407
ukai@chromium.orge8077812012-02-03 03:41:46 +0000408 def GetIsGerrit(self):
409 """Return true if this repo is assosiated with gerrit code review system."""
410 if self.is_gerrit is None:
411 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
412 return self.is_gerrit
413
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000414 def GetGitEditor(self):
415 """Return the editor specified in the git config, or None if none is."""
416 if self.git_editor is None:
417 self.git_editor = self._GetConfig('core.editor', error_ok=True)
418 return self.git_editor or None
419
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000420 def _GetConfig(self, param, **kwargs):
421 self.LazyUpdateIfNeeded()
422 return RunGit(['config', param], **kwargs).strip()
423
424
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000425def ShortBranchName(branch):
426 """Convert a name like 'refs/heads/foo' to just 'foo'."""
427 return branch.replace('refs/heads/', '')
428
429
430class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000431 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000432 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000433 global settings
434 if not settings:
435 # Happens when git_cl.py is used as a utility library.
436 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000437 settings.GetDefaultServerUrl()
438 self.branchref = branchref
439 if self.branchref:
440 self.branch = ShortBranchName(self.branchref)
441 else:
442 self.branch = None
443 self.rietveld_server = None
444 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000445 self.lookedup_issue = False
446 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000447 self.has_description = False
448 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000449 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000450 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000451 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000452 self.cc = None
453 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000454 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000455 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000456
457 def GetCCList(self):
458 """Return the users cc'd on this CL.
459
460 Return is a string suitable for passing to gcl with the --cc flag.
461 """
462 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000463 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000464 more_cc = ','.join(self.watchers)
465 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
466 return self.cc
467
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000468 def GetCCListWithoutDefault(self):
469 """Return the users cc'd on this CL excluding default ones."""
470 if self.cc is None:
471 self.cc = ','.join(self.watchers)
472 return self.cc
473
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000474 def SetWatchers(self, watchers):
475 """Set the list of email addresses that should be cc'd based on the changed
476 files in this CL.
477 """
478 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000479
480 def GetBranch(self):
481 """Returns the short branch name, e.g. 'master'."""
482 if not self.branch:
483 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
484 self.branch = ShortBranchName(self.branchref)
485 return self.branch
486
487 def GetBranchRef(self):
488 """Returns the full branch name, e.g. 'refs/heads/master'."""
489 self.GetBranch() # Poke the lazy loader.
490 return self.branchref
491
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000492 @staticmethod
493 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000494 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000495 e.g. 'origin', 'refs/heads/master'
496 """
497 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000498 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
499 error_ok=True).strip()
500 if upstream_branch:
501 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
502 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000503 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
504 error_ok=True).strip()
505 if upstream_branch:
506 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000507 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000508 # Fall back on trying a git-svn upstream branch.
509 if settings.GetIsGitSvn():
510 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000511 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000512 # Else, try to guess the origin remote.
513 remote_branches = RunGit(['branch', '-r']).split()
514 if 'origin/master' in remote_branches:
515 # Fall back on origin/master if it exits.
516 remote = 'origin'
517 upstream_branch = 'refs/heads/master'
518 elif 'origin/trunk' in remote_branches:
519 # Fall back on origin/trunk if it exists. Generally a shared
520 # git-svn clone
521 remote = 'origin'
522 upstream_branch = 'refs/heads/trunk'
523 else:
524 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000525Either pass complete "git diff"-style arguments, like
526 git cl upload origin/master
527or verify this branch is set up to track another (via the --track argument to
528"git checkout -b ...").""")
529
530 return remote, upstream_branch
531
532 def GetUpstreamBranch(self):
533 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000534 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000535 if remote is not '.':
536 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
537 self.upstream_branch = upstream_branch
538 return self.upstream_branch
539
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000540 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000541 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000542 remote, branch = None, self.GetBranch()
543 seen_branches = set()
544 while branch not in seen_branches:
545 seen_branches.add(branch)
546 remote, branch = self.FetchUpstreamTuple(branch)
547 branch = ShortBranchName(branch)
548 if remote != '.' or branch.startswith('refs/remotes'):
549 break
550 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000551 remotes = RunGit(['remote'], error_ok=True).split()
552 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000553 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000554 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000555 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000556 logging.warning('Could not determine which remote this change is '
557 'associated with, so defaulting to "%s". This may '
558 'not be what you want. You may prevent this message '
559 'by running "git svn info" as documented here: %s',
560 self._remote,
561 GIT_INSTRUCTIONS_URL)
562 else:
563 logging.warn('Could not determine which remote this change is '
564 'associated with. You may prevent this message by '
565 'running "git svn info" as documented here: %s',
566 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000567 branch = 'HEAD'
568 if branch.startswith('refs/remotes'):
569 self._remote = (remote, branch)
570 else:
571 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000572 return self._remote
573
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000574 def GitSanityChecks(self, upstream_git_obj):
575 """Checks git repo status and ensures diff is from local commits."""
576
577 # Verify the commit we're diffing against is in our current branch.
578 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
579 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
580 if upstream_sha != common_ancestor:
581 print >> sys.stderr, (
582 'ERROR: %s is not in the current branch. You may need to rebase '
583 'your tracking branch' % upstream_sha)
584 return False
585
586 # List the commits inside the diff, and verify they are all local.
587 commits_in_diff = RunGit(
588 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
589 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
590 remote_branch = remote_branch.strip()
591 if code != 0:
592 _, remote_branch = self.GetRemoteBranch()
593
594 commits_in_remote = RunGit(
595 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
596
597 common_commits = set(commits_in_diff) & set(commits_in_remote)
598 if common_commits:
599 print >> sys.stderr, (
600 'ERROR: Your diff contains %d commits already in %s.\n'
601 'Run "git log --oneline %s..HEAD" to get a list of commits in '
602 'the diff. If you are using a custom git flow, you can override'
603 ' the reference used for this check with "git config '
604 'gitcl.remotebranch <git-ref>".' % (
605 len(common_commits), remote_branch, upstream_git_obj))
606 return False
607 return True
608
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000609 def GetGitBaseUrlFromConfig(self):
610 """Return the configured base URL from branch.<branchname>.baseurl.
611
612 Returns None if it is not set.
613 """
614 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
615 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000616
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000617 def GetRemoteUrl(self):
618 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
619
620 Returns None if there is no remote.
621 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000622 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000623 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
624
625 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000626 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000627 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000628 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000629 self.issue = int(issue) or None if issue else None
630 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000631 return self.issue
632
633 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000634 if not self.rietveld_server:
635 # If we're on a branch then get the server potentially associated
636 # with that branch.
637 if self.GetIssue():
638 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
639 ['config', self._RietveldServer()], error_ok=True).strip())
640 if not self.rietveld_server:
641 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000642 return self.rietveld_server
643
644 def GetIssueURL(self):
645 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000646 if not self.GetIssue():
647 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000648 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
649
650 def GetDescription(self, pretty=False):
651 if not self.has_description:
652 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000653 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000654 try:
655 self.description = self.RpcServer().get_description(issue).strip()
656 except urllib2.HTTPError, e:
657 if e.code == 404:
658 DieWithError(
659 ('\nWhile fetching the description for issue %d, received a '
660 '404 (not found)\n'
661 'error. It is likely that you deleted this '
662 'issue on the server. If this is the\n'
663 'case, please run\n\n'
664 ' git cl issue 0\n\n'
665 'to clear the association with the deleted issue. Then run '
666 'this command again.') % issue)
667 else:
668 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000669 '\nFailed to fetch issue description. HTTP error %d' % e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000670 self.has_description = True
671 if pretty:
672 wrapper = textwrap.TextWrapper()
673 wrapper.initial_indent = wrapper.subsequent_indent = ' '
674 return wrapper.fill(self.description)
675 return self.description
676
677 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000678 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000679 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000680 patchset = RunGit(['config', self._PatchsetSetting()],
681 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000682 self.patchset = int(patchset) or None if patchset else None
683 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000684 return self.patchset
685
686 def SetPatchset(self, patchset):
687 """Set this branch's patchset. If patchset=0, clears the patchset."""
688 if patchset:
689 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000690 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000691 else:
692 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000693 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000694 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000695
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000696 def GetMostRecentPatchset(self):
697 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000698
699 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000700 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000701 '/download/issue%s_%s.diff' % (issue, patchset))
702
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000703 def GetIssueProperties(self):
704 if self._props is None:
705 issue = self.GetIssue()
706 if not issue:
707 self._props = {}
708 else:
709 self._props = self.RpcServer().get_issue_properties(issue, True)
710 return self._props
711
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000712 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000713 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000714
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000715 def SetIssue(self, issue):
716 """Set this branch's issue. If issue=0, clears the issue."""
717 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000718 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719 RunGit(['config', self._IssueSetting(), str(issue)])
720 if self.rietveld_server:
721 RunGit(['config', self._RietveldServer(), self.rietveld_server])
722 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000723 current_issue = self.GetIssue()
724 if current_issue:
725 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000726 self.issue = None
727 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000728
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000729 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000730 if not self.GitSanityChecks(upstream_branch):
731 DieWithError('\nGit sanity check failure')
732
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000733 env = os.environ.copy()
734 # 'cat' is a magical git string that disables pagers on all platforms.
735 env['GIT_PAGER'] = 'cat'
736
737 root = RunCommand(['git', 'rev-parse', '--show-cdup'], env=env).strip()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000738 if not root:
739 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000740 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000741
742 # We use the sha1 of HEAD as a name of this change.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000743 name = RunCommand(['git', 'rev-parse', 'HEAD'], env=env).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000744 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000745 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000746 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000747 except subprocess2.CalledProcessError:
748 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000749 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000750 'This branch probably doesn\'t exist anymore. To reset the\n'
751 'tracking branch, please run\n'
752 ' git branch --set-upstream %s trunk\n'
753 'replacing trunk with origin/master or the relevant branch') %
754 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000755
maruel@chromium.org52424302012-08-29 15:14:30 +0000756 issue = self.GetIssue()
757 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000758 if issue:
759 description = self.GetDescription()
760 else:
761 # If the change was never uploaded, use the log messages of all commits
762 # up to the branch point, as git cl upload will prefill the description
763 # with these log messages.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000764 description = RunCommand(['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000765 'log', '--pretty=format:%s%n%n%b',
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000766 '%s...' % (upstream_branch)],
767 env=env).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000768
769 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000770 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000771 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000772 name,
773 description,
774 absroot,
775 files,
776 issue,
777 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000778 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000779
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000780 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000781 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000782
783 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000784 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000785 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000786 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000787 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000788 except presubmit_support.PresubmitFailure, e:
789 DieWithError(
790 ('%s\nMaybe your depot_tools is out of date?\n'
791 'If all fails, contact maruel@') % e)
792
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000793 def UpdateDescription(self, description):
794 self.description = description
795 return self.RpcServer().update_description(
796 self.GetIssue(), self.description)
797
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000798 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000799 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000800 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000802 def SetFlag(self, flag, value):
803 """Patchset must match."""
804 if not self.GetPatchset():
805 DieWithError('The patchset needs to match. Send another patchset.')
806 try:
807 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000808 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000809 except urllib2.HTTPError, e:
810 if e.code == 404:
811 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
812 if e.code == 403:
813 DieWithError(
814 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
815 'match?') % (self.GetIssue(), self.GetPatchset()))
816 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000817
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000818 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000819 """Returns an upload.RpcServer() to access this review's rietveld instance.
820 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000821 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000822 self._rpc_server = rietveld.CachingRietveld(
823 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000824 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000825
826 def _IssueSetting(self):
827 """Return the git setting that stores this change's issue."""
828 return 'branch.%s.rietveldissue' % self.GetBranch()
829
830 def _PatchsetSetting(self):
831 """Return the git setting that stores this change's most recent patchset."""
832 return 'branch.%s.rietveldpatchset' % self.GetBranch()
833
834 def _RietveldServer(self):
835 """Returns the git setting that stores this change's rietveld server."""
836 return 'branch.%s.rietveldserver' % self.GetBranch()
837
838
839def GetCodereviewSettingsInteractively():
840 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000841 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000842 server = settings.GetDefaultServerUrl(error_ok=True)
843 prompt = 'Rietveld server (host[:port])'
844 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000845 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000846 if not server and not newserver:
847 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000848 if newserver:
849 newserver = gclient_utils.UpgradeToHttps(newserver)
850 if newserver != server:
851 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000852
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000853 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000854 prompt = caption
855 if initial:
856 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000857 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000858 if new_val == 'x':
859 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000860 elif new_val:
861 if is_url:
862 new_val = gclient_utils.UpgradeToHttps(new_val)
863 if new_val != initial:
864 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000865
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000866 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000867 SetProperty(settings.GetDefaultPrivateFlag(),
868 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000869 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000870 'tree-status-url', False)
871 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000872 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000873
874 # TODO: configure a default branch to diff against, rather than this
875 # svn-based hackery.
876
877
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000878class ChangeDescription(object):
879 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000880 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000881 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000882
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000883 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000884 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000885
agable@chromium.org42c20792013-09-12 17:34:49 +0000886 @property # www.logilab.org/ticket/89786
887 def description(self): # pylint: disable=E0202
888 return '\n'.join(self._description_lines)
889
890 def set_description(self, desc):
891 if isinstance(desc, basestring):
892 lines = desc.splitlines()
893 else:
894 lines = [line.rstrip() for line in desc]
895 while lines and not lines[0]:
896 lines.pop(0)
897 while lines and not lines[-1]:
898 lines.pop(-1)
899 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000900
901 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000902 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000903 assert isinstance(reviewers, list), reviewers
904 if not reviewers:
905 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000906 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000907
agable@chromium.org42c20792013-09-12 17:34:49 +0000908 # Get the set of R= and TBR= lines and remove them from the desciption.
909 regexp = re.compile(self.R_LINE)
910 matches = [regexp.match(line) for line in self._description_lines]
911 new_desc = [l for i, l in enumerate(self._description_lines)
912 if not matches[i]]
913 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000914
agable@chromium.org42c20792013-09-12 17:34:49 +0000915 # Construct new unified R= and TBR= lines.
916 r_names = []
917 tbr_names = []
918 for match in matches:
919 if not match:
920 continue
921 people = cleanup_list([match.group(2).strip()])
922 if match.group(1) == 'TBR':
923 tbr_names.extend(people)
924 else:
925 r_names.extend(people)
926 for name in r_names:
927 if name not in reviewers:
928 reviewers.append(name)
929 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
930 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
931
932 # Put the new lines in the description where the old first R= line was.
933 line_loc = next((i for i, match in enumerate(matches) if match), -1)
934 if 0 <= line_loc < len(self._description_lines):
935 if new_tbr_line:
936 self._description_lines.insert(line_loc, new_tbr_line)
937 if new_r_line:
938 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000939 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000940 if new_r_line:
941 self.append_footer(new_r_line)
942 if new_tbr_line:
943 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000944
945 def prompt(self):
946 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000947 self.set_description([
948 '# Enter a description of the change.',
949 '# This will be displayed on the codereview site.',
950 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000951 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000952 '--------------------',
953 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000954
agable@chromium.org42c20792013-09-12 17:34:49 +0000955 regexp = re.compile(self.BUG_LINE)
956 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +0000957 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +0000958 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000959 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000960 if not content:
961 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000962 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000963
964 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000965 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
966 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000967 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000968 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000969
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000970 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000971 if self._description_lines:
972 # Add an empty line if either the last line or the new line isn't a tag.
973 last_line = self._description_lines[-1]
974 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
975 not presubmit_support.Change.TAG_LINE_RE.match(line)):
976 self._description_lines.append('')
977 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000978
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000979 def get_reviewers(self):
980 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000981 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
982 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000983 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000984
985
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000986def get_approving_reviewers(props):
987 """Retrieves the reviewers that approved a CL from the issue properties with
988 messages.
989
990 Note that the list may contain reviewers that are not committer, thus are not
991 considered by the CQ.
992 """
993 return sorted(
994 set(
995 message['sender']
996 for message in props['messages']
997 if message['approval'] and message['sender'] in props['reviewers']
998 )
999 )
1000
1001
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002def FindCodereviewSettingsFile(filename='codereview.settings'):
1003 """Finds the given file starting in the cwd and going up.
1004
1005 Only looks up to the top of the repository unless an
1006 'inherit-review-settings-ok' file exists in the root of the repository.
1007 """
1008 inherit_ok_file = 'inherit-review-settings-ok'
1009 cwd = os.getcwd()
1010 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
1011 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1012 root = '/'
1013 while True:
1014 if filename in os.listdir(cwd):
1015 if os.path.isfile(os.path.join(cwd, filename)):
1016 return open(os.path.join(cwd, filename))
1017 if cwd == root:
1018 break
1019 cwd = os.path.dirname(cwd)
1020
1021
1022def LoadCodereviewSettingsFromFile(fileobj):
1023 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001024 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001025
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001026 def SetProperty(name, setting, unset_error_ok=False):
1027 fullname = 'rietveld.' + name
1028 if setting in keyvals:
1029 RunGit(['config', fullname, keyvals[setting]])
1030 else:
1031 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1032
1033 SetProperty('server', 'CODE_REVIEW_SERVER')
1034 # Only server setting is required. Other settings can be absent.
1035 # In that case, we ignore errors raised during option deletion attempt.
1036 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001037 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001038 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1039 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001040 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001041
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001042 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001043 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001044
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001045 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1046 #should be of the form
1047 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1048 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1049 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1050 keyvals['ORIGIN_URL_CONFIG']])
1051
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001052
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001053def urlretrieve(source, destination):
1054 """urllib is broken for SSL connections via a proxy therefore we
1055 can't use urllib.urlretrieve()."""
1056 with open(destination, 'w') as f:
1057 f.write(urllib2.urlopen(source).read())
1058
1059
ukai@chromium.org712d6102013-11-27 00:52:58 +00001060def hasSheBang(fname):
1061 """Checks fname is a #! script."""
1062 with open(fname) as f:
1063 return f.read(2).startswith('#!')
1064
1065
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001066def DownloadHooks(force):
1067 """downloads hooks
1068
1069 Args:
1070 force: True to update hooks. False to install hooks if not present.
1071 """
1072 if not settings.GetIsGerrit():
1073 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001074 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001075 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1076 if not os.access(dst, os.X_OK):
1077 if os.path.exists(dst):
1078 if not force:
1079 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001080 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001081 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001082 if not hasSheBang(dst):
1083 DieWithError('Not a script: %s\n'
1084 'You need to download from\n%s\n'
1085 'into .git/hooks/commit-msg and '
1086 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001087 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1088 except Exception:
1089 if os.path.exists(dst):
1090 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001091 DieWithError('\nFailed to download hooks.\n'
1092 'You need to download from\n%s\n'
1093 'into .git/hooks/commit-msg and '
1094 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001095
1096
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001097@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001098def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001099 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001101 parser.add_option('--activate-update', action='store_true',
1102 help='activate auto-updating [rietveld] section in '
1103 '.git/config')
1104 parser.add_option('--deactivate-update', action='store_true',
1105 help='deactivate auto-updating [rietveld] section in '
1106 '.git/config')
1107 options, args = parser.parse_args(args)
1108
1109 if options.deactivate_update:
1110 RunGit(['config', 'rietveld.autoupdate', 'false'])
1111 return
1112
1113 if options.activate_update:
1114 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1115 return
1116
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001117 if len(args) == 0:
1118 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001119 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120 return 0
1121
1122 url = args[0]
1123 if not url.endswith('codereview.settings'):
1124 url = os.path.join(url, 'codereview.settings')
1125
1126 # Load code review settings and download hooks (if available).
1127 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001128 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129 return 0
1130
1131
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001132def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001133 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001134 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1135 branch = ShortBranchName(branchref)
1136 _, args = parser.parse_args(args)
1137 if not args:
1138 print("Current base-url:")
1139 return RunGit(['config', 'branch.%s.base-url' % branch],
1140 error_ok=False).strip()
1141 else:
1142 print("Setting base-url to %s" % args[0])
1143 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1144 error_ok=False).strip()
1145
1146
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001147def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001148 """Show status of changelists.
1149
1150 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001151 - Red not sent for review or broken
1152 - Blue waiting for review
1153 - Yellow waiting for you to reply to review
1154 - Green LGTM'ed
1155 - Magenta in the commit queue
1156 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001157
1158 Also see 'git cl comments'.
1159 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160 parser.add_option('--field',
1161 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001162 parser.add_option('-f', '--fast', action='store_true',
1163 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001164 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001165 if args:
1166 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001167
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001168 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001169 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170 if options.field.startswith('desc'):
1171 print cl.GetDescription()
1172 elif options.field == 'id':
1173 issueid = cl.GetIssue()
1174 if issueid:
1175 print issueid
1176 elif options.field == 'patch':
1177 patchset = cl.GetPatchset()
1178 if patchset:
1179 print patchset
1180 elif options.field == 'url':
1181 url = cl.GetIssueURL()
1182 if url:
1183 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001184 return 0
1185
1186 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1187 if not branches:
1188 print('No local branch found.')
1189 return 0
1190
1191 changes = (Changelist(branchref=b) for b in branches.splitlines())
1192 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1193 alignment = max(5, max(len(b) for b in branches))
1194 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001195 # Adhoc thread pool to request data concurrently.
1196 output = Queue.Queue()
1197
1198 # Silence upload.py otherwise it becomes unweldly.
1199 upload.verbosity = 0
1200
1201 if not options.fast:
1202 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001203 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001204 c = Changelist(branchref=b)
1205 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001206 props = {}
1207 r = None
1208 if i:
1209 try:
1210 props = c.GetIssueProperties()
1211 r = c.GetApprovingReviewers() if i else None
1212 except urllib2.HTTPError:
1213 # The issue probably doesn't exist anymore.
1214 i += ' (broken)'
1215
1216 msgs = props.get('messages') or []
1217
1218 if not i:
1219 color = Fore.WHITE
1220 elif props.get('closed'):
1221 # Issue is closed.
1222 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001223 elif props.get('commit'):
1224 # Issue is in the commit queue.
1225 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001226 elif r:
1227 # Was LGTM'ed.
1228 color = Fore.GREEN
1229 elif not msgs:
1230 # No message was sent.
1231 color = Fore.RED
1232 elif msgs[-1]['sender'] != props.get('owner_email'):
1233 color = Fore.YELLOW
1234 else:
1235 color = Fore.BLUE
1236 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001237
1238 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1239 for t in threads:
1240 t.daemon = True
1241 t.start()
1242 else:
1243 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1244 for b in branches:
1245 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001246 url = c.GetIssueURL()
1247 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001248
1249 tmp = {}
1250 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001251 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001252 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001253 b, i, color = output.get()
1254 tmp[b] = (i, color)
1255 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001256 reset = Fore.RESET
1257 if not sys.stdout.isatty():
1258 color = ''
1259 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001260 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001261 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001262
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001263 cl = Changelist()
1264 print
1265 print 'Current branch:',
1266 if not cl.GetIssue():
1267 print 'no issue assigned.'
1268 return 0
1269 print cl.GetBranch()
1270 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1271 print 'Issue description:'
1272 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001273 return 0
1274
1275
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001276def colorize_CMDstatus_doc():
1277 """To be called once in main() to add colors to git cl status help."""
1278 colors = [i for i in dir(Fore) if i[0].isupper()]
1279
1280 def colorize_line(line):
1281 for color in colors:
1282 if color in line.upper():
1283 # Extract whitespaces first and the leading '-'.
1284 indent = len(line) - len(line.lstrip(' ')) + 1
1285 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1286 return line
1287
1288 lines = CMDstatus.__doc__.splitlines()
1289 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1290
1291
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001292@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001294 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001295
1296 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001297 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001298 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001299
1300 cl = Changelist()
1301 if len(args) > 0:
1302 try:
1303 issue = int(args[0])
1304 except ValueError:
1305 DieWithError('Pass a number to set the issue or none to list it.\n'
1306 'Maybe you want to run git cl status?')
1307 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001308 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001309 return 0
1310
1311
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001312def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001313 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001314 (_, args) = parser.parse_args(args)
1315 if args:
1316 parser.error('Unsupported argument: %s' % args)
1317
1318 cl = Changelist()
1319 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001320 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001321 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001322 if message['disapproval']:
1323 color = Fore.RED
1324 elif message['approval']:
1325 color = Fore.GREEN
1326 elif message['sender'] == data['owner_email']:
1327 color = Fore.MAGENTA
1328 else:
1329 color = Fore.BLUE
1330 print '\n%s%s %s%s' % (
1331 color, message['date'].split('.', 1)[0], message['sender'],
1332 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001333 if message['text'].strip():
1334 print '\n'.join(' ' + l for l in message['text'].splitlines())
1335 return 0
1336
1337
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001338def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001339 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001340 cl = Changelist()
1341 if not cl.GetIssue():
1342 DieWithError('This branch has no associated changelist.')
1343 description = ChangeDescription(cl.GetDescription())
1344 description.prompt()
1345 cl.UpdateDescription(description.description)
1346 return 0
1347
1348
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349def CreateDescriptionFromLog(args):
1350 """Pulls out the commit log to use as a base for the CL description."""
1351 log_args = []
1352 if len(args) == 1 and not args[0].endswith('.'):
1353 log_args = [args[0] + '..']
1354 elif len(args) == 1 and args[0].endswith('...'):
1355 log_args = [args[0][:-1]]
1356 elif len(args) == 2:
1357 log_args = [args[0] + '..' + args[1]]
1358 else:
1359 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001360 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001361
1362
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001363def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001364 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001365 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001367 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001368 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001369 (options, args) = parser.parse_args(args)
1370
ukai@chromium.org259e4682012-10-25 07:36:33 +00001371 if not options.force and is_dirty_git_tree('presubmit'):
1372 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001373 return 1
1374
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001375 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001376 if args:
1377 base_branch = args[0]
1378 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001379 # Default to diffing against the common ancestor of the upstream branch.
1380 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001381
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001382 cl.RunHook(
1383 committing=not options.upload,
1384 may_prompt=False,
1385 verbose=options.verbose,
1386 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001387 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001388
1389
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001390def AddChangeIdToCommitMessage(options, args):
1391 """Re-commits using the current message, assumes the commit hook is in
1392 place.
1393 """
1394 log_desc = options.message or CreateDescriptionFromLog(args)
1395 git_command = ['commit', '--amend', '-m', log_desc]
1396 RunGit(git_command)
1397 new_log_desc = CreateDescriptionFromLog(args)
1398 if CHANGE_ID in new_log_desc:
1399 print 'git-cl: Added Change-Id to commit message.'
1400 else:
1401 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1402
1403
ukai@chromium.orge8077812012-02-03 03:41:46 +00001404def GerritUpload(options, args, cl):
1405 """upload the current branch to gerrit."""
1406 # We assume the remote called "origin" is the one we want.
1407 # It is probably not worthwhile to support different workflows.
1408 remote = 'origin'
1409 branch = 'master'
1410 if options.target_branch:
1411 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001413 change_desc = ChangeDescription(
1414 options.message or CreateDescriptionFromLog(args))
1415 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001416 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001418 if CHANGE_ID not in change_desc.description:
1419 AddChangeIdToCommitMessage(options, args)
1420 if options.reviewers:
1421 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422
ukai@chromium.orge8077812012-02-03 03:41:46 +00001423 receive_options = []
1424 cc = cl.GetCCList().split(',')
1425 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001426 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001427 cc = filter(None, cc)
1428 if cc:
1429 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001430 if change_desc.get_reviewers():
1431 receive_options.extend(
1432 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001433
ukai@chromium.orge8077812012-02-03 03:41:46 +00001434 git_command = ['push']
1435 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001436 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001437 ' '.join(receive_options))
1438 git_command += [remote, 'HEAD:refs/for/' + branch]
1439 RunGit(git_command)
1440 # TODO(ukai): parse Change-Id: and set issue number?
1441 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001442
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443
ukai@chromium.orge8077812012-02-03 03:41:46 +00001444def RietveldUpload(options, args, cl):
1445 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001446 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1447 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001448 if options.emulate_svn_auto_props:
1449 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001450
1451 change_desc = None
1452
pgervais@chromium.org91141372014-01-09 23:27:20 +00001453 if options.email is not None:
1454 upload_args.extend(['--email', options.email])
1455
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001456 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001457 if options.title:
1458 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001459 if options.message:
1460 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001461 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001462 print ("This branch is associated with issue %s. "
1463 "Adding patch to that issue." % cl.GetIssue())
1464 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001465 if options.title:
1466 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001467 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001468 change_desc = ChangeDescription(message)
1469 if options.reviewers:
1470 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001471 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001472 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001473
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001474 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475 print "Description is empty; aborting."
1476 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001477
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001478 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001479 if change_desc.get_reviewers():
1480 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001481 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001482 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001483 DieWithError("Must specify reviewers to send email.")
1484 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001485
1486 # We check this before applying rietveld.private assuming that in
1487 # rietveld.cc only addresses which we can send private CLs to are listed
1488 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1489 # --private is specified explicitly on the command line.
1490 if options.private:
1491 logging.warn('rietveld.cc is ignored since private flag is specified. '
1492 'You need to review and add them manually if necessary.')
1493 cc = cl.GetCCListWithoutDefault()
1494 else:
1495 cc = cl.GetCCList()
1496 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001497 if cc:
1498 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001499
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001500 if options.private or settings.GetDefaultPrivateFlag() == "True":
1501 upload_args.append('--private')
1502
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001503 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001504 if not options.find_copies:
1505 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001506
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001507 # Include the upstream repo's URL in the change -- this is useful for
1508 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001509 remote_url = cl.GetGitBaseUrlFromConfig()
1510 if not remote_url:
1511 if settings.GetIsGitSvn():
1512 # URL is dependent on the current directory.
1513 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1514 if data:
1515 keys = dict(line.split(': ', 1) for line in data.splitlines()
1516 if ': ' in line)
1517 remote_url = keys.get('URL', None)
1518 else:
1519 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1520 remote_url = (cl.GetRemoteUrl() + '@'
1521 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001522 if remote_url:
1523 upload_args.extend(['--base_url', remote_url])
1524
1525 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001526 upload_args = ['upload'] + upload_args + args
1527 logging.info('upload.RealMain(%s)', upload_args)
1528 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001529 issue = int(issue)
1530 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001531 except KeyboardInterrupt:
1532 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001533 except:
1534 # If we got an exception after the user typed a description for their
1535 # change, back up the description before re-raising.
1536 if change_desc:
1537 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1538 print '\nGot exception while uploading -- saving description to %s\n' \
1539 % backup_path
1540 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001541 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001542 backup_file.close()
1543 raise
1544
1545 if not cl.GetIssue():
1546 cl.SetIssue(issue)
1547 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001548
1549 if options.use_commit_queue:
1550 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001551 return 0
1552
1553
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001554def cleanup_list(l):
1555 """Fixes a list so that comma separated items are put as individual items.
1556
1557 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1558 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1559 """
1560 items = sum((i.split(',') for i in l), [])
1561 stripped_items = (i.strip() for i in items)
1562 return sorted(filter(None, stripped_items))
1563
1564
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001565@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001566def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001567 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001568 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1569 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001570 parser.add_option('--bypass-watchlists', action='store_true',
1571 dest='bypass_watchlists',
1572 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001573 parser.add_option('-f', action='store_true', dest='force',
1574 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001575 parser.add_option('-m', dest='message', help='message for patchset')
1576 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001577 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001578 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001579 help='reviewer email addresses')
1580 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001581 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001582 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001583 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001584 help='send email to reviewer immediately')
1585 parser.add_option("--emulate_svn_auto_props", action="store_true",
1586 dest="emulate_svn_auto_props",
1587 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001588 parser.add_option('-c', '--use-commit-queue', action='store_true',
1589 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001590 parser.add_option('--private', action='store_true',
1591 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001592 parser.add_option('--target_branch',
1593 help='When uploading to gerrit, remote branch to '
1594 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001595 parser.add_option('--email', default=None,
1596 help='email address to use to connect to Rietveld')
1597
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001598 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001599 (options, args) = parser.parse_args(args)
1600
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001601 if options.target_branch and not settings.GetIsGerrit():
1602 parser.error('Use --target_branch for non gerrit repository.')
1603
ukai@chromium.org259e4682012-10-25 07:36:33 +00001604 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001605 return 1
1606
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001607 options.reviewers = cleanup_list(options.reviewers)
1608 options.cc = cleanup_list(options.cc)
1609
ukai@chromium.orge8077812012-02-03 03:41:46 +00001610 cl = Changelist()
1611 if args:
1612 # TODO(ukai): is it ok for gerrit case?
1613 base_branch = args[0]
1614 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001615 # Default to diffing against common ancestor of upstream branch
1616 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001617 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001618
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001619 # Apply watchlists on upload.
1620 change = cl.GetChange(base_branch, None)
1621 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1622 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001623 if not options.bypass_watchlists:
1624 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001625
ukai@chromium.orge8077812012-02-03 03:41:46 +00001626 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001627 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001628 may_prompt=not options.force,
1629 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001630 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001631 if not hook_results.should_continue():
1632 return 1
1633 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001634 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001635
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001636 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001637 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001638 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001639 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001640 print ('The last upload made from this repository was patchset #%d but '
1641 'the most recent patchset on the server is #%d.'
1642 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001643 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1644 'from another machine or branch the patch you\'re uploading now '
1645 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001646 ask_for_data('About to upload; enter to confirm.')
1647
iannucci@chromium.org79540052012-10-19 23:15:26 +00001648 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001649 if settings.GetIsGerrit():
1650 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001651 ret = RietveldUpload(options, args, cl)
1652 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001653 git_set_branch_value('last-upload-hash',
1654 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001655
1656 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001657
1658
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001659def IsSubmoduleMergeCommit(ref):
1660 # When submodules are added to the repo, we expect there to be a single
1661 # non-git-svn merge commit at remote HEAD with a signature comment.
1662 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001663 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001664 return RunGit(cmd) != ''
1665
1666
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001667def SendUpstream(parser, args, cmd):
1668 """Common code for CmdPush and CmdDCommit
1669
1670 Squashed commit into a single.
1671 Updates changelog with metadata (e.g. pointer to review).
1672 Pushes/dcommits the code upstream.
1673 Updates review and closes.
1674 """
1675 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1676 help='bypass upload presubmit hook')
1677 parser.add_option('-m', dest='message',
1678 help="override review description")
1679 parser.add_option('-f', action='store_true', dest='force',
1680 help="force yes to questions (don't prompt)")
1681 parser.add_option('-c', dest='contributor',
1682 help="external contributor for patch (appended to " +
1683 "description and used as author for git). Should be " +
1684 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001685 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001686 (options, args) = parser.parse_args(args)
1687 cl = Changelist()
1688
1689 if not args or cmd == 'push':
1690 # Default to merging against our best guess of the upstream branch.
1691 args = [cl.GetUpstreamBranch()]
1692
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001693 if options.contributor:
1694 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1695 print "Please provide contibutor as 'First Last <email@example.com>'"
1696 return 1
1697
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001698 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001699 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001700
ukai@chromium.org259e4682012-10-25 07:36:33 +00001701 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001702 return 1
1703
1704 # This rev-list syntax means "show all commits not in my branch that
1705 # are in base_branch".
1706 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1707 base_branch]).splitlines()
1708 if upstream_commits:
1709 print ('Base branch "%s" has %d commits '
1710 'not in this branch.' % (base_branch, len(upstream_commits)))
1711 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1712 return 1
1713
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001714 # This is the revision `svn dcommit` will commit on top of.
1715 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1716 '--pretty=format:%H'])
1717
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001718 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001719 # If the base_head is a submodule merge commit, the first parent of the
1720 # base_head should be a git-svn commit, which is what we're interested in.
1721 base_svn_head = base_branch
1722 if base_has_submodules:
1723 base_svn_head += '^1'
1724
1725 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001726 if extra_commits:
1727 print ('This branch has %d additional commits not upstreamed yet.'
1728 % len(extra_commits.splitlines()))
1729 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1730 'before attempting to %s.' % (base_branch, cmd))
1731 return 1
1732
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001733 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001734 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001735 author = None
1736 if options.contributor:
1737 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001738 hook_results = cl.RunHook(
1739 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001740 may_prompt=not options.force,
1741 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001742 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001743 if not hook_results.should_continue():
1744 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001745
1746 if cmd == 'dcommit':
1747 # Check the tree status if the tree status URL is set.
1748 status = GetTreeStatus()
1749 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001750 print('The tree is closed. Please wait for it to reopen. Use '
1751 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001752 return 1
1753 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001754 print('Unable to determine tree status. Please verify manually and '
1755 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001756 else:
1757 breakpad.SendStack(
1758 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001759 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1760 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001761 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001762
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001763 change_desc = ChangeDescription(options.message)
1764 if not change_desc.description and cl.GetIssue():
1765 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001766
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001767 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001768 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001769 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001770 else:
1771 print 'No description set.'
1772 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1773 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001774
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001775 # Keep a separate copy for the commit message, because the commit message
1776 # contains the link to the Rietveld issue, while the Rietveld message contains
1777 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001778 # Keep a separate copy for the commit message.
1779 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001780 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001781
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001782 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001783 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001784 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001785 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001786 commit_desc.append_footer('Patch from %s.' % options.contributor)
1787
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001788 print('Description:')
1789 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001790
1791 branches = [base_branch, cl.GetBranchRef()]
1792 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001793 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001794 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001795
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001796 # We want to squash all this branch's commits into one commit with the proper
1797 # description. We do this by doing a "reset --soft" to the base branch (which
1798 # keeps the working copy the same), then dcommitting that. If origin/master
1799 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1800 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001801 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001802 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1803 # Delete the branches if they exist.
1804 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1805 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1806 result = RunGitWithCode(showref_cmd)
1807 if result[0] == 0:
1808 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001809
1810 # We might be in a directory that's present in this branch but not in the
1811 # trunk. Move up to the top of the tree so that git commands that expect a
1812 # valid CWD won't fail after we check out the merge branch.
1813 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1814 if rel_base_path:
1815 os.chdir(rel_base_path)
1816
1817 # Stuff our change into the merge branch.
1818 # We wrap in a try...finally block so if anything goes wrong,
1819 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001820 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001821 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001822 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1823 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001824 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001825 RunGit(
1826 [
1827 'commit', '--author', options.contributor,
1828 '-m', commit_desc.description,
1829 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001830 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001831 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001832 if base_has_submodules:
1833 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1834 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1835 RunGit(['checkout', CHERRY_PICK_BRANCH])
1836 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001837 if cmd == 'push':
1838 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001839 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001840 retcode, output = RunGitWithCode(
1841 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1842 logging.debug(output)
1843 else:
1844 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001845 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001846 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001847 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001848 finally:
1849 # And then swap back to the original branch and clean up.
1850 RunGit(['checkout', '-q', cl.GetBranch()])
1851 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001852 if base_has_submodules:
1853 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001854
1855 if cl.GetIssue():
1856 if cmd == 'dcommit' and 'Committed r' in output:
1857 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1858 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001859 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1860 for l in output.splitlines(False))
1861 match = filter(None, match)
1862 if len(match) != 1:
1863 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1864 output)
1865 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001866 else:
1867 return 1
1868 viewvc_url = settings.GetViewVCUrl()
1869 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001870 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001871 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001872 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001873 print ('Closing issue '
1874 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001875 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001876 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001877 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001878 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001879 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001880 if options.bypass_hooks:
1881 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
1882 else:
1883 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001884 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001885 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001886
1887 if retcode == 0:
1888 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1889 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001890 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001891
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001892 return 0
1893
1894
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001895@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001896def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001897 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001898 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001899 message = """This doesn't appear to be an SVN repository.
1900If your project has a git mirror with an upstream SVN master, you probably need
1901to run 'git svn init', see your project's git mirror documentation.
1902If your project has a true writeable upstream repository, you probably want
1903to run 'git cl push' instead.
1904Choose wisely, if you get this wrong, your commit might appear to succeed but
1905will instead be silently ignored."""
1906 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001907 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001908 return SendUpstream(parser, args, 'dcommit')
1909
1910
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001911@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001912def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001913 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001914 if settings.GetIsGitSvn():
1915 print('This appears to be an SVN repository.')
1916 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001917 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001918 return SendUpstream(parser, args, 'push')
1919
1920
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001921@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001922def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00001923 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001924 parser.add_option('-b', dest='newbranch',
1925 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001926 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001927 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001928 parser.add_option('-d', '--directory', action='store', metavar='DIR',
1929 help='Change to the directory DIR immediately, '
1930 'before doing anything else.')
1931 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001932 help='failed patches spew .rej files rather than '
1933 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001934 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1935 help="don't commit after patch applies")
1936 (options, args) = parser.parse_args(args)
1937 if len(args) != 1:
1938 parser.print_help()
1939 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001940 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001941
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001942 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001943 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001944
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001945 if options.newbranch:
1946 if options.force:
1947 RunGit(['branch', '-D', options.newbranch],
1948 stderr=subprocess2.PIPE, error_ok=True)
1949 RunGit(['checkout', '-b', options.newbranch,
1950 Changelist().GetUpstreamBranch()])
1951
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001952 return PatchIssue(issue_arg, options.reject, options.nocommit,
1953 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001954
1955
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001956def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001957 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001958 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001959 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001960 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001961 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001962 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001963 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001964 # Assume it's a URL to the patch. Default to https.
1965 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001966 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001967 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001968 DieWithError('Must pass an issue ID or full URL for '
1969 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001970 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001971 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001972 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001973
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001974 # Switch up to the top-level directory, if necessary, in preparation for
1975 # applying the patch.
1976 top = RunGit(['rev-parse', '--show-cdup']).strip()
1977 if top:
1978 os.chdir(top)
1979
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001980 # Git patches have a/ at the beginning of source paths. We strip that out
1981 # with a sed script rather than the -p flag to patch so we can feed either
1982 # Git or svn-style patches into the same apply command.
1983 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001984 try:
1985 patch_data = subprocess2.check_output(
1986 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1987 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001988 DieWithError('Git patch mungling failed.')
1989 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001990 env = os.environ.copy()
1991 # 'cat' is a magical git string that disables pagers on all platforms.
1992 env['GIT_PAGER'] = 'cat'
1993
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001994 # We use "git apply" to apply the patch instead of "patch" so that we can
1995 # pick up file adds.
1996 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001997 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001998 if directory:
1999 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002000 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002001 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002002 elif IsGitVersionAtLeast('1.7.12'):
2003 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002004 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002005 subprocess2.check_call(cmd, env=env,
2006 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002007 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002008 DieWithError('Failed to apply the patch')
2009
2010 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002011 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002012 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2013 cl = Changelist()
2014 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002015 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002016 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002017 else:
2018 print "Patch applied to index."
2019 return 0
2020
2021
2022def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002023 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002024 # Provide a wrapper for git svn rebase to help avoid accidental
2025 # git svn dcommit.
2026 # It's the only command that doesn't use parser at all since we just defer
2027 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002028 env = os.environ.copy()
2029 # 'cat' is a magical git string that disables pagers on all platforms.
2030 env['GIT_PAGER'] = 'cat'
2031
2032 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002033
2034
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002035def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002036 """Fetches the tree status and returns either 'open', 'closed',
2037 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002038 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002039 if url:
2040 status = urllib2.urlopen(url).read().lower()
2041 if status.find('closed') != -1 or status == '0':
2042 return 'closed'
2043 elif status.find('open') != -1 or status == '1':
2044 return 'open'
2045 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002046 return 'unset'
2047
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002048
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002049def GetTreeStatusReason():
2050 """Fetches the tree status from a json url and returns the message
2051 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002052 url = settings.GetTreeStatusUrl()
2053 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002054 connection = urllib2.urlopen(json_url)
2055 status = json.loads(connection.read())
2056 connection.close()
2057 return status['message']
2058
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002059
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002060def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002061 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002062 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002063 status = GetTreeStatus()
2064 if 'unset' == status:
2065 print 'You must configure your tree status URL by running "git cl config".'
2066 return 2
2067
2068 print "The tree is %s" % status
2069 print
2070 print GetTreeStatusReason()
2071 if status != 'open':
2072 return 1
2073 return 0
2074
2075
maruel@chromium.org15192402012-09-06 12:38:29 +00002076def CMDtry(parser, args):
2077 """Triggers a try job through Rietveld."""
2078 group = optparse.OptionGroup(parser, "Try job options")
2079 group.add_option(
2080 "-b", "--bot", action="append",
2081 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2082 "times to specify multiple builders. ex: "
2083 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2084 "the try server waterfall for the builders name and the tests "
2085 "available. Can also be used to specify gtest_filter, e.g. "
2086 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2087 group.add_option(
2088 "-r", "--revision",
2089 help="Revision to use for the try job; default: the "
2090 "revision will be determined by the try server; see "
2091 "its waterfall for more info")
2092 group.add_option(
2093 "-c", "--clobber", action="store_true", default=False,
2094 help="Force a clobber before building; e.g. don't do an "
2095 "incremental build")
2096 group.add_option(
2097 "--project",
2098 help="Override which project to use. Projects are defined "
2099 "server-side to define what default bot set to use")
2100 group.add_option(
2101 "-t", "--testfilter", action="append", default=[],
2102 help=("Apply a testfilter to all the selected builders. Unless the "
2103 "builders configurations are similar, use multiple "
2104 "--bot <builder>:<test> arguments."))
2105 group.add_option(
2106 "-n", "--name", help="Try job name; default to current branch name")
2107 parser.add_option_group(group)
2108 options, args = parser.parse_args(args)
2109
2110 if args:
2111 parser.error('Unknown arguments: %s' % args)
2112
2113 cl = Changelist()
2114 if not cl.GetIssue():
2115 parser.error('Need to upload first')
2116
2117 if not options.name:
2118 options.name = cl.GetBranch()
2119
2120 # Process --bot and --testfilter.
2121 if not options.bot:
2122 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002123 change = cl.GetChange(
2124 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
2125 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002126 options.bot = presubmit_support.DoGetTrySlaves(
2127 change,
2128 change.LocalPaths(),
2129 settings.GetRoot(),
2130 None,
2131 None,
2132 options.verbose,
2133 sys.stdout)
2134 if not options.bot:
2135 parser.error('No default try builder to try, use --bot')
2136
2137 builders_and_tests = {}
stip@chromium.org43064fd2013-12-18 20:07:44 +00002138 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2139 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2140
2141 for bot in old_style:
maruel@chromium.org15192402012-09-06 12:38:29 +00002142 if ':' in bot:
2143 builder, tests = bot.split(':', 1)
2144 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2145 elif ',' in bot:
2146 parser.error('Specify one bot per --bot flag')
2147 else:
2148 builders_and_tests.setdefault(bot, []).append('defaulttests')
2149
stip@chromium.org43064fd2013-12-18 20:07:44 +00002150 for bot, tests in new_style:
2151 builders_and_tests.setdefault(bot, []).extend(tests)
2152
maruel@chromium.org15192402012-09-06 12:38:29 +00002153 if options.testfilter:
2154 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2155 builders_and_tests = dict(
2156 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2157 if t != ['compile'])
2158
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002159 if any('triggered' in b for b in builders_and_tests):
2160 print >> sys.stderr, (
2161 'ERROR You are trying to send a job to a triggered bot. This type of'
2162 ' bot requires an\ninitial job from a parent (usually a builder). '
2163 'Instead send your job to the parent.\n'
2164 'Bot list: %s' % builders_and_tests)
2165 return 1
2166
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002167 patchset = cl.GetMostRecentPatchset()
2168 if patchset and patchset != cl.GetPatchset():
2169 print(
2170 '\nWARNING Mismatch between local config and server. Did a previous '
2171 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2172 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002173 try:
2174 cl.RpcServer().trigger_try_jobs(
2175 cl.GetIssue(), patchset, options.name, options.clobber,
2176 options.revision, builders_and_tests)
2177 except urllib2.HTTPError, e:
2178 if e.code == 404:
2179 print('404 from rietveld; '
2180 'did you mean to use "git try" instead of "git cl try"?')
2181 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002182 print('Tried jobs on:')
2183 length = max(len(builder) for builder in builders_and_tests)
2184 for builder in sorted(builders_and_tests):
2185 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002186 return 0
2187
2188
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002189@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002190def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002191 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002192 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002193 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002194 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002195
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002196 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002197 if args:
2198 # One arg means set upstream branch.
2199 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2200 cl = Changelist()
2201 print "Upstream branch set to " + cl.GetUpstreamBranch()
2202 else:
2203 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002204 return 0
2205
2206
thestig@chromium.org00858c82013-12-02 23:08:03 +00002207def CMDweb(parser, args):
2208 """Opens the current CL in the web browser."""
2209 _, args = parser.parse_args(args)
2210 if args:
2211 parser.error('Unrecognized args: %s' % ' '.join(args))
2212
2213 issue_url = Changelist().GetIssueURL()
2214 if not issue_url:
2215 print >> sys.stderr, 'ERROR No issue to open'
2216 return 1
2217
2218 webbrowser.open(issue_url)
2219 return 0
2220
2221
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002222def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002223 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002224 _, args = parser.parse_args(args)
2225 if args:
2226 parser.error('Unrecognized args: %s' % ' '.join(args))
2227 cl = Changelist()
2228 cl.SetFlag('commit', '1')
2229 return 0
2230
2231
groby@chromium.org411034a2013-02-26 15:12:01 +00002232def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002233 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002234 _, args = parser.parse_args(args)
2235 if args:
2236 parser.error('Unrecognized args: %s' % ' '.join(args))
2237 cl = Changelist()
2238 # Ensure there actually is an issue to close.
2239 cl.GetDescription()
2240 cl.CloseIssue()
2241 return 0
2242
2243
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002244def CMDdiff(parser, args):
2245 """shows differences between local tree and last upload."""
2246 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002247 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002248 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002249 if not issue:
2250 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002251 TMP_BRANCH = 'git-cl-diff'
2252 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2253
2254 # Create a new branch based on the merge-base
2255 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2256 try:
2257 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002258 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002259 if rtn != 0:
2260 return rtn
2261
2262 # Switch back to starting brand and diff against the temporary
2263 # branch containing the latest rietveld patch.
2264 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2265 finally:
2266 RunGit(['checkout', '-q', branch])
2267 RunGit(['branch', '-D', TMP_BRANCH])
2268
2269 return 0
2270
2271
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002272def CMDowners(parser, args):
2273 """interactively find the owners for reviewing"""
2274 parser.add_option(
2275 '--no-color',
2276 action='store_true',
2277 help='Use this option to disable color output')
2278 options, args = parser.parse_args(args)
2279
2280 author = RunGit(['config', 'user.email']).strip() or None
2281
2282 cl = Changelist()
2283
2284 if args:
2285 if len(args) > 1:
2286 parser.error('Unknown args')
2287 base_branch = args[0]
2288 else:
2289 # Default to diffing against the common ancestor of the upstream branch.
2290 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2291
2292 change = cl.GetChange(base_branch, None)
2293 return owners_finder.OwnersFinder(
2294 [f.LocalPath() for f in
2295 cl.GetChange(base_branch, None).AffectedFiles()],
2296 change.RepositoryRoot(), author,
2297 fopen=file, os_path=os.path, glob=glob.glob,
2298 disable_color=options.no_color).run()
2299
2300
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002301def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002302 """Runs clang-format on the diff."""
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002303 CLANG_EXTS = ['.cc', '.cpp', '.h']
2304 parser.add_option('--full', action='store_true', default=False)
2305 opts, args = parser.parse_args(args)
2306 if args:
2307 parser.error('Unrecognized args: %s' % ' '.join(args))
2308
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002309 # git diff generates paths against the root of the repository. Change
2310 # to that directory so clang-format can find files even within subdirs.
2311 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
2312 if rel_base_path:
2313 os.chdir(rel_base_path)
2314
digit@chromium.org29e47272013-05-17 17:01:46 +00002315 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002316 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002317 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002318 # Only list the names of modified files.
2319 diff_cmd.append('--name-only')
2320 else:
2321 # Only generate context-less patches.
2322 diff_cmd.append('-U0')
2323
2324 # Grab the merge-base commit, i.e. the upstream commit of the current
2325 # branch when it was created or the last time it was rebased. This is
2326 # to cover the case where the user may have called "git fetch origin",
2327 # moving the origin branch to a newer commit, but hasn't rebased yet.
2328 upstream_commit = None
2329 cl = Changelist()
2330 upstream_branch = cl.GetUpstreamBranch()
2331 if upstream_branch:
2332 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2333 upstream_commit = upstream_commit.strip()
2334
2335 if not upstream_commit:
2336 DieWithError('Could not find base commit for this branch. '
2337 'Are you in detached state?')
2338
2339 diff_cmd.append(upstream_commit)
2340
2341 # Handle source file filtering.
2342 diff_cmd.append('--')
2343 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2344 diff_output = RunGit(diff_cmd)
2345
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002346 top_dir = os.path.normpath(
2347 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2348
2349 # Locate the clang-format binary in the checkout
2350 try:
2351 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2352 except clang_format.NotFoundError, e:
2353 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002354
digit@chromium.org29e47272013-05-17 17:01:46 +00002355 if opts.full:
2356 # diff_output is a list of files to send to clang-format.
2357 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002358 if not files:
2359 print "Nothing to format."
2360 return 0
nick@chromium.org0680c692014-01-18 01:08:21 +00002361 RunCommand([clang_format_tool, '-i'] + files,
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002362 cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002363 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002364 env = os.environ.copy()
2365 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002366 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002367 try:
2368 script = clang_format.FindClangFormatScriptInChromiumTree(
2369 'clang-format-diff.py')
2370 except clang_format.NotFoundError, e:
2371 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002372
nick@chromium.org0680c692014-01-18 01:08:21 +00002373 cmd = [sys.executable, script, '-p0', '-i']
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002374
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002375 RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002376
2377 return 0
2378
2379
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002380class OptionParser(optparse.OptionParser):
2381 """Creates the option parse and add --verbose support."""
2382 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002383 optparse.OptionParser.__init__(
2384 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002385 self.add_option(
2386 '-v', '--verbose', action='count', default=0,
2387 help='Use 2 times for more debugging info')
2388
2389 def parse_args(self, args=None, values=None):
2390 options, args = optparse.OptionParser.parse_args(self, args, values)
2391 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2392 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2393 return options, args
2394
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002395
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002396def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002397 if sys.hexversion < 0x02060000:
2398 print >> sys.stderr, (
2399 '\nYour python version %s is unsupported, please upgrade.\n' %
2400 sys.version.split(' ', 1)[0])
2401 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002402
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002403 # Reload settings.
2404 global settings
2405 settings = Settings()
2406
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002407 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002408 dispatcher = subcommand.CommandDispatcher(__name__)
2409 try:
2410 return dispatcher.execute(OptionParser(), argv)
2411 except urllib2.HTTPError, e:
2412 if e.code != 500:
2413 raise
2414 DieWithError(
2415 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2416 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002417
2418
2419if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002420 # These affect sys.stdout so do it outside of main() to simplify mocks in
2421 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002422 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002423 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002424 sys.exit(main(sys.argv[1:]))