blob: 2ccb61b34b4504263081db820173ba17496b7a98 [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
iannucci@chromium.org9e849272014-04-04 00:31:55 +000038import git_common
39import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000040import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000041import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000043import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000044import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000045import watchlists
46
maruel@chromium.org0633fb42013-08-16 20:06:14 +000047__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000048
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000049DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000050POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000051DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000052GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000053CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000054
thestig@chromium.org44202a22014-03-11 19:22:18 +000055# Valid extensions for files we want to lint.
56DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
57DEFAULT_LINT_IGNORE_REGEX = r"$^"
58
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000059# Shortcut since it quickly becomes redundant.
60Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000061
maruel@chromium.orgddd59412011-11-30 14:20:38 +000062# Initialized in main()
63settings = None
64
65
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000066def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000067 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068 sys.exit(1)
69
70
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000071def GetNoGitPagerEnv():
72 env = os.environ.copy()
73 # 'cat' is a magical git string that disables pagers on all platforms.
74 env['GIT_PAGER'] = 'cat'
75 return env
76
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000077
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000078def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000079 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000080 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000081 except subprocess2.CalledProcessError as e:
82 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000083 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000084 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000085 'Command "%s" failed.\n%s' % (
86 ' '.join(args), error_message or e.stdout or ''))
87 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088
89
90def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000091 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000092 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000093
94
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000095def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000096 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000097 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000098 if suppress_stderr:
99 stderr = subprocess2.VOID
100 else:
101 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000102 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000103 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000104 stdout=subprocess2.PIPE,
105 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000106 return code, out[0]
107 except ValueError:
108 # When the subprocess fails, it returns None. That triggers a ValueError
109 # when trying to unpack the return value into (out, code).
110 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000111
112
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000113def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000114 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000115 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000116 return (version.startswith(prefix) and
117 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000118
119
maruel@chromium.org90541732011-04-01 17:54:18 +0000120def ask_for_data(prompt):
121 try:
122 return raw_input(prompt)
123 except KeyboardInterrupt:
124 # Hide the exception.
125 sys.exit(1)
126
127
iannucci@chromium.org79540052012-10-19 23:15:26 +0000128def git_set_branch_value(key, value):
129 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000130 if not branch:
131 return
132
133 cmd = ['config']
134 if isinstance(value, int):
135 cmd.append('--int')
136 git_key = 'branch.%s.%s' % (branch, key)
137 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000138
139
140def git_get_branch_default(key, default):
141 branch = Changelist().GetBranch()
142 if branch:
143 git_key = 'branch.%s.%s' % (branch, key)
144 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
145 try:
146 return int(stdout.strip())
147 except ValueError:
148 pass
149 return default
150
151
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000152def add_git_similarity(parser):
153 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000154 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000155 help='Sets the percentage that a pair of files need to match in order to'
156 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000157 parser.add_option(
158 '--find-copies', action='store_true',
159 help='Allows git to look for copies.')
160 parser.add_option(
161 '--no-find-copies', action='store_false', dest='find_copies',
162 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000163
164 old_parser_args = parser.parse_args
165 def Parse(args):
166 options, args = old_parser_args(args)
167
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000168 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000169 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000170 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000171 print('Note: Saving similarity of %d%% in git config.'
172 % options.similarity)
173 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000174
iannucci@chromium.org79540052012-10-19 23:15:26 +0000175 options.similarity = max(0, min(options.similarity, 100))
176
177 if options.find_copies is None:
178 options.find_copies = bool(
179 git_get_branch_default('git-find-copies', True))
180 else:
181 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000182
183 print('Using %d%% similarity for rename/copy detection. '
184 'Override with --similarity.' % options.similarity)
185
186 return options, args
187 parser.parse_args = Parse
188
189
ukai@chromium.org259e4682012-10-25 07:36:33 +0000190def is_dirty_git_tree(cmd):
191 # Make sure index is up-to-date before running diff-index.
192 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
193 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
194 if dirty:
195 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
196 print 'Uncommitted files: (git diff-index --name-status HEAD)'
197 print dirty[:4096]
198 if len(dirty) > 4096:
199 print '... (run "git diff-index --name-status HEAD" to see full output).'
200 return True
201 return False
202
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000203
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000204def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
205 """Return the corresponding git ref if |base_url| together with |glob_spec|
206 matches the full |url|.
207
208 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
209 """
210 fetch_suburl, as_ref = glob_spec.split(':')
211 if allow_wildcards:
212 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
213 if glob_match:
214 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
215 # "branches/{472,597,648}/src:refs/remotes/svn/*".
216 branch_re = re.escape(base_url)
217 if glob_match.group(1):
218 branch_re += '/' + re.escape(glob_match.group(1))
219 wildcard = glob_match.group(2)
220 if wildcard == '*':
221 branch_re += '([^/]*)'
222 else:
223 # Escape and replace surrounding braces with parentheses and commas
224 # with pipe symbols.
225 wildcard = re.escape(wildcard)
226 wildcard = re.sub('^\\\\{', '(', wildcard)
227 wildcard = re.sub('\\\\,', '|', wildcard)
228 wildcard = re.sub('\\\\}$', ')', wildcard)
229 branch_re += wildcard
230 if glob_match.group(3):
231 branch_re += re.escape(glob_match.group(3))
232 match = re.match(branch_re, url)
233 if match:
234 return re.sub('\*$', match.group(1), as_ref)
235
236 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
237 if fetch_suburl:
238 full_url = base_url + '/' + fetch_suburl
239 else:
240 full_url = base_url
241 if full_url == url:
242 return as_ref
243 return None
244
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000245
iannucci@chromium.org79540052012-10-19 23:15:26 +0000246def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000247 """Prints statistics about the change to the user."""
248 # --no-ext-diff is broken in some versions of Git, so try to work around
249 # this by overriding the environment (but there is still a problem if the
250 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000251 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000252 if 'GIT_EXTERNAL_DIFF' in env:
253 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000254
255 if find_copies:
256 similarity_options = ['--find-copies-harder', '-l100000',
257 '-C%s' % similarity]
258 else:
259 similarity_options = ['-M%s' % similarity]
260
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000261 try:
262 stdout = sys.stdout.fileno()
263 except AttributeError:
264 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000265 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000266 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000267 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000268 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000269
270
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000271class Settings(object):
272 def __init__(self):
273 self.default_server = None
274 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000275 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000276 self.is_git_svn = None
277 self.svn_branch = None
278 self.tree_status_url = None
279 self.viewvc_url = None
280 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000281 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000282 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000283 self.project = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000284 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000285
286 def LazyUpdateIfNeeded(self):
287 """Updates the settings from a codereview.settings file, if available."""
288 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000289 # The only value that actually changes the behavior is
290 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000291 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000292 error_ok=True
293 ).strip().lower()
294
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000295 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000296 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000297 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000298 # set updated to True to avoid infinite calling loop
299 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000300 self.updated = True
301 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000302 self.updated = True
303
304 def GetDefaultServerUrl(self, error_ok=False):
305 if not self.default_server:
306 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000307 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000308 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000309 if error_ok:
310 return self.default_server
311 if not self.default_server:
312 error_message = ('Could not find settings file. You must configure '
313 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000314 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000315 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000316 return self.default_server
317
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000318 @staticmethod
319 def GetRelativeRoot():
320 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000321
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000322 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000323 if self.root is None:
324 self.root = os.path.abspath(self.GetRelativeRoot())
325 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000326
327 def GetIsGitSvn(self):
328 """Return true if this repo looks like it's using git-svn."""
329 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000330 if self.GetPendingRefPrefix():
331 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
332 self.is_git_svn = False
333 else:
334 # If you have any "svn-remote.*" config keys, we think you're using svn.
335 self.is_git_svn = RunGitWithCode(
336 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000337 return self.is_git_svn
338
339 def GetSVNBranch(self):
340 if self.svn_branch is None:
341 if not self.GetIsGitSvn():
342 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
343
344 # Try to figure out which remote branch we're based on.
345 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000346 # 1) iterate through our branch history and find the svn URL.
347 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000348
349 # regexp matching the git-svn line that contains the URL.
350 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
351
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000352 # We don't want to go through all of history, so read a line from the
353 # pipe at a time.
354 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000355 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000356 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
357 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000358 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000359 for line in proc.stdout:
360 match = git_svn_re.match(line)
361 if match:
362 url = match.group(1)
363 proc.stdout.close() # Cut pipe.
364 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000365
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000366 if url:
367 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
368 remotes = RunGit(['config', '--get-regexp',
369 r'^svn-remote\..*\.url']).splitlines()
370 for remote in remotes:
371 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000372 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000373 remote = match.group(1)
374 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000375 rewrite_root = RunGit(
376 ['config', 'svn-remote.%s.rewriteRoot' % remote],
377 error_ok=True).strip()
378 if rewrite_root:
379 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000380 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000381 ['config', 'svn-remote.%s.fetch' % remote],
382 error_ok=True).strip()
383 if fetch_spec:
384 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
385 if self.svn_branch:
386 break
387 branch_spec = RunGit(
388 ['config', 'svn-remote.%s.branches' % remote],
389 error_ok=True).strip()
390 if branch_spec:
391 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
392 if self.svn_branch:
393 break
394 tag_spec = RunGit(
395 ['config', 'svn-remote.%s.tags' % remote],
396 error_ok=True).strip()
397 if tag_spec:
398 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
399 if self.svn_branch:
400 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000401
402 if not self.svn_branch:
403 DieWithError('Can\'t guess svn branch -- try specifying it on the '
404 'command line')
405
406 return self.svn_branch
407
408 def GetTreeStatusUrl(self, error_ok=False):
409 if not self.tree_status_url:
410 error_message = ('You must configure your tree status URL by running '
411 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000412 self.tree_status_url = self._GetRietveldConfig(
413 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000414 return self.tree_status_url
415
416 def GetViewVCUrl(self):
417 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000418 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000419 return self.viewvc_url
420
rmistry@google.com90752582014-01-14 21:04:50 +0000421 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000422 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000423
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000424 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000425 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000426
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000427 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000428 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000429
ukai@chromium.orge8077812012-02-03 03:41:46 +0000430 def GetIsGerrit(self):
431 """Return true if this repo is assosiated with gerrit code review system."""
432 if self.is_gerrit is None:
433 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
434 return self.is_gerrit
435
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000436 def GetGitEditor(self):
437 """Return the editor specified in the git config, or None if none is."""
438 if self.git_editor is None:
439 self.git_editor = self._GetConfig('core.editor', error_ok=True)
440 return self.git_editor or None
441
thestig@chromium.org44202a22014-03-11 19:22:18 +0000442 def GetLintRegex(self):
443 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
444 DEFAULT_LINT_REGEX)
445
446 def GetLintIgnoreRegex(self):
447 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
448 DEFAULT_LINT_IGNORE_REGEX)
449
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000450 def GetProject(self):
451 if not self.project:
452 self.project = self._GetRietveldConfig('project', error_ok=True)
453 return self.project
454
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000455 def GetPendingRefPrefix(self):
456 if not self.pending_ref_prefix:
457 self.pending_ref_prefix = self._GetRietveldConfig(
458 'pending-ref-prefix', error_ok=True)
459 return self.pending_ref_prefix
460
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000461 def _GetRietveldConfig(self, param, **kwargs):
462 return self._GetConfig('rietveld.' + param, **kwargs)
463
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000464 def _GetConfig(self, param, **kwargs):
465 self.LazyUpdateIfNeeded()
466 return RunGit(['config', param], **kwargs).strip()
467
468
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000469def ShortBranchName(branch):
470 """Convert a name like 'refs/heads/foo' to just 'foo'."""
471 return branch.replace('refs/heads/', '')
472
473
474class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000475 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000476 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000477 global settings
478 if not settings:
479 # Happens when git_cl.py is used as a utility library.
480 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000481 settings.GetDefaultServerUrl()
482 self.branchref = branchref
483 if self.branchref:
484 self.branch = ShortBranchName(self.branchref)
485 else:
486 self.branch = None
487 self.rietveld_server = None
488 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000489 self.lookedup_issue = False
490 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000491 self.has_description = False
492 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000493 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000494 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000495 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000496 self.cc = None
497 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000498 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000499 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000500
501 def GetCCList(self):
502 """Return the users cc'd on this CL.
503
504 Return is a string suitable for passing to gcl with the --cc flag.
505 """
506 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000507 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000508 more_cc = ','.join(self.watchers)
509 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
510 return self.cc
511
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000512 def GetCCListWithoutDefault(self):
513 """Return the users cc'd on this CL excluding default ones."""
514 if self.cc is None:
515 self.cc = ','.join(self.watchers)
516 return self.cc
517
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000518 def SetWatchers(self, watchers):
519 """Set the list of email addresses that should be cc'd based on the changed
520 files in this CL.
521 """
522 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000523
524 def GetBranch(self):
525 """Returns the short branch name, e.g. 'master'."""
526 if not self.branch:
527 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
528 self.branch = ShortBranchName(self.branchref)
529 return self.branch
530
531 def GetBranchRef(self):
532 """Returns the full branch name, e.g. 'refs/heads/master'."""
533 self.GetBranch() # Poke the lazy loader.
534 return self.branchref
535
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000536 @staticmethod
537 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000538 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000539 e.g. 'origin', 'refs/heads/master'
540 """
541 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000542 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
543 error_ok=True).strip()
544 if upstream_branch:
545 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
546 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000547 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
548 error_ok=True).strip()
549 if upstream_branch:
550 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000551 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000552 # Fall back on trying a git-svn upstream branch.
553 if settings.GetIsGitSvn():
554 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000555 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000556 # Else, try to guess the origin remote.
557 remote_branches = RunGit(['branch', '-r']).split()
558 if 'origin/master' in remote_branches:
559 # Fall back on origin/master if it exits.
560 remote = 'origin'
561 upstream_branch = 'refs/heads/master'
562 elif 'origin/trunk' in remote_branches:
563 # Fall back on origin/trunk if it exists. Generally a shared
564 # git-svn clone
565 remote = 'origin'
566 upstream_branch = 'refs/heads/trunk'
567 else:
568 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000569Either pass complete "git diff"-style arguments, like
570 git cl upload origin/master
571or verify this branch is set up to track another (via the --track argument to
572"git checkout -b ...").""")
573
574 return remote, upstream_branch
575
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000576 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000577 return git_common.get_or_create_merge_base(self.GetBranch(),
578 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000579
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000580 def GetUpstreamBranch(self):
581 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000582 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000583 if remote is not '.':
584 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
585 self.upstream_branch = upstream_branch
586 return self.upstream_branch
587
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000588 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000589 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000590 remote, branch = None, self.GetBranch()
591 seen_branches = set()
592 while branch not in seen_branches:
593 seen_branches.add(branch)
594 remote, branch = self.FetchUpstreamTuple(branch)
595 branch = ShortBranchName(branch)
596 if remote != '.' or branch.startswith('refs/remotes'):
597 break
598 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000599 remotes = RunGit(['remote'], error_ok=True).split()
600 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000601 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000602 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000603 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000604 logging.warning('Could not determine which remote this change is '
605 'associated with, so defaulting to "%s". This may '
606 'not be what you want. You may prevent this message '
607 'by running "git svn info" as documented here: %s',
608 self._remote,
609 GIT_INSTRUCTIONS_URL)
610 else:
611 logging.warn('Could not determine which remote this change is '
612 'associated with. You may prevent this message by '
613 'running "git svn info" as documented here: %s',
614 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000615 branch = 'HEAD'
616 if branch.startswith('refs/remotes'):
617 self._remote = (remote, branch)
618 else:
619 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000620 return self._remote
621
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000622 def GitSanityChecks(self, upstream_git_obj):
623 """Checks git repo status and ensures diff is from local commits."""
624
625 # Verify the commit we're diffing against is in our current branch.
626 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
627 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
628 if upstream_sha != common_ancestor:
629 print >> sys.stderr, (
630 'ERROR: %s is not in the current branch. You may need to rebase '
631 'your tracking branch' % upstream_sha)
632 return False
633
634 # List the commits inside the diff, and verify they are all local.
635 commits_in_diff = RunGit(
636 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
637 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
638 remote_branch = remote_branch.strip()
639 if code != 0:
640 _, remote_branch = self.GetRemoteBranch()
641
642 commits_in_remote = RunGit(
643 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
644
645 common_commits = set(commits_in_diff) & set(commits_in_remote)
646 if common_commits:
647 print >> sys.stderr, (
648 'ERROR: Your diff contains %d commits already in %s.\n'
649 'Run "git log --oneline %s..HEAD" to get a list of commits in '
650 'the diff. If you are using a custom git flow, you can override'
651 ' the reference used for this check with "git config '
652 'gitcl.remotebranch <git-ref>".' % (
653 len(common_commits), remote_branch, upstream_git_obj))
654 return False
655 return True
656
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000657 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000658 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000659
660 Returns None if it is not set.
661 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000662 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
663 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000664
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000665 def GetRemoteUrl(self):
666 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
667
668 Returns None if there is no remote.
669 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000670 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000671 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
672
673 # If URL is pointing to a local directory, it is probably a git cache.
674 if os.path.isdir(url):
675 url = RunGit(['config', 'remote.%s.url' % remote],
676 error_ok=True,
677 cwd=url).strip()
678 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000679
680 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000681 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000682 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000683 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000684 self.issue = int(issue) or None if issue else None
685 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000686 return self.issue
687
688 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000689 if not self.rietveld_server:
690 # If we're on a branch then get the server potentially associated
691 # with that branch.
692 if self.GetIssue():
693 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
694 ['config', self._RietveldServer()], error_ok=True).strip())
695 if not self.rietveld_server:
696 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000697 return self.rietveld_server
698
699 def GetIssueURL(self):
700 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000701 if not self.GetIssue():
702 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000703 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
704
705 def GetDescription(self, pretty=False):
706 if not self.has_description:
707 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000708 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000709 try:
710 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000711 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000712 if e.code == 404:
713 DieWithError(
714 ('\nWhile fetching the description for issue %d, received a '
715 '404 (not found)\n'
716 'error. It is likely that you deleted this '
717 'issue on the server. If this is the\n'
718 'case, please run\n\n'
719 ' git cl issue 0\n\n'
720 'to clear the association with the deleted issue. Then run '
721 'this command again.') % issue)
722 else:
723 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000724 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000725 except urllib2.URLError as e:
726 print >> sys.stderr, (
727 'Warning: Failed to retrieve CL description due to network '
728 'failure.')
729 self.description = ''
730
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000731 self.has_description = True
732 if pretty:
733 wrapper = textwrap.TextWrapper()
734 wrapper.initial_indent = wrapper.subsequent_indent = ' '
735 return wrapper.fill(self.description)
736 return self.description
737
738 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000739 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000740 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000741 patchset = RunGit(['config', self._PatchsetSetting()],
742 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000743 self.patchset = int(patchset) or None if patchset else None
744 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000745 return self.patchset
746
747 def SetPatchset(self, patchset):
748 """Set this branch's patchset. If patchset=0, clears the patchset."""
749 if patchset:
750 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000751 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000752 else:
753 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000754 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000755 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000756
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000757 def GetMostRecentPatchset(self):
758 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000759
760 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000761 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000762 '/download/issue%s_%s.diff' % (issue, patchset))
763
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000764 def GetIssueProperties(self):
765 if self._props is None:
766 issue = self.GetIssue()
767 if not issue:
768 self._props = {}
769 else:
770 self._props = self.RpcServer().get_issue_properties(issue, True)
771 return self._props
772
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000773 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000774 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000775
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000776 def SetIssue(self, issue):
777 """Set this branch's issue. If issue=0, clears the issue."""
778 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000779 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000780 RunGit(['config', self._IssueSetting(), str(issue)])
781 if self.rietveld_server:
782 RunGit(['config', self._RietveldServer(), self.rietveld_server])
783 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000784 current_issue = self.GetIssue()
785 if current_issue:
786 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000787 self.issue = None
788 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000789
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000790 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000791 if not self.GitSanityChecks(upstream_branch):
792 DieWithError('\nGit sanity check failure')
793
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000794 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000795 if not root:
796 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000797 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000798
799 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000800 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000801 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000802 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000803 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000804 except subprocess2.CalledProcessError:
805 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000806 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000807 'This branch probably doesn\'t exist anymore. To reset the\n'
808 'tracking branch, please run\n'
809 ' git branch --set-upstream %s trunk\n'
810 'replacing trunk with origin/master or the relevant branch') %
811 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000812
maruel@chromium.org52424302012-08-29 15:14:30 +0000813 issue = self.GetIssue()
814 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000815 if issue:
816 description = self.GetDescription()
817 else:
818 # If the change was never uploaded, use the log messages of all commits
819 # up to the branch point, as git cl upload will prefill the description
820 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000821 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
822 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000823
824 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000825 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000826 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000827 name,
828 description,
829 absroot,
830 files,
831 issue,
832 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000833 author,
834 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000835
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000836 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000837 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000838
839 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000840 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000841 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000842 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000843 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000844 except presubmit_support.PresubmitFailure, e:
845 DieWithError(
846 ('%s\nMaybe your depot_tools is out of date?\n'
847 'If all fails, contact maruel@') % e)
848
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000849 def UpdateDescription(self, description):
850 self.description = description
851 return self.RpcServer().update_description(
852 self.GetIssue(), self.description)
853
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000854 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000855 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000856 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000857
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000858 def SetFlag(self, flag, value):
859 """Patchset must match."""
860 if not self.GetPatchset():
861 DieWithError('The patchset needs to match. Send another patchset.')
862 try:
863 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000864 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000865 except urllib2.HTTPError, e:
866 if e.code == 404:
867 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
868 if e.code == 403:
869 DieWithError(
870 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
871 'match?') % (self.GetIssue(), self.GetPatchset()))
872 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000873
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000874 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875 """Returns an upload.RpcServer() to access this review's rietveld instance.
876 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000877 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000878 self._rpc_server = rietveld.CachingRietveld(
879 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000880 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000881
882 def _IssueSetting(self):
883 """Return the git setting that stores this change's issue."""
884 return 'branch.%s.rietveldissue' % self.GetBranch()
885
886 def _PatchsetSetting(self):
887 """Return the git setting that stores this change's most recent patchset."""
888 return 'branch.%s.rietveldpatchset' % self.GetBranch()
889
890 def _RietveldServer(self):
891 """Returns the git setting that stores this change's rietveld server."""
892 return 'branch.%s.rietveldserver' % self.GetBranch()
893
894
895def GetCodereviewSettingsInteractively():
896 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000897 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000898 server = settings.GetDefaultServerUrl(error_ok=True)
899 prompt = 'Rietveld server (host[:port])'
900 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000901 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000902 if not server and not newserver:
903 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000904 if newserver:
905 newserver = gclient_utils.UpgradeToHttps(newserver)
906 if newserver != server:
907 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000908
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000909 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000910 prompt = caption
911 if initial:
912 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000913 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000914 if new_val == 'x':
915 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000916 elif new_val:
917 if is_url:
918 new_val = gclient_utils.UpgradeToHttps(new_val)
919 if new_val != initial:
920 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000921
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000922 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000923 SetProperty(settings.GetDefaultPrivateFlag(),
924 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000925 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000926 'tree-status-url', False)
927 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000928 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000929
930 # TODO: configure a default branch to diff against, rather than this
931 # svn-based hackery.
932
933
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000934class ChangeDescription(object):
935 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000936 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000937 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000938
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000939 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000940 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000941
agable@chromium.org42c20792013-09-12 17:34:49 +0000942 @property # www.logilab.org/ticket/89786
943 def description(self): # pylint: disable=E0202
944 return '\n'.join(self._description_lines)
945
946 def set_description(self, desc):
947 if isinstance(desc, basestring):
948 lines = desc.splitlines()
949 else:
950 lines = [line.rstrip() for line in desc]
951 while lines and not lines[0]:
952 lines.pop(0)
953 while lines and not lines[-1]:
954 lines.pop(-1)
955 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000956
957 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000958 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000959 assert isinstance(reviewers, list), reviewers
960 if not reviewers:
961 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000962 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000963
agable@chromium.org42c20792013-09-12 17:34:49 +0000964 # Get the set of R= and TBR= lines and remove them from the desciption.
965 regexp = re.compile(self.R_LINE)
966 matches = [regexp.match(line) for line in self._description_lines]
967 new_desc = [l for i, l in enumerate(self._description_lines)
968 if not matches[i]]
969 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000970
agable@chromium.org42c20792013-09-12 17:34:49 +0000971 # Construct new unified R= and TBR= lines.
972 r_names = []
973 tbr_names = []
974 for match in matches:
975 if not match:
976 continue
977 people = cleanup_list([match.group(2).strip()])
978 if match.group(1) == 'TBR':
979 tbr_names.extend(people)
980 else:
981 r_names.extend(people)
982 for name in r_names:
983 if name not in reviewers:
984 reviewers.append(name)
985 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
986 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
987
988 # Put the new lines in the description where the old first R= line was.
989 line_loc = next((i for i, match in enumerate(matches) if match), -1)
990 if 0 <= line_loc < len(self._description_lines):
991 if new_tbr_line:
992 self._description_lines.insert(line_loc, new_tbr_line)
993 if new_r_line:
994 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000995 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000996 if new_r_line:
997 self.append_footer(new_r_line)
998 if new_tbr_line:
999 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001000
1001 def prompt(self):
1002 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001003 self.set_description([
1004 '# Enter a description of the change.',
1005 '# This will be displayed on the codereview site.',
1006 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001007 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001008 '--------------------',
1009 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001010
agable@chromium.org42c20792013-09-12 17:34:49 +00001011 regexp = re.compile(self.BUG_LINE)
1012 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001013 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001014 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001015 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001016 if not content:
1017 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001018 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001019
1020 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001021 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1022 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001023 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001024 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001025
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001026 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001027 if self._description_lines:
1028 # Add an empty line if either the last line or the new line isn't a tag.
1029 last_line = self._description_lines[-1]
1030 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1031 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1032 self._description_lines.append('')
1033 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001034
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001035 def get_reviewers(self):
1036 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001037 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1038 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001039 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001040
1041
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001042def get_approving_reviewers(props):
1043 """Retrieves the reviewers that approved a CL from the issue properties with
1044 messages.
1045
1046 Note that the list may contain reviewers that are not committer, thus are not
1047 considered by the CQ.
1048 """
1049 return sorted(
1050 set(
1051 message['sender']
1052 for message in props['messages']
1053 if message['approval'] and message['sender'] in props['reviewers']
1054 )
1055 )
1056
1057
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001058def FindCodereviewSettingsFile(filename='codereview.settings'):
1059 """Finds the given file starting in the cwd and going up.
1060
1061 Only looks up to the top of the repository unless an
1062 'inherit-review-settings-ok' file exists in the root of the repository.
1063 """
1064 inherit_ok_file = 'inherit-review-settings-ok'
1065 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001066 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001067 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1068 root = '/'
1069 while True:
1070 if filename in os.listdir(cwd):
1071 if os.path.isfile(os.path.join(cwd, filename)):
1072 return open(os.path.join(cwd, filename))
1073 if cwd == root:
1074 break
1075 cwd = os.path.dirname(cwd)
1076
1077
1078def LoadCodereviewSettingsFromFile(fileobj):
1079 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001080 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001082 def SetProperty(name, setting, unset_error_ok=False):
1083 fullname = 'rietveld.' + name
1084 if setting in keyvals:
1085 RunGit(['config', fullname, keyvals[setting]])
1086 else:
1087 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1088
1089 SetProperty('server', 'CODE_REVIEW_SERVER')
1090 # Only server setting is required. Other settings can be absent.
1091 # In that case, we ignore errors raised during option deletion attempt.
1092 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001093 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001094 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1095 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001096 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001097 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
1098 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001099 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001100 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001101
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001102 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001103 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001104
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001105 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1106 #should be of the form
1107 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1108 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1109 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1110 keyvals['ORIGIN_URL_CONFIG']])
1111
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001112
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001113def urlretrieve(source, destination):
1114 """urllib is broken for SSL connections via a proxy therefore we
1115 can't use urllib.urlretrieve()."""
1116 with open(destination, 'w') as f:
1117 f.write(urllib2.urlopen(source).read())
1118
1119
ukai@chromium.org712d6102013-11-27 00:52:58 +00001120def hasSheBang(fname):
1121 """Checks fname is a #! script."""
1122 with open(fname) as f:
1123 return f.read(2).startswith('#!')
1124
1125
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001126def DownloadHooks(force):
1127 """downloads hooks
1128
1129 Args:
1130 force: True to update hooks. False to install hooks if not present.
1131 """
1132 if not settings.GetIsGerrit():
1133 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001134 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001135 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1136 if not os.access(dst, os.X_OK):
1137 if os.path.exists(dst):
1138 if not force:
1139 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001140 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001141 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001142 if not hasSheBang(dst):
1143 DieWithError('Not a script: %s\n'
1144 'You need to download from\n%s\n'
1145 'into .git/hooks/commit-msg and '
1146 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001147 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1148 except Exception:
1149 if os.path.exists(dst):
1150 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001151 DieWithError('\nFailed to download hooks.\n'
1152 'You need to download from\n%s\n'
1153 'into .git/hooks/commit-msg and '
1154 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001155
1156
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001157@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001158def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001159 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001161 parser.add_option('--activate-update', action='store_true',
1162 help='activate auto-updating [rietveld] section in '
1163 '.git/config')
1164 parser.add_option('--deactivate-update', action='store_true',
1165 help='deactivate auto-updating [rietveld] section in '
1166 '.git/config')
1167 options, args = parser.parse_args(args)
1168
1169 if options.deactivate_update:
1170 RunGit(['config', 'rietveld.autoupdate', 'false'])
1171 return
1172
1173 if options.activate_update:
1174 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1175 return
1176
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001177 if len(args) == 0:
1178 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001179 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001180 return 0
1181
1182 url = args[0]
1183 if not url.endswith('codereview.settings'):
1184 url = os.path.join(url, 'codereview.settings')
1185
1186 # Load code review settings and download hooks (if available).
1187 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001188 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001189 return 0
1190
1191
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001192def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001193 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001194 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1195 branch = ShortBranchName(branchref)
1196 _, args = parser.parse_args(args)
1197 if not args:
1198 print("Current base-url:")
1199 return RunGit(['config', 'branch.%s.base-url' % branch],
1200 error_ok=False).strip()
1201 else:
1202 print("Setting base-url to %s" % args[0])
1203 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1204 error_ok=False).strip()
1205
1206
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001208 """Show status of changelists.
1209
1210 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001211 - Red not sent for review or broken
1212 - Blue waiting for review
1213 - Yellow waiting for you to reply to review
1214 - Green LGTM'ed
1215 - Magenta in the commit queue
1216 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001217
1218 Also see 'git cl comments'.
1219 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001220 parser.add_option('--field',
1221 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001222 parser.add_option('-f', '--fast', action='store_true',
1223 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001224 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001225 if args:
1226 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001228 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001229 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001230 if options.field.startswith('desc'):
1231 print cl.GetDescription()
1232 elif options.field == 'id':
1233 issueid = cl.GetIssue()
1234 if issueid:
1235 print issueid
1236 elif options.field == 'patch':
1237 patchset = cl.GetPatchset()
1238 if patchset:
1239 print patchset
1240 elif options.field == 'url':
1241 url = cl.GetIssueURL()
1242 if url:
1243 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001244 return 0
1245
1246 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1247 if not branches:
1248 print('No local branch found.')
1249 return 0
1250
1251 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001252 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001253 alignment = max(5, max(len(b) for b in branches))
1254 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001255 # Adhoc thread pool to request data concurrently.
1256 output = Queue.Queue()
1257
1258 # Silence upload.py otherwise it becomes unweldly.
1259 upload.verbosity = 0
1260
1261 if not options.fast:
1262 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001263 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001264 c = Changelist(branchref=b)
1265 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001266 props = {}
1267 r = None
1268 if i:
1269 try:
1270 props = c.GetIssueProperties()
1271 r = c.GetApprovingReviewers() if i else None
1272 except urllib2.HTTPError:
1273 # The issue probably doesn't exist anymore.
1274 i += ' (broken)'
1275
1276 msgs = props.get('messages') or []
1277
1278 if not i:
1279 color = Fore.WHITE
1280 elif props.get('closed'):
1281 # Issue is closed.
1282 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001283 elif props.get('commit'):
1284 # Issue is in the commit queue.
1285 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001286 elif r:
1287 # Was LGTM'ed.
1288 color = Fore.GREEN
1289 elif not msgs:
1290 # No message was sent.
1291 color = Fore.RED
1292 elif msgs[-1]['sender'] != props.get('owner_email'):
1293 color = Fore.YELLOW
1294 else:
1295 color = Fore.BLUE
1296 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001297
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001298 # Process one branch synchronously to work through authentication, then
1299 # spawn threads to process all the other branches in parallel.
1300 if branches:
1301 fetch(branches[0])
1302 threads = [
1303 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001304 for t in threads:
1305 t.daemon = True
1306 t.start()
1307 else:
1308 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1309 for b in branches:
1310 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001311 url = c.GetIssueURL()
1312 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001313
1314 tmp = {}
1315 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001316 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001317 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001318 b, i, color = output.get()
1319 tmp[b] = (i, color)
1320 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001321 reset = Fore.RESET
1322 if not sys.stdout.isatty():
1323 color = ''
1324 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001325 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001326 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001327
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001328 cl = Changelist()
1329 print
1330 print 'Current branch:',
1331 if not cl.GetIssue():
1332 print 'no issue assigned.'
1333 return 0
1334 print cl.GetBranch()
1335 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001336 if not options.fast:
1337 print 'Issue description:'
1338 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339 return 0
1340
1341
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001342def colorize_CMDstatus_doc():
1343 """To be called once in main() to add colors to git cl status help."""
1344 colors = [i for i in dir(Fore) if i[0].isupper()]
1345
1346 def colorize_line(line):
1347 for color in colors:
1348 if color in line.upper():
1349 # Extract whitespaces first and the leading '-'.
1350 indent = len(line) - len(line.lstrip(' ')) + 1
1351 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1352 return line
1353
1354 lines = CMDstatus.__doc__.splitlines()
1355 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1356
1357
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001358@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001359def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001360 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001361
1362 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001363 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001364 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001365
1366 cl = Changelist()
1367 if len(args) > 0:
1368 try:
1369 issue = int(args[0])
1370 except ValueError:
1371 DieWithError('Pass a number to set the issue or none to list it.\n'
1372 'Maybe you want to run git cl status?')
1373 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001374 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001375 return 0
1376
1377
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001378def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001379 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001380 (_, args) = parser.parse_args(args)
1381 if args:
1382 parser.error('Unsupported argument: %s' % args)
1383
1384 cl = Changelist()
1385 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001386 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001387 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001388 if message['disapproval']:
1389 color = Fore.RED
1390 elif message['approval']:
1391 color = Fore.GREEN
1392 elif message['sender'] == data['owner_email']:
1393 color = Fore.MAGENTA
1394 else:
1395 color = Fore.BLUE
1396 print '\n%s%s %s%s' % (
1397 color, message['date'].split('.', 1)[0], message['sender'],
1398 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001399 if message['text'].strip():
1400 print '\n'.join(' ' + l for l in message['text'].splitlines())
1401 return 0
1402
1403
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001404def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001405 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001406 cl = Changelist()
1407 if not cl.GetIssue():
1408 DieWithError('This branch has no associated changelist.')
1409 description = ChangeDescription(cl.GetDescription())
1410 description.prompt()
1411 cl.UpdateDescription(description.description)
1412 return 0
1413
1414
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415def CreateDescriptionFromLog(args):
1416 """Pulls out the commit log to use as a base for the CL description."""
1417 log_args = []
1418 if len(args) == 1 and not args[0].endswith('.'):
1419 log_args = [args[0] + '..']
1420 elif len(args) == 1 and args[0].endswith('...'):
1421 log_args = [args[0][:-1]]
1422 elif len(args) == 2:
1423 log_args = [args[0] + '..' + args[1]]
1424 else:
1425 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001426 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427
1428
thestig@chromium.org44202a22014-03-11 19:22:18 +00001429def CMDlint(parser, args):
1430 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001431 parser.add_option('--filter', action='append', metavar='-x,+y',
1432 help='Comma-separated list of cpplint\'s category-filters')
1433 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001434
1435 # Access to a protected member _XX of a client class
1436 # pylint: disable=W0212
1437 try:
1438 import cpplint
1439 import cpplint_chromium
1440 except ImportError:
1441 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1442 return 1
1443
1444 # Change the current working directory before calling lint so that it
1445 # shows the correct base.
1446 previous_cwd = os.getcwd()
1447 os.chdir(settings.GetRoot())
1448 try:
1449 cl = Changelist()
1450 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1451 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001452 if not files:
1453 print "Cannot lint an empty CL"
1454 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001455
1456 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001457 command = args + files
1458 if options.filter:
1459 command = ['--filter=' + ','.join(options.filter)] + command
1460 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001461
1462 white_regex = re.compile(settings.GetLintRegex())
1463 black_regex = re.compile(settings.GetLintIgnoreRegex())
1464 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1465 for filename in filenames:
1466 if white_regex.match(filename):
1467 if black_regex.match(filename):
1468 print "Ignoring file %s" % filename
1469 else:
1470 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1471 extra_check_functions)
1472 else:
1473 print "Skipping file %s" % filename
1474 finally:
1475 os.chdir(previous_cwd)
1476 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1477 if cpplint._cpplint_state.error_count != 0:
1478 return 1
1479 return 0
1480
1481
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001482def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001483 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001484 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001485 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001486 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001487 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001488 (options, args) = parser.parse_args(args)
1489
ukai@chromium.org259e4682012-10-25 07:36:33 +00001490 if not options.force and is_dirty_git_tree('presubmit'):
1491 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492 return 1
1493
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001494 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001495 if args:
1496 base_branch = args[0]
1497 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001498 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001499 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001500
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001501 cl.RunHook(
1502 committing=not options.upload,
1503 may_prompt=False,
1504 verbose=options.verbose,
1505 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001506 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001507
1508
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001509def AddChangeIdToCommitMessage(options, args):
1510 """Re-commits using the current message, assumes the commit hook is in
1511 place.
1512 """
1513 log_desc = options.message or CreateDescriptionFromLog(args)
1514 git_command = ['commit', '--amend', '-m', log_desc]
1515 RunGit(git_command)
1516 new_log_desc = CreateDescriptionFromLog(args)
1517 if CHANGE_ID in new_log_desc:
1518 print 'git-cl: Added Change-Id to commit message.'
1519 else:
1520 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1521
1522
ukai@chromium.orge8077812012-02-03 03:41:46 +00001523def GerritUpload(options, args, cl):
1524 """upload the current branch to gerrit."""
1525 # We assume the remote called "origin" is the one we want.
1526 # It is probably not worthwhile to support different workflows.
1527 remote = 'origin'
1528 branch = 'master'
1529 if options.target_branch:
1530 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001531
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001532 change_desc = ChangeDescription(
1533 options.message or CreateDescriptionFromLog(args))
1534 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001535 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001536 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001537 if CHANGE_ID not in change_desc.description:
1538 AddChangeIdToCommitMessage(options, args)
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001539
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001540 commits = RunGit(['rev-list', '%s/%s..' % (remote, branch)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001541 if len(commits) > 1:
1542 print('WARNING: This will upload %d commits. Run the following command '
1543 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001544 print('git log %s/%s..' % (remote, branch))
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001545 print('You can also use `git squash-branch` to squash these into a single'
1546 'commit.')
1547 ask_for_data('About to upload; enter to confirm.')
1548
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001549 if options.reviewers:
1550 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001551
ukai@chromium.orge8077812012-02-03 03:41:46 +00001552 receive_options = []
1553 cc = cl.GetCCList().split(',')
1554 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001555 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001556 cc = filter(None, cc)
1557 if cc:
1558 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001559 if change_desc.get_reviewers():
1560 receive_options.extend(
1561 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001562
ukai@chromium.orge8077812012-02-03 03:41:46 +00001563 git_command = ['push']
1564 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001565 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001566 ' '.join(receive_options))
1567 git_command += [remote, 'HEAD:refs/for/' + branch]
1568 RunGit(git_command)
1569 # TODO(ukai): parse Change-Id: and set issue number?
1570 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001571
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001572
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001573def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001574 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001575 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1576 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001577 if options.emulate_svn_auto_props:
1578 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001579
1580 change_desc = None
1581
pgervais@chromium.org91141372014-01-09 23:27:20 +00001582 if options.email is not None:
1583 upload_args.extend(['--email', options.email])
1584
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001585 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001586 if options.title:
1587 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001588 if options.message:
1589 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001590 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001591 print ("This branch is associated with issue %s. "
1592 "Adding patch to that issue." % cl.GetIssue())
1593 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001594 if options.title:
1595 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001596 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001597 change_desc = ChangeDescription(message)
1598 if options.reviewers:
1599 change_desc.update_reviewers(options.reviewers)
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001600 if options.auto_bots:
1601 masters = presubmit_support.DoGetTryMasters(
1602 change,
1603 change.LocalPaths(),
1604 settings.GetRoot(),
1605 None,
1606 None,
1607 options.verbose,
1608 sys.stdout)
1609
1610 if masters:
1611 change_description = change_desc.description + '\nCQ_TRYBOTS='
1612 lst = []
1613 for master, mapping in masters.iteritems():
1614 lst.append(master + ':' + ','.join(mapping.keys()))
1615 change_desc.set_description(change_description + ';'.join(lst))
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001616 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001617 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001618
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001619 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001620 print "Description is empty; aborting."
1621 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001622
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001623 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001624 if change_desc.get_reviewers():
1625 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001626 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001627 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001628 DieWithError("Must specify reviewers to send email.")
1629 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001630
1631 # We check this before applying rietveld.private assuming that in
1632 # rietveld.cc only addresses which we can send private CLs to are listed
1633 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1634 # --private is specified explicitly on the command line.
1635 if options.private:
1636 logging.warn('rietveld.cc is ignored since private flag is specified. '
1637 'You need to review and add them manually if necessary.')
1638 cc = cl.GetCCListWithoutDefault()
1639 else:
1640 cc = cl.GetCCList()
1641 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001642 if cc:
1643 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001644
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001645 if options.private or settings.GetDefaultPrivateFlag() == "True":
1646 upload_args.append('--private')
1647
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001648 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001649 if not options.find_copies:
1650 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001651
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001652 # Include the upstream repo's URL in the change -- this is useful for
1653 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001654 remote_url = cl.GetGitBaseUrlFromConfig()
1655 if not remote_url:
1656 if settings.GetIsGitSvn():
1657 # URL is dependent on the current directory.
1658 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1659 if data:
1660 keys = dict(line.split(': ', 1) for line in data.splitlines()
1661 if ': ' in line)
1662 remote_url = keys.get('URL', None)
1663 else:
1664 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1665 remote_url = (cl.GetRemoteUrl() + '@'
1666 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001667 if remote_url:
1668 upload_args.extend(['--base_url', remote_url])
1669
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001670 project = settings.GetProject()
1671 if project:
1672 upload_args.extend(['--project', project])
1673
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001674 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001675 upload_args = ['upload'] + upload_args + args
1676 logging.info('upload.RealMain(%s)', upload_args)
1677 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001678 issue = int(issue)
1679 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001680 except KeyboardInterrupt:
1681 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001682 except:
1683 # If we got an exception after the user typed a description for their
1684 # change, back up the description before re-raising.
1685 if change_desc:
1686 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1687 print '\nGot exception while uploading -- saving description to %s\n' \
1688 % backup_path
1689 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001690 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001691 backup_file.close()
1692 raise
1693
1694 if not cl.GetIssue():
1695 cl.SetIssue(issue)
1696 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001697
1698 if options.use_commit_queue:
1699 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001700 return 0
1701
1702
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001703def cleanup_list(l):
1704 """Fixes a list so that comma separated items are put as individual items.
1705
1706 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1707 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1708 """
1709 items = sum((i.split(',') for i in l), [])
1710 stripped_items = (i.strip() for i in items)
1711 return sorted(filter(None, stripped_items))
1712
1713
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001714@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001715def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001716 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001717 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1718 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001719 parser.add_option('--bypass-watchlists', action='store_true',
1720 dest='bypass_watchlists',
1721 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001722 parser.add_option('-f', action='store_true', dest='force',
1723 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001724 parser.add_option('-m', dest='message', help='message for patchset')
1725 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001726 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001727 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001728 help='reviewer email addresses')
1729 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001730 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001731 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001732 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001733 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001734 parser.add_option('--emulate_svn_auto_props',
1735 '--emulate-svn-auto-props',
1736 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00001737 dest="emulate_svn_auto_props",
1738 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001739 parser.add_option('-c', '--use-commit-queue', action='store_true',
1740 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001741 parser.add_option('--private', action='store_true',
1742 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001743 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001744 '--target-branch',
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001745 help='When uploading to gerrit, remote branch to '
1746 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001747 parser.add_option('--email', default=None,
1748 help='email address to use to connect to Rietveld')
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001749 parser.add_option('--auto-bots', default=False, action='store_true',
1750 help='Autogenerate which trybots to use for this CL')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001751
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001752 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001753 (options, args) = parser.parse_args(args)
1754
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001755 if options.target_branch and not settings.GetIsGerrit():
1756 parser.error('Use --target_branch for non gerrit repository.')
1757
ukai@chromium.org259e4682012-10-25 07:36:33 +00001758 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001759 return 1
1760
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001761 options.reviewers = cleanup_list(options.reviewers)
1762 options.cc = cleanup_list(options.cc)
1763
ukai@chromium.orge8077812012-02-03 03:41:46 +00001764 cl = Changelist()
1765 if args:
1766 # TODO(ukai): is it ok for gerrit case?
1767 base_branch = args[0]
1768 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001769 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001770 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001771 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001772
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001773 # Apply watchlists on upload.
1774 change = cl.GetChange(base_branch, None)
1775 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1776 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001777 if not options.bypass_watchlists:
1778 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001779
ukai@chromium.orge8077812012-02-03 03:41:46 +00001780 if not options.bypass_hooks:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001781 if options.reviewers:
1782 # Set the reviewer list now so that presubmit checks can access it.
1783 change_description = ChangeDescription(change.FullDescriptionText())
1784 change_description.update_reviewers(options.reviewers)
1785 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001786 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001787 may_prompt=not options.force,
1788 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001789 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001790 if not hook_results.should_continue():
1791 return 1
1792 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001793 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001794
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001795 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001796 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001797 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001798 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001799 print ('The last upload made from this repository was patchset #%d but '
1800 'the most recent patchset on the server is #%d.'
1801 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001802 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1803 'from another machine or branch the patch you\'re uploading now '
1804 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001805 ask_for_data('About to upload; enter to confirm.')
1806
iannucci@chromium.org79540052012-10-19 23:15:26 +00001807 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001808 if settings.GetIsGerrit():
1809 return GerritUpload(options, args, cl)
martiniss@chromium.org090df6a2014-06-26 17:38:38 +00001810 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001811 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001812 git_set_branch_value('last-upload-hash',
1813 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001814
1815 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001816
1817
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001818def IsSubmoduleMergeCommit(ref):
1819 # When submodules are added to the repo, we expect there to be a single
1820 # non-git-svn merge commit at remote HEAD with a signature comment.
1821 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001822 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001823 return RunGit(cmd) != ''
1824
1825
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001826def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00001827 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001828
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001829 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001830 Updates changelog with metadata (e.g. pointer to review).
1831 Pushes/dcommits the code upstream.
1832 Updates review and closes.
1833 """
1834 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1835 help='bypass upload presubmit hook')
1836 parser.add_option('-m', dest='message',
1837 help="override review description")
1838 parser.add_option('-f', action='store_true', dest='force',
1839 help="force yes to questions (don't prompt)")
1840 parser.add_option('-c', dest='contributor',
1841 help="external contributor for patch (appended to " +
1842 "description and used as author for git). Should be " +
1843 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001844 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001845 (options, args) = parser.parse_args(args)
1846 cl = Changelist()
1847
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001848 current = cl.GetBranch()
1849 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1850 if not settings.GetIsGitSvn() and remote == '.':
1851 print
1852 print 'Attempting to push branch %r into another local branch!' % current
1853 print
1854 print 'Either reparent this branch on top of origin/master:'
1855 print ' git reparent-branch --root'
1856 print
1857 print 'OR run `git rebase-update` if you think the parent branch is already'
1858 print 'committed.'
1859 print
1860 print ' Current parent: %r' % upstream_branch
1861 return 1
1862
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001863 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001864 # Default to merging against our best guess of the upstream branch.
1865 args = [cl.GetUpstreamBranch()]
1866
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001867 if options.contributor:
1868 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1869 print "Please provide contibutor as 'First Last <email@example.com>'"
1870 return 1
1871
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001872 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001873 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001874
ukai@chromium.org259e4682012-10-25 07:36:33 +00001875 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001876 return 1
1877
1878 # This rev-list syntax means "show all commits not in my branch that
1879 # are in base_branch".
1880 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1881 base_branch]).splitlines()
1882 if upstream_commits:
1883 print ('Base branch "%s" has %d commits '
1884 'not in this branch.' % (base_branch, len(upstream_commits)))
1885 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1886 return 1
1887
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001888 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001889 svn_head = None
1890 if cmd == 'dcommit' or base_has_submodules:
1891 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1892 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001893
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001894 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001895 # If the base_head is a submodule merge commit, the first parent of the
1896 # base_head should be a git-svn commit, which is what we're interested in.
1897 base_svn_head = base_branch
1898 if base_has_submodules:
1899 base_svn_head += '^1'
1900
1901 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001902 if extra_commits:
1903 print ('This branch has %d additional commits not upstreamed yet.'
1904 % len(extra_commits.splitlines()))
1905 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1906 'before attempting to %s.' % (base_branch, cmd))
1907 return 1
1908
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001909 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001910 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001911 author = None
1912 if options.contributor:
1913 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001914 hook_results = cl.RunHook(
1915 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001916 may_prompt=not options.force,
1917 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001918 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001919 if not hook_results.should_continue():
1920 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001921
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001922 # Check the tree status if the tree status URL is set.
1923 status = GetTreeStatus()
1924 if 'closed' == status:
1925 print('The tree is closed. Please wait for it to reopen. Use '
1926 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
1927 return 1
1928 elif 'unknown' == status:
1929 print('Unable to determine tree status. Please verify manually and '
1930 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
1931 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00001932 else:
1933 breakpad.SendStack(
1934 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001935 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1936 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001937 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001938
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001939 change_desc = ChangeDescription(options.message)
1940 if not change_desc.description and cl.GetIssue():
1941 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001942
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001943 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001944 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001945 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001946 else:
1947 print 'No description set.'
1948 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1949 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001950
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001951 # Keep a separate copy for the commit message, because the commit message
1952 # contains the link to the Rietveld issue, while the Rietveld message contains
1953 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001954 # Keep a separate copy for the commit message.
1955 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001956 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001957
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001958 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001959 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001960 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001961 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001962 commit_desc.append_footer('Patch from %s.' % options.contributor)
1963
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001964 print('Description:')
1965 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001966
1967 branches = [base_branch, cl.GetBranchRef()]
1968 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001969 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001970
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001971 # We want to squash all this branch's commits into one commit with the proper
1972 # description. We do this by doing a "reset --soft" to the base branch (which
1973 # keeps the working copy the same), then dcommitting that. If origin/master
1974 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1975 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001976 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001977 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1978 # Delete the branches if they exist.
1979 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1980 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1981 result = RunGitWithCode(showref_cmd)
1982 if result[0] == 0:
1983 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001984
1985 # We might be in a directory that's present in this branch but not in the
1986 # trunk. Move up to the top of the tree so that git commands that expect a
1987 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001988 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001989 if rel_base_path:
1990 os.chdir(rel_base_path)
1991
1992 # Stuff our change into the merge branch.
1993 # We wrap in a try...finally block so if anything goes wrong,
1994 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001995 retcode = -1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001996 used_pending = False
1997 pending_ref = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001998 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001999 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
2000 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002001 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002002 RunGit(
2003 [
2004 'commit', '--author', options.contributor,
2005 '-m', commit_desc.description,
2006 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002007 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002008 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002009 if base_has_submodules:
2010 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2011 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2012 RunGit(['checkout', CHERRY_PICK_BRANCH])
2013 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002014 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002015 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002016 pending_prefix = settings.GetPendingRefPrefix()
2017 if not pending_prefix or branch.startswith(pending_prefix):
2018 # If not using refs/pending/heads/* at all, or target ref is already set
2019 # to pending, then push to the target ref directly.
2020 retcode, output = RunGitWithCode(
2021 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
2022 used_pending = pending_prefix and branch.startswith(pending_prefix)
2023 else:
2024 # Cherry-pick the change on top of pending ref and then push it.
2025 assert branch.startswith('refs/'), branch
2026 assert pending_prefix[-1] == '/', pending_prefix
2027 pending_ref = pending_prefix + branch[len('refs/'):]
2028 retcode, output = PushToGitPending(remote, pending_ref, branch)
2029 used_pending = (retcode == 0)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002030 logging.debug(output)
2031 else:
2032 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00002033 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002034 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00002035 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002036 finally:
2037 # And then swap back to the original branch and clean up.
2038 RunGit(['checkout', '-q', cl.GetBranch()])
2039 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002040 if base_has_submodules:
2041 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002042
2043 if cl.GetIssue():
2044 if cmd == 'dcommit' and 'Committed r' in output:
2045 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002046 elif cmd == 'land' and retcode == 0:
mark@chromium.org671c7a32014-07-31 17:09:36 +00002047 match = (re.match(r'.*?([a-f0-9]{7,})\.\.([a-f0-9]{7,})$', l)
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00002048 for l in output.splitlines(False))
2049 match = filter(None, match)
2050 if len(match) != 1:
2051 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
2052 output)
2053 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002054 else:
2055 return 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002056 to_pending = ' to pending queue' if used_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002057 viewvc_url = settings.GetViewVCUrl()
2058 if viewvc_url and revision:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002059 change_desc.append_footer(
2060 'Committed%s: %s%s' % (to_pending, viewvc_url, revision))
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00002061 elif revision:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002062 change_desc.append_footer('Committed%s: %s' % (to_pending, revision))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002063 print ('Closing issue '
2064 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002065 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002066 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002067 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002068 patch_num = len(props['patchsets'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002069 comment = "Committed patchset #%d%s manually as %s" % (
2070 patch_num, to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002071 if options.bypass_hooks:
2072 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2073 else:
2074 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002075 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002076 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002077
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002078 if used_pending and retcode == 0:
2079 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2080 print 'The commit is in the pending queue (%s).' % pending_ref
2081 print (
2082 'It will show up on %s in ~1 min, once it gets Cr-Commit-Position '
2083 'footer.' % branch)
2084
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002085 if retcode == 0:
2086 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2087 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002088 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002089
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002090 return 0
2091
2092
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002093def PushToGitPending(remote, pending_ref, upstream_ref):
2094 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2095
2096 Returns:
2097 (retcode of last operation, output log of last operation).
2098 """
2099 assert pending_ref.startswith('refs/'), pending_ref
2100 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2101 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2102 code = 0
2103 out = ''
2104 attempt = 0
2105 while attempt < 5:
2106 attempt += 1
2107
2108 # Fetch. Retry fetch errors.
2109 code, out = RunGitWithCode(
2110 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)],
2111 suppress_stderr=True)
2112 if code:
2113 continue
2114
2115 # Try to cherry pick. Abort on merge conflicts.
2116 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
2117 code, out = RunGitWithCode(['cherry-pick', cherry], suppress_stderr=True)
2118 if code:
2119 print (
2120 'Your patch doesn\'t apply cleanly to upstream ref \'%s\', '
2121 'the following files have merge conflicts:' % upstream_ref)
2122 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2123 print 'Please rebase your patch and try again.'
2124 RunGitWithCode(['cherry-pick', '--abort'], suppress_stderr=True)
2125 return code, out
2126
2127 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
2128 code, out = RunGitWithCode(
2129 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2130 if code == 0:
2131 # Success.
2132 return code, out
2133
2134 return code, out
2135
2136
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002137@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002138def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002139 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002140 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002141 message = """This doesn't appear to be an SVN repository.
2142If your project has a git mirror with an upstream SVN master, you probably need
2143to run 'git svn init', see your project's git mirror documentation.
2144If your project has a true writeable upstream repository, you probably want
mark@chromium.org671c7a32014-07-31 17:09:36 +00002145to run 'git cl land' instead.
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002146Choose wisely, if you get this wrong, your commit might appear to succeed but
2147will instead be silently ignored."""
2148 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002149 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002150 return SendUpstream(parser, args, 'dcommit')
2151
2152
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002153@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002154def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002155 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002156 if settings.GetIsGitSvn():
2157 print('This appears to be an SVN repository.')
2158 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002159 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002160 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002161
2162
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002163@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002164def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002165 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002166 parser.add_option('-b', dest='newbranch',
2167 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002168 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002169 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002170 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2171 help='Change to the directory DIR immediately, '
2172 'before doing anything else.')
2173 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002174 help='failed patches spew .rej files rather than '
2175 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002176 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2177 help="don't commit after patch applies")
2178 (options, args) = parser.parse_args(args)
2179 if len(args) != 1:
2180 parser.print_help()
2181 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002182 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002183
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002184 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002185 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002186
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002187 if options.newbranch:
2188 if options.force:
2189 RunGit(['branch', '-D', options.newbranch],
2190 stderr=subprocess2.PIPE, error_ok=True)
2191 RunGit(['checkout', '-b', options.newbranch,
2192 Changelist().GetUpstreamBranch()])
2193
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002194 return PatchIssue(issue_arg, options.reject, options.nocommit,
2195 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002196
2197
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002198def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002199 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002200 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002201 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002202 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002203 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002204 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002205 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002206 # Assume it's a URL to the patch. Default to https.
2207 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002208 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002209 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002210 DieWithError('Must pass an issue ID or full URL for '
2211 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002212 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002213 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002214 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002215
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002216 # Switch up to the top-level directory, if necessary, in preparation for
2217 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002218 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002219 if top:
2220 os.chdir(top)
2221
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002222 # Git patches have a/ at the beginning of source paths. We strip that out
2223 # with a sed script rather than the -p flag to patch so we can feed either
2224 # Git or svn-style patches into the same apply command.
2225 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002226 try:
2227 patch_data = subprocess2.check_output(
2228 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2229 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002230 DieWithError('Git patch mungling failed.')
2231 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002232
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002233 # We use "git apply" to apply the patch instead of "patch" so that we can
2234 # pick up file adds.
2235 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002236 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002237 if directory:
2238 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002239 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002240 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002241 elif IsGitVersionAtLeast('1.7.12'):
2242 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002243 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002244 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002245 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002246 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002247 DieWithError('Failed to apply the patch')
2248
2249 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002250 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002251 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2252 cl = Changelist()
2253 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002254 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002255 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002256 else:
2257 print "Patch applied to index."
2258 return 0
2259
2260
2261def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002262 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002263 # Provide a wrapper for git svn rebase to help avoid accidental
2264 # git svn dcommit.
2265 # It's the only command that doesn't use parser at all since we just defer
2266 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002267
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002268 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002269
2270
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002271def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002272 """Fetches the tree status and returns either 'open', 'closed',
2273 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002274 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002275 if url:
2276 status = urllib2.urlopen(url).read().lower()
2277 if status.find('closed') != -1 or status == '0':
2278 return 'closed'
2279 elif status.find('open') != -1 or status == '1':
2280 return 'open'
2281 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002282 return 'unset'
2283
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002284
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002285def GetTreeStatusReason():
2286 """Fetches the tree status from a json url and returns the message
2287 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002288 url = settings.GetTreeStatusUrl()
2289 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002290 connection = urllib2.urlopen(json_url)
2291 status = json.loads(connection.read())
2292 connection.close()
2293 return status['message']
2294
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002295
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002296def GetBuilderMaster(bot_list):
2297 """For a given builder, fetch the master from AE if available."""
2298 map_url = 'https://builders-map.appspot.com/'
2299 try:
2300 master_map = json.load(urllib2.urlopen(map_url))
2301 except urllib2.URLError as e:
2302 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2303 (map_url, e))
2304 except ValueError as e:
2305 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2306 if not master_map:
2307 return None, 'Failed to build master map.'
2308
2309 result_master = ''
2310 for bot in bot_list:
2311 builder = bot.split(':', 1)[0]
2312 master_list = master_map.get(builder, [])
2313 if not master_list:
2314 return None, ('No matching master for builder %s.' % builder)
2315 elif len(master_list) > 1:
2316 return None, ('The builder name %s exists in multiple masters %s.' %
2317 (builder, master_list))
2318 else:
2319 cur_master = master_list[0]
2320 if not result_master:
2321 result_master = cur_master
2322 elif result_master != cur_master:
2323 return None, 'The builders do not belong to the same master.'
2324 return result_master, None
2325
2326
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002327def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002328 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002329 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002330 status = GetTreeStatus()
2331 if 'unset' == status:
2332 print 'You must configure your tree status URL by running "git cl config".'
2333 return 2
2334
2335 print "The tree is %s" % status
2336 print
2337 print GetTreeStatusReason()
2338 if status != 'open':
2339 return 1
2340 return 0
2341
2342
maruel@chromium.org15192402012-09-06 12:38:29 +00002343def CMDtry(parser, args):
2344 """Triggers a try job through Rietveld."""
2345 group = optparse.OptionGroup(parser, "Try job options")
2346 group.add_option(
2347 "-b", "--bot", action="append",
2348 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2349 "times to specify multiple builders. ex: "
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002350 "'-b win_rel:ui_tests,webkit_unit_tests -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002351 "the try server waterfall for the builders name and the tests "
2352 "available. Can also be used to specify gtest_filter, e.g. "
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002353 "-b win_rel:base_unittests:ValuesTest.*Value"))
maruel@chromium.org15192402012-09-06 12:38:29 +00002354 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002355 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002356 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002357 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002358 "-r", "--revision",
2359 help="Revision to use for the try job; default: the "
2360 "revision will be determined by the try server; see "
2361 "its waterfall for more info")
2362 group.add_option(
2363 "-c", "--clobber", action="store_true", default=False,
2364 help="Force a clobber before building; e.g. don't do an "
2365 "incremental build")
2366 group.add_option(
2367 "--project",
2368 help="Override which project to use. Projects are defined "
2369 "server-side to define what default bot set to use")
2370 group.add_option(
2371 "-t", "--testfilter", action="append", default=[],
2372 help=("Apply a testfilter to all the selected builders. Unless the "
2373 "builders configurations are similar, use multiple "
2374 "--bot <builder>:<test> arguments."))
2375 group.add_option(
2376 "-n", "--name", help="Try job name; default to current branch name")
2377 parser.add_option_group(group)
2378 options, args = parser.parse_args(args)
2379
2380 if args:
2381 parser.error('Unknown arguments: %s' % args)
2382
2383 cl = Changelist()
2384 if not cl.GetIssue():
2385 parser.error('Need to upload first')
2386
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002387 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002388 if props.get('closed'):
2389 parser.error('Cannot send tryjobs for a closed CL')
2390
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002391 if props.get('private'):
2392 parser.error('Cannot use trybots with private issue')
2393
maruel@chromium.org15192402012-09-06 12:38:29 +00002394 if not options.name:
2395 options.name = cl.GetBranch()
2396
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002397 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002398 options.master, err_msg = GetBuilderMaster(options.bot)
2399 if err_msg:
2400 parser.error('Tryserver master cannot be found because: %s\n'
2401 'Please manually specify the tryserver master'
2402 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002403
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002404 def GetMasterMap():
2405 # Process --bot and --testfilter.
2406 if not options.bot:
2407 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002408
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002409 # Get try masters from PRESUBMIT.py files.
2410 masters = presubmit_support.DoGetTryMasters(
2411 change,
2412 change.LocalPaths(),
2413 settings.GetRoot(),
2414 None,
2415 None,
2416 options.verbose,
2417 sys.stdout)
2418 if masters:
2419 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002420
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002421 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2422 options.bot = presubmit_support.DoGetTrySlaves(
2423 change,
2424 change.LocalPaths(),
2425 settings.GetRoot(),
2426 None,
2427 None,
2428 options.verbose,
2429 sys.stdout)
2430 if not options.bot:
2431 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002432
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002433 builders_and_tests = {}
2434 # TODO(machenbach): The old style command-line options don't support
2435 # multiple try masters yet.
2436 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2437 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2438
2439 for bot in old_style:
2440 if ':' in bot:
2441 builder, tests = bot.split(':', 1)
2442 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2443 elif ',' in bot:
2444 parser.error('Specify one bot per --bot flag')
2445 else:
2446 builders_and_tests.setdefault(bot, []).append('defaulttests')
2447
2448 for bot, tests in new_style:
2449 builders_and_tests.setdefault(bot, []).extend(tests)
2450
2451 # Return a master map with one master to be backwards compatible. The
2452 # master name defaults to an empty string, which will cause the master
2453 # not to be set on rietveld (deprecated).
2454 return {options.master: builders_and_tests}
2455
2456 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002457
maruel@chromium.org15192402012-09-06 12:38:29 +00002458 if options.testfilter:
2459 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002460 masters = dict((master, dict(
2461 (b, forced_tests) for b, t in slaves.iteritems()
2462 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002463
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002464 for builders in masters.itervalues():
2465 if any('triggered' in b for b in builders):
2466 print >> sys.stderr, (
2467 'ERROR You are trying to send a job to a triggered bot. This type of'
2468 ' bot requires an\ninitial job from a parent (usually a builder). '
2469 'Instead send your job to the parent.\n'
2470 'Bot list: %s' % builders)
2471 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002472
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002473 patchset = cl.GetMostRecentPatchset()
2474 if patchset and patchset != cl.GetPatchset():
2475 print(
2476 '\nWARNING Mismatch between local config and server. Did a previous '
2477 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2478 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002479 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002480 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002481 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002482 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002483 except urllib2.HTTPError, e:
2484 if e.code == 404:
2485 print('404 from rietveld; '
2486 'did you mean to use "git try" instead of "git cl try"?')
2487 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002488 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002489
2490 for (master, builders) in masters.iteritems():
2491 if master:
2492 print 'Master: %s' % master
2493 length = max(len(builder) for builder in builders)
2494 for builder in sorted(builders):
2495 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002496 return 0
2497
2498
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002499@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002500def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002501 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002502 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002503 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002504 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002505
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002506 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002507 if args:
2508 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002509 branch = cl.GetBranch()
2510 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002511 cl = Changelist()
2512 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002513
2514 # Clear configured merge-base, if there is one.
2515 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002516 else:
2517 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002518 return 0
2519
2520
thestig@chromium.org00858c82013-12-02 23:08:03 +00002521def CMDweb(parser, args):
2522 """Opens the current CL in the web browser."""
2523 _, args = parser.parse_args(args)
2524 if args:
2525 parser.error('Unrecognized args: %s' % ' '.join(args))
2526
2527 issue_url = Changelist().GetIssueURL()
2528 if not issue_url:
2529 print >> sys.stderr, 'ERROR No issue to open'
2530 return 1
2531
2532 webbrowser.open(issue_url)
2533 return 0
2534
2535
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002536def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002537 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002538 _, args = parser.parse_args(args)
2539 if args:
2540 parser.error('Unrecognized args: %s' % ' '.join(args))
2541 cl = Changelist()
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002542 props = cl.GetIssueProperties()
2543 if props.get('private'):
2544 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002545 cl.SetFlag('commit', '1')
2546 return 0
2547
2548
groby@chromium.org411034a2013-02-26 15:12:01 +00002549def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002550 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002551 _, args = parser.parse_args(args)
2552 if args:
2553 parser.error('Unrecognized args: %s' % ' '.join(args))
2554 cl = Changelist()
2555 # Ensure there actually is an issue to close.
2556 cl.GetDescription()
2557 cl.CloseIssue()
2558 return 0
2559
2560
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002561def CMDdiff(parser, args):
2562 """shows differences between local tree and last upload."""
2563 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002564 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002565 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002566 if not issue:
2567 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002568 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002569 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002570
2571 # Create a new branch based on the merge-base
2572 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2573 try:
2574 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002575 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002576 if rtn != 0:
2577 return rtn
2578
2579 # Switch back to starting brand and diff against the temporary
2580 # branch containing the latest rietveld patch.
2581 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2582 finally:
2583 RunGit(['checkout', '-q', branch])
2584 RunGit(['branch', '-D', TMP_BRANCH])
2585
2586 return 0
2587
2588
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002589def CMDowners(parser, args):
2590 """interactively find the owners for reviewing"""
2591 parser.add_option(
2592 '--no-color',
2593 action='store_true',
2594 help='Use this option to disable color output')
2595 options, args = parser.parse_args(args)
2596
2597 author = RunGit(['config', 'user.email']).strip() or None
2598
2599 cl = Changelist()
2600
2601 if args:
2602 if len(args) > 1:
2603 parser.error('Unknown args')
2604 base_branch = args[0]
2605 else:
2606 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002607 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002608
2609 change = cl.GetChange(base_branch, None)
2610 return owners_finder.OwnersFinder(
2611 [f.LocalPath() for f in
2612 cl.GetChange(base_branch, None).AffectedFiles()],
2613 change.RepositoryRoot(), author,
2614 fopen=file, os_path=os.path, glob=glob.glob,
2615 disable_color=options.no_color).run()
2616
2617
enne@chromium.org555cfe42014-01-29 18:21:39 +00002618@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002619def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002620 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002621 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002622 parser.add_option('--full', action='store_true',
2623 help='Reformat the full content of all touched files')
2624 parser.add_option('--dry-run', action='store_true',
2625 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002626 parser.add_option('--diff', action='store_true',
2627 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002628 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002629
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002630 # git diff generates paths against the root of the repository. Change
2631 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002632 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002633 if rel_base_path:
2634 os.chdir(rel_base_path)
2635
digit@chromium.org29e47272013-05-17 17:01:46 +00002636 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002637 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002638 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002639 # Only list the names of modified files.
2640 diff_cmd.append('--name-only')
2641 else:
2642 # Only generate context-less patches.
2643 diff_cmd.append('-U0')
2644
2645 # Grab the merge-base commit, i.e. the upstream commit of the current
2646 # branch when it was created or the last time it was rebased. This is
2647 # to cover the case where the user may have called "git fetch origin",
2648 # moving the origin branch to a newer commit, but hasn't rebased yet.
2649 upstream_commit = None
2650 cl = Changelist()
2651 upstream_branch = cl.GetUpstreamBranch()
2652 if upstream_branch:
2653 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2654 upstream_commit = upstream_commit.strip()
2655
2656 if not upstream_commit:
2657 DieWithError('Could not find base commit for this branch. '
2658 'Are you in detached state?')
2659
2660 diff_cmd.append(upstream_commit)
2661
2662 # Handle source file filtering.
2663 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002664 if args:
2665 for arg in args:
2666 if os.path.isdir(arg):
2667 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2668 elif os.path.isfile(arg):
2669 diff_cmd.append(arg)
2670 else:
2671 DieWithError('Argument "%s" is not a file or a directory' % arg)
2672 else:
2673 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002674 diff_output = RunGit(diff_cmd)
2675
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002676 top_dir = os.path.normpath(
2677 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2678
2679 # Locate the clang-format binary in the checkout
2680 try:
2681 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2682 except clang_format.NotFoundError, e:
2683 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002684
digit@chromium.org29e47272013-05-17 17:01:46 +00002685 if opts.full:
2686 # diff_output is a list of files to send to clang-format.
2687 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002688 if not files:
2689 print "Nothing to format."
2690 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002691 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002692 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002693 cmd.append('-i')
2694 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002695 if opts.diff:
2696 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002697 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002698 env = os.environ.copy()
2699 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002700 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002701 try:
2702 script = clang_format.FindClangFormatScriptInChromiumTree(
2703 'clang-format-diff.py')
2704 except clang_format.NotFoundError, e:
2705 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002706
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002707 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002708 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002709 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002710
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002711 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002712 if opts.diff:
2713 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002714 if opts.dry_run and len(stdout) > 0:
2715 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002716
2717 return 0
2718
2719
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002720class OptionParser(optparse.OptionParser):
2721 """Creates the option parse and add --verbose support."""
2722 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002723 optparse.OptionParser.__init__(
2724 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002725 self.add_option(
2726 '-v', '--verbose', action='count', default=0,
2727 help='Use 2 times for more debugging info')
2728
2729 def parse_args(self, args=None, values=None):
2730 options, args = optparse.OptionParser.parse_args(self, args, values)
2731 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2732 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2733 return options, args
2734
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002735
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002736def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002737 if sys.hexversion < 0x02060000:
2738 print >> sys.stderr, (
2739 '\nYour python version %s is unsupported, please upgrade.\n' %
2740 sys.version.split(' ', 1)[0])
2741 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002742
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002743 # Reload settings.
2744 global settings
2745 settings = Settings()
2746
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002747 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002748 dispatcher = subcommand.CommandDispatcher(__name__)
2749 try:
2750 return dispatcher.execute(OptionParser(), argv)
2751 except urllib2.HTTPError, e:
2752 if e.code != 500:
2753 raise
2754 DieWithError(
2755 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2756 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002757
2758
2759if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002760 # These affect sys.stdout so do it outside of main() to simplify mocks in
2761 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002762 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002763 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002764 sys.exit(main(sys.argv[1:]))