blob: 4acbed58195d58d163aa291b8aa703d6c7a98440 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000011import glob
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000012import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000013import logging
14import optparse
15import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000016import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000018import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import textwrap
maruel@chromium.org1033efd2013-07-23 23:25:09 +000021import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000023import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000024import webbrowser
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025
26try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000027 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028except ImportError:
29 pass
30
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000032from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033from third_party import upload
34import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000035import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000036import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000037import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000038import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000039import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000040import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000041import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042import watchlists
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000043import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000044
maruel@chromium.org0633fb42013-08-16 20:06:14 +000045__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000046
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000047DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000048POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000049DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000050GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000051CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000052
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000053# Shortcut since it quickly becomes redundant.
54Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000055
maruel@chromium.orgddd59412011-11-30 14:20:38 +000056# Initialized in main()
57settings = None
58
59
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000060def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000061 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000062 sys.exit(1)
63
64
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000065def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000066 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000067 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000068 except subprocess2.CalledProcessError as e:
69 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000070 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000071 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000072 'Command "%s" failed.\n%s' % (
73 ' '.join(args), error_message or e.stdout or ''))
74 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000075
76
77def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000078 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000079 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000080
81
82def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000083 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000084 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +000085 env = os.environ.copy()
86 # 'cat' is a magical git string that disables pagers on all platforms.
87 env['GIT_PAGER'] = 'cat'
88 out, code = subprocess2.communicate(['git'] + args,
89 env=env,
bratell@opera.comf267b0e2013-05-02 09:11:43 +000090 stdout=subprocess2.PIPE)
szager@chromium.org9bb85e22012-06-13 20:28:23 +000091 return code, out[0]
92 except ValueError:
93 # When the subprocess fails, it returns None. That triggers a ValueError
94 # when trying to unpack the return value into (out, code).
95 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000096
97
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000098def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +000099 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000100 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000101 return (version.startswith(prefix) and
102 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000103
104
maruel@chromium.org90541732011-04-01 17:54:18 +0000105def ask_for_data(prompt):
106 try:
107 return raw_input(prompt)
108 except KeyboardInterrupt:
109 # Hide the exception.
110 sys.exit(1)
111
112
iannucci@chromium.org79540052012-10-19 23:15:26 +0000113def git_set_branch_value(key, value):
114 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000115 if not branch:
116 return
117
118 cmd = ['config']
119 if isinstance(value, int):
120 cmd.append('--int')
121 git_key = 'branch.%s.%s' % (branch, key)
122 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000123
124
125def git_get_branch_default(key, default):
126 branch = Changelist().GetBranch()
127 if branch:
128 git_key = 'branch.%s.%s' % (branch, key)
129 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
130 try:
131 return int(stdout.strip())
132 except ValueError:
133 pass
134 return default
135
136
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000137def add_git_similarity(parser):
138 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000139 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000140 help='Sets the percentage that a pair of files need to match in order to'
141 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000142 parser.add_option(
143 '--find-copies', action='store_true',
144 help='Allows git to look for copies.')
145 parser.add_option(
146 '--no-find-copies', action='store_false', dest='find_copies',
147 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000148
149 old_parser_args = parser.parse_args
150 def Parse(args):
151 options, args = old_parser_args(args)
152
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000153 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000154 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000155 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000156 print('Note: Saving similarity of %d%% in git config.'
157 % options.similarity)
158 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000159
iannucci@chromium.org79540052012-10-19 23:15:26 +0000160 options.similarity = max(0, min(options.similarity, 100))
161
162 if options.find_copies is None:
163 options.find_copies = bool(
164 git_get_branch_default('git-find-copies', True))
165 else:
166 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000167
168 print('Using %d%% similarity for rename/copy detection. '
169 'Override with --similarity.' % options.similarity)
170
171 return options, args
172 parser.parse_args = Parse
173
174
ukai@chromium.org259e4682012-10-25 07:36:33 +0000175def is_dirty_git_tree(cmd):
176 # Make sure index is up-to-date before running diff-index.
177 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
178 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
179 if dirty:
180 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
181 print 'Uncommitted files: (git diff-index --name-status HEAD)'
182 print dirty[:4096]
183 if len(dirty) > 4096:
184 print '... (run "git diff-index --name-status HEAD" to see full output).'
185 return True
186 return False
187
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000188
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000189def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
190 """Return the corresponding git ref if |base_url| together with |glob_spec|
191 matches the full |url|.
192
193 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
194 """
195 fetch_suburl, as_ref = glob_spec.split(':')
196 if allow_wildcards:
197 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
198 if glob_match:
199 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
200 # "branches/{472,597,648}/src:refs/remotes/svn/*".
201 branch_re = re.escape(base_url)
202 if glob_match.group(1):
203 branch_re += '/' + re.escape(glob_match.group(1))
204 wildcard = glob_match.group(2)
205 if wildcard == '*':
206 branch_re += '([^/]*)'
207 else:
208 # Escape and replace surrounding braces with parentheses and commas
209 # with pipe symbols.
210 wildcard = re.escape(wildcard)
211 wildcard = re.sub('^\\\\{', '(', wildcard)
212 wildcard = re.sub('\\\\,', '|', wildcard)
213 wildcard = re.sub('\\\\}$', ')', wildcard)
214 branch_re += wildcard
215 if glob_match.group(3):
216 branch_re += re.escape(glob_match.group(3))
217 match = re.match(branch_re, url)
218 if match:
219 return re.sub('\*$', match.group(1), as_ref)
220
221 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
222 if fetch_suburl:
223 full_url = base_url + '/' + fetch_suburl
224 else:
225 full_url = base_url
226 if full_url == url:
227 return as_ref
228 return None
229
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000230
iannucci@chromium.org79540052012-10-19 23:15:26 +0000231def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000232 """Prints statistics about the change to the user."""
233 # --no-ext-diff is broken in some versions of Git, so try to work around
234 # this by overriding the environment (but there is still a problem if the
235 # git config key "diff.external" is used).
236 env = os.environ.copy()
237 if 'GIT_EXTERNAL_DIFF' in env:
238 del env['GIT_EXTERNAL_DIFF']
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000239 # 'cat' is a magical git string that disables pagers on all platforms.
240 env['GIT_PAGER'] = 'cat'
iannucci@chromium.org79540052012-10-19 23:15:26 +0000241
242 if find_copies:
243 similarity_options = ['--find-copies-harder', '-l100000',
244 '-C%s' % similarity]
245 else:
246 similarity_options = ['-M%s' % similarity]
247
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000248 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000249 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000250 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000251 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000252
253
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000254class Settings(object):
255 def __init__(self):
256 self.default_server = None
257 self.cc = None
258 self.root = None
259 self.is_git_svn = None
260 self.svn_branch = None
261 self.tree_status_url = None
262 self.viewvc_url = None
263 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000264 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000265 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000266
267 def LazyUpdateIfNeeded(self):
268 """Updates the settings from a codereview.settings file, if available."""
269 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000270 # The only value that actually changes the behavior is
271 # autoupdate = "false". Everything else means "true".
272 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
273 error_ok=True
274 ).strip().lower()
275
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000276 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000277 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000278 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000279 # set updated to True to avoid infinite calling loop
280 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000281 self.updated = True
282 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000283 self.updated = True
284
285 def GetDefaultServerUrl(self, error_ok=False):
286 if not self.default_server:
287 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000288 self.default_server = gclient_utils.UpgradeToHttps(
289 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000290 if error_ok:
291 return self.default_server
292 if not self.default_server:
293 error_message = ('Could not find settings file. You must configure '
294 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000295 self.default_server = gclient_utils.UpgradeToHttps(
296 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000297 return self.default_server
298
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000299 def GetRoot(self):
300 if not self.root:
301 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
302 return self.root
303
304 def GetIsGitSvn(self):
305 """Return true if this repo looks like it's using git-svn."""
306 if self.is_git_svn is None:
307 # If you have any "svn-remote.*" config keys, we think you're using svn.
308 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000309 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000310 return self.is_git_svn
311
312 def GetSVNBranch(self):
313 if self.svn_branch is None:
314 if not self.GetIsGitSvn():
315 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
316
317 # Try to figure out which remote branch we're based on.
318 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000319 # 1) iterate through our branch history and find the svn URL.
320 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000321
322 # regexp matching the git-svn line that contains the URL.
323 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
324
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000325 env = os.environ.copy()
326 # 'cat' is a magical git string that disables pagers on all platforms.
327 env['GIT_PAGER'] = 'cat'
328
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000329 # We don't want to go through all of history, so read a line from the
330 # pipe at a time.
331 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000332 cmd = ['git', 'log', '-100', '--pretty=medium']
333 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE, env=env)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000334 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000335 for line in proc.stdout:
336 match = git_svn_re.match(line)
337 if match:
338 url = match.group(1)
339 proc.stdout.close() # Cut pipe.
340 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000341
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000342 if url:
343 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
344 remotes = RunGit(['config', '--get-regexp',
345 r'^svn-remote\..*\.url']).splitlines()
346 for remote in remotes:
347 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000348 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000349 remote = match.group(1)
350 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000351 rewrite_root = RunGit(
352 ['config', 'svn-remote.%s.rewriteRoot' % remote],
353 error_ok=True).strip()
354 if rewrite_root:
355 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000356 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000357 ['config', 'svn-remote.%s.fetch' % remote],
358 error_ok=True).strip()
359 if fetch_spec:
360 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
361 if self.svn_branch:
362 break
363 branch_spec = RunGit(
364 ['config', 'svn-remote.%s.branches' % remote],
365 error_ok=True).strip()
366 if branch_spec:
367 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
368 if self.svn_branch:
369 break
370 tag_spec = RunGit(
371 ['config', 'svn-remote.%s.tags' % remote],
372 error_ok=True).strip()
373 if tag_spec:
374 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
375 if self.svn_branch:
376 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000377
378 if not self.svn_branch:
379 DieWithError('Can\'t guess svn branch -- try specifying it on the '
380 'command line')
381
382 return self.svn_branch
383
384 def GetTreeStatusUrl(self, error_ok=False):
385 if not self.tree_status_url:
386 error_message = ('You must configure your tree status URL by running '
387 '"git cl config".')
388 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
389 error_ok=error_ok,
390 error_message=error_message)
391 return self.tree_status_url
392
393 def GetViewVCUrl(self):
394 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000395 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000396 return self.viewvc_url
397
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000398 def GetDefaultCCList(self):
399 return self._GetConfig('rietveld.cc', error_ok=True)
400
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000401 def GetDefaultPrivateFlag(self):
402 return self._GetConfig('rietveld.private', error_ok=True)
403
ukai@chromium.orge8077812012-02-03 03:41:46 +0000404 def GetIsGerrit(self):
405 """Return true if this repo is assosiated with gerrit code review system."""
406 if self.is_gerrit is None:
407 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
408 return self.is_gerrit
409
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000410 def GetGitEditor(self):
411 """Return the editor specified in the git config, or None if none is."""
412 if self.git_editor is None:
413 self.git_editor = self._GetConfig('core.editor', error_ok=True)
414 return self.git_editor or None
415
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000416 def _GetConfig(self, param, **kwargs):
417 self.LazyUpdateIfNeeded()
418 return RunGit(['config', param], **kwargs).strip()
419
420
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000421def ShortBranchName(branch):
422 """Convert a name like 'refs/heads/foo' to just 'foo'."""
423 return branch.replace('refs/heads/', '')
424
425
426class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000427 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000428 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000429 global settings
430 if not settings:
431 # Happens when git_cl.py is used as a utility library.
432 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000433 settings.GetDefaultServerUrl()
434 self.branchref = branchref
435 if self.branchref:
436 self.branch = ShortBranchName(self.branchref)
437 else:
438 self.branch = None
439 self.rietveld_server = None
440 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000441 self.lookedup_issue = False
442 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000443 self.has_description = False
444 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000445 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000446 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000447 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000448 self.cc = None
449 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000450 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000451 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000452
453 def GetCCList(self):
454 """Return the users cc'd on this CL.
455
456 Return is a string suitable for passing to gcl with the --cc flag.
457 """
458 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000459 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000460 more_cc = ','.join(self.watchers)
461 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
462 return self.cc
463
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000464 def GetCCListWithoutDefault(self):
465 """Return the users cc'd on this CL excluding default ones."""
466 if self.cc is None:
467 self.cc = ','.join(self.watchers)
468 return self.cc
469
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000470 def SetWatchers(self, watchers):
471 """Set the list of email addresses that should be cc'd based on the changed
472 files in this CL.
473 """
474 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000475
476 def GetBranch(self):
477 """Returns the short branch name, e.g. 'master'."""
478 if not self.branch:
479 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
480 self.branch = ShortBranchName(self.branchref)
481 return self.branch
482
483 def GetBranchRef(self):
484 """Returns the full branch name, e.g. 'refs/heads/master'."""
485 self.GetBranch() # Poke the lazy loader.
486 return self.branchref
487
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000488 @staticmethod
489 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000490 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000491 e.g. 'origin', 'refs/heads/master'
492 """
493 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000494 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
495 error_ok=True).strip()
496 if upstream_branch:
497 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
498 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000499 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
500 error_ok=True).strip()
501 if upstream_branch:
502 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000503 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000504 # Fall back on trying a git-svn upstream branch.
505 if settings.GetIsGitSvn():
506 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000507 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000508 # Else, try to guess the origin remote.
509 remote_branches = RunGit(['branch', '-r']).split()
510 if 'origin/master' in remote_branches:
511 # Fall back on origin/master if it exits.
512 remote = 'origin'
513 upstream_branch = 'refs/heads/master'
514 elif 'origin/trunk' in remote_branches:
515 # Fall back on origin/trunk if it exists. Generally a shared
516 # git-svn clone
517 remote = 'origin'
518 upstream_branch = 'refs/heads/trunk'
519 else:
520 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000521Either pass complete "git diff"-style arguments, like
522 git cl upload origin/master
523or verify this branch is set up to track another (via the --track argument to
524"git checkout -b ...").""")
525
526 return remote, upstream_branch
527
528 def GetUpstreamBranch(self):
529 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000530 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000531 if remote is not '.':
532 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
533 self.upstream_branch = upstream_branch
534 return self.upstream_branch
535
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000536 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000537 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000538 remote, branch = None, self.GetBranch()
539 seen_branches = set()
540 while branch not in seen_branches:
541 seen_branches.add(branch)
542 remote, branch = self.FetchUpstreamTuple(branch)
543 branch = ShortBranchName(branch)
544 if remote != '.' or branch.startswith('refs/remotes'):
545 break
546 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000547 remotes = RunGit(['remote'], error_ok=True).split()
548 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000549 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000550 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000551 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000552 logging.warning('Could not determine which remote this change is '
553 'associated with, so defaulting to "%s". This may '
554 'not be what you want. You may prevent this message '
555 'by running "git svn info" as documented here: %s',
556 self._remote,
557 GIT_INSTRUCTIONS_URL)
558 else:
559 logging.warn('Could not determine which remote this change is '
560 'associated with. You may prevent this message by '
561 'running "git svn info" as documented here: %s',
562 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000563 branch = 'HEAD'
564 if branch.startswith('refs/remotes'):
565 self._remote = (remote, branch)
566 else:
567 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000568 return self._remote
569
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000570 def GitSanityChecks(self, upstream_git_obj):
571 """Checks git repo status and ensures diff is from local commits."""
572
573 # Verify the commit we're diffing against is in our current branch.
574 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
575 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
576 if upstream_sha != common_ancestor:
577 print >> sys.stderr, (
578 'ERROR: %s is not in the current branch. You may need to rebase '
579 'your tracking branch' % upstream_sha)
580 return False
581
582 # List the commits inside the diff, and verify they are all local.
583 commits_in_diff = RunGit(
584 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
585 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
586 remote_branch = remote_branch.strip()
587 if code != 0:
588 _, remote_branch = self.GetRemoteBranch()
589
590 commits_in_remote = RunGit(
591 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
592
593 common_commits = set(commits_in_diff) & set(commits_in_remote)
594 if common_commits:
595 print >> sys.stderr, (
596 'ERROR: Your diff contains %d commits already in %s.\n'
597 'Run "git log --oneline %s..HEAD" to get a list of commits in '
598 'the diff. If you are using a custom git flow, you can override'
599 ' the reference used for this check with "git config '
600 'gitcl.remotebranch <git-ref>".' % (
601 len(common_commits), remote_branch, upstream_git_obj))
602 return False
603 return True
604
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000605 def GetGitBaseUrlFromConfig(self):
606 """Return the configured base URL from branch.<branchname>.baseurl.
607
608 Returns None if it is not set.
609 """
610 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
611 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000612
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000613 def GetRemoteUrl(self):
614 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
615
616 Returns None if there is no remote.
617 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000618 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000619 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
620
621 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000622 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000623 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000624 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000625 self.issue = int(issue) or None if issue else None
626 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000627 return self.issue
628
629 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000630 if not self.rietveld_server:
631 # If we're on a branch then get the server potentially associated
632 # with that branch.
633 if self.GetIssue():
634 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
635 ['config', self._RietveldServer()], error_ok=True).strip())
636 if not self.rietveld_server:
637 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000638 return self.rietveld_server
639
640 def GetIssueURL(self):
641 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000642 if not self.GetIssue():
643 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000644 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
645
646 def GetDescription(self, pretty=False):
647 if not self.has_description:
648 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000649 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000650 try:
651 self.description = self.RpcServer().get_description(issue).strip()
652 except urllib2.HTTPError, e:
653 if e.code == 404:
654 DieWithError(
655 ('\nWhile fetching the description for issue %d, received a '
656 '404 (not found)\n'
657 'error. It is likely that you deleted this '
658 'issue on the server. If this is the\n'
659 'case, please run\n\n'
660 ' git cl issue 0\n\n'
661 'to clear the association with the deleted issue. Then run '
662 'this command again.') % issue)
663 else:
664 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000665 '\nFailed to fetch issue description. HTTP error %d' % e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000666 self.has_description = True
667 if pretty:
668 wrapper = textwrap.TextWrapper()
669 wrapper.initial_indent = wrapper.subsequent_indent = ' '
670 return wrapper.fill(self.description)
671 return self.description
672
673 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000674 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000675 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000676 patchset = RunGit(['config', self._PatchsetSetting()],
677 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000678 self.patchset = int(patchset) or None if patchset else None
679 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000680 return self.patchset
681
682 def SetPatchset(self, patchset):
683 """Set this branch's patchset. If patchset=0, clears the patchset."""
684 if patchset:
685 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000686 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000687 else:
688 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000689 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000690 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000691
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000692 def GetMostRecentPatchset(self):
693 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000694
695 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000696 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000697 '/download/issue%s_%s.diff' % (issue, patchset))
698
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000699 def GetIssueProperties(self):
700 if self._props is None:
701 issue = self.GetIssue()
702 if not issue:
703 self._props = {}
704 else:
705 self._props = self.RpcServer().get_issue_properties(issue, True)
706 return self._props
707
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000708 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000709 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000710
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000711 def SetIssue(self, issue):
712 """Set this branch's issue. If issue=0, clears the issue."""
713 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000714 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000715 RunGit(['config', self._IssueSetting(), str(issue)])
716 if self.rietveld_server:
717 RunGit(['config', self._RietveldServer(), self.rietveld_server])
718 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000719 current_issue = self.GetIssue()
720 if current_issue:
721 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000722 self.issue = None
723 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000724
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000725 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000726 if not self.GitSanityChecks(upstream_branch):
727 DieWithError('\nGit sanity check failure')
728
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000729 env = os.environ.copy()
730 # 'cat' is a magical git string that disables pagers on all platforms.
731 env['GIT_PAGER'] = 'cat'
732
733 root = RunCommand(['git', 'rev-parse', '--show-cdup'], env=env).strip()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000734 if not root:
735 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000736 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000737
738 # We use the sha1 of HEAD as a name of this change.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000739 name = RunCommand(['git', 'rev-parse', 'HEAD'], env=env).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000740 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000741 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000742 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000743 except subprocess2.CalledProcessError:
744 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000745 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000746 'This branch probably doesn\'t exist anymore. To reset the\n'
747 'tracking branch, please run\n'
748 ' git branch --set-upstream %s trunk\n'
749 'replacing trunk with origin/master or the relevant branch') %
750 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000751
maruel@chromium.org52424302012-08-29 15:14:30 +0000752 issue = self.GetIssue()
753 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000754 if issue:
755 description = self.GetDescription()
756 else:
757 # If the change was never uploaded, use the log messages of all commits
758 # up to the branch point, as git cl upload will prefill the description
759 # with these log messages.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000760 description = RunCommand(['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000761 'log', '--pretty=format:%s%n%n%b',
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000762 '%s...' % (upstream_branch)],
763 env=env).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000764
765 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000766 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000767 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000768 name,
769 description,
770 absroot,
771 files,
772 issue,
773 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000774 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000775
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000776 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000777 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000778
779 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000780 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000781 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000782 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000783 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000784 except presubmit_support.PresubmitFailure, e:
785 DieWithError(
786 ('%s\nMaybe your depot_tools is out of date?\n'
787 'If all fails, contact maruel@') % e)
788
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000789 def UpdateDescription(self, description):
790 self.description = description
791 return self.RpcServer().update_description(
792 self.GetIssue(), self.description)
793
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000795 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000796 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000798 def SetFlag(self, flag, value):
799 """Patchset must match."""
800 if not self.GetPatchset():
801 DieWithError('The patchset needs to match. Send another patchset.')
802 try:
803 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000804 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000805 except urllib2.HTTPError, e:
806 if e.code == 404:
807 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
808 if e.code == 403:
809 DieWithError(
810 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
811 'match?') % (self.GetIssue(), self.GetPatchset()))
812 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000813
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000814 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000815 """Returns an upload.RpcServer() to access this review's rietveld instance.
816 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000817 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000818 self._rpc_server = rietveld.CachingRietveld(
819 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000820 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000821
822 def _IssueSetting(self):
823 """Return the git setting that stores this change's issue."""
824 return 'branch.%s.rietveldissue' % self.GetBranch()
825
826 def _PatchsetSetting(self):
827 """Return the git setting that stores this change's most recent patchset."""
828 return 'branch.%s.rietveldpatchset' % self.GetBranch()
829
830 def _RietveldServer(self):
831 """Returns the git setting that stores this change's rietveld server."""
832 return 'branch.%s.rietveldserver' % self.GetBranch()
833
834
835def GetCodereviewSettingsInteractively():
836 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000837 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000838 server = settings.GetDefaultServerUrl(error_ok=True)
839 prompt = 'Rietveld server (host[:port])'
840 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000841 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000842 if not server and not newserver:
843 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000844 if newserver:
845 newserver = gclient_utils.UpgradeToHttps(newserver)
846 if newserver != server:
847 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000849 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000850 prompt = caption
851 if initial:
852 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000853 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000854 if new_val == 'x':
855 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000856 elif new_val:
857 if is_url:
858 new_val = gclient_utils.UpgradeToHttps(new_val)
859 if new_val != initial:
860 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000861
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000862 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000863 SetProperty(settings.GetDefaultPrivateFlag(),
864 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000865 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000866 'tree-status-url', False)
867 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000868
869 # TODO: configure a default branch to diff against, rather than this
870 # svn-based hackery.
871
872
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000873class ChangeDescription(object):
874 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000875 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000876 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000877
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000878 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000879 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000880
agable@chromium.org42c20792013-09-12 17:34:49 +0000881 @property # www.logilab.org/ticket/89786
882 def description(self): # pylint: disable=E0202
883 return '\n'.join(self._description_lines)
884
885 def set_description(self, desc):
886 if isinstance(desc, basestring):
887 lines = desc.splitlines()
888 else:
889 lines = [line.rstrip() for line in desc]
890 while lines and not lines[0]:
891 lines.pop(0)
892 while lines and not lines[-1]:
893 lines.pop(-1)
894 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000895
896 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000897 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000898 assert isinstance(reviewers, list), reviewers
899 if not reviewers:
900 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000901 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000902
agable@chromium.org42c20792013-09-12 17:34:49 +0000903 # Get the set of R= and TBR= lines and remove them from the desciption.
904 regexp = re.compile(self.R_LINE)
905 matches = [regexp.match(line) for line in self._description_lines]
906 new_desc = [l for i, l in enumerate(self._description_lines)
907 if not matches[i]]
908 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000909
agable@chromium.org42c20792013-09-12 17:34:49 +0000910 # Construct new unified R= and TBR= lines.
911 r_names = []
912 tbr_names = []
913 for match in matches:
914 if not match:
915 continue
916 people = cleanup_list([match.group(2).strip()])
917 if match.group(1) == 'TBR':
918 tbr_names.extend(people)
919 else:
920 r_names.extend(people)
921 for name in r_names:
922 if name not in reviewers:
923 reviewers.append(name)
924 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
925 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
926
927 # Put the new lines in the description where the old first R= line was.
928 line_loc = next((i for i, match in enumerate(matches) if match), -1)
929 if 0 <= line_loc < len(self._description_lines):
930 if new_tbr_line:
931 self._description_lines.insert(line_loc, new_tbr_line)
932 if new_r_line:
933 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000934 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000935 if new_r_line:
936 self.append_footer(new_r_line)
937 if new_tbr_line:
938 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000939
940 def prompt(self):
941 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000942 self.set_description([
943 '# Enter a description of the change.',
944 '# This will be displayed on the codereview site.',
945 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000946 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000947 '--------------------',
948 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000949
agable@chromium.org42c20792013-09-12 17:34:49 +0000950 regexp = re.compile(self.BUG_LINE)
951 if not any((regexp.match(line) for line in self._description_lines)):
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000952 self.append_footer('BUG=')
agable@chromium.org42c20792013-09-12 17:34:49 +0000953 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000954 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000955 if not content:
956 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000957 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000958
959 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000960 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
961 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000962 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000963 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000964
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000965 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000966 if self._description_lines:
967 # Add an empty line if either the last line or the new line isn't a tag.
968 last_line = self._description_lines[-1]
969 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
970 not presubmit_support.Change.TAG_LINE_RE.match(line)):
971 self._description_lines.append('')
972 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000973
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000974 def get_reviewers(self):
975 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000976 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
977 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000978 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000979
980
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000981def get_approving_reviewers(props):
982 """Retrieves the reviewers that approved a CL from the issue properties with
983 messages.
984
985 Note that the list may contain reviewers that are not committer, thus are not
986 considered by the CQ.
987 """
988 return sorted(
989 set(
990 message['sender']
991 for message in props['messages']
992 if message['approval'] and message['sender'] in props['reviewers']
993 )
994 )
995
996
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000997def FindCodereviewSettingsFile(filename='codereview.settings'):
998 """Finds the given file starting in the cwd and going up.
999
1000 Only looks up to the top of the repository unless an
1001 'inherit-review-settings-ok' file exists in the root of the repository.
1002 """
1003 inherit_ok_file = 'inherit-review-settings-ok'
1004 cwd = os.getcwd()
1005 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
1006 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1007 root = '/'
1008 while True:
1009 if filename in os.listdir(cwd):
1010 if os.path.isfile(os.path.join(cwd, filename)):
1011 return open(os.path.join(cwd, filename))
1012 if cwd == root:
1013 break
1014 cwd = os.path.dirname(cwd)
1015
1016
1017def LoadCodereviewSettingsFromFile(fileobj):
1018 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001019 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001020
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001021 def SetProperty(name, setting, unset_error_ok=False):
1022 fullname = 'rietveld.' + name
1023 if setting in keyvals:
1024 RunGit(['config', fullname, keyvals[setting]])
1025 else:
1026 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1027
1028 SetProperty('server', 'CODE_REVIEW_SERVER')
1029 # Only server setting is required. Other settings can be absent.
1030 # In that case, we ignore errors raised during option deletion attempt.
1031 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001032 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001033 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1034 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
1035
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001036 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001037 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001038
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001039 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1040 #should be of the form
1041 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1042 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1043 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1044 keyvals['ORIGIN_URL_CONFIG']])
1045
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001046
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001047def urlretrieve(source, destination):
1048 """urllib is broken for SSL connections via a proxy therefore we
1049 can't use urllib.urlretrieve()."""
1050 with open(destination, 'w') as f:
1051 f.write(urllib2.urlopen(source).read())
1052
1053
ukai@chromium.org712d6102013-11-27 00:52:58 +00001054def hasSheBang(fname):
1055 """Checks fname is a #! script."""
1056 with open(fname) as f:
1057 return f.read(2).startswith('#!')
1058
1059
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001060def DownloadHooks(force):
1061 """downloads hooks
1062
1063 Args:
1064 force: True to update hooks. False to install hooks if not present.
1065 """
1066 if not settings.GetIsGerrit():
1067 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001068 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001069 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1070 if not os.access(dst, os.X_OK):
1071 if os.path.exists(dst):
1072 if not force:
1073 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001074 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001075 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001076 if not hasSheBang(dst):
1077 DieWithError('Not a script: %s\n'
1078 'You need to download from\n%s\n'
1079 'into .git/hooks/commit-msg and '
1080 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001081 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1082 except Exception:
1083 if os.path.exists(dst):
1084 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001085 DieWithError('\nFailed to download hooks.\n'
1086 'You need to download from\n%s\n'
1087 'into .git/hooks/commit-msg and '
1088 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001089
1090
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001091@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001093 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001094
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001095 parser.add_option('--activate-update', action='store_true',
1096 help='activate auto-updating [rietveld] section in '
1097 '.git/config')
1098 parser.add_option('--deactivate-update', action='store_true',
1099 help='deactivate auto-updating [rietveld] section in '
1100 '.git/config')
1101 options, args = parser.parse_args(args)
1102
1103 if options.deactivate_update:
1104 RunGit(['config', 'rietveld.autoupdate', 'false'])
1105 return
1106
1107 if options.activate_update:
1108 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1109 return
1110
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001111 if len(args) == 0:
1112 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001113 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001114 return 0
1115
1116 url = args[0]
1117 if not url.endswith('codereview.settings'):
1118 url = os.path.join(url, 'codereview.settings')
1119
1120 # Load code review settings and download hooks (if available).
1121 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001122 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001123 return 0
1124
1125
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001126def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001127 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001128 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1129 branch = ShortBranchName(branchref)
1130 _, args = parser.parse_args(args)
1131 if not args:
1132 print("Current base-url:")
1133 return RunGit(['config', 'branch.%s.base-url' % branch],
1134 error_ok=False).strip()
1135 else:
1136 print("Setting base-url to %s" % args[0])
1137 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1138 error_ok=False).strip()
1139
1140
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001142 """Show status of changelists.
1143
1144 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001145 - Red not sent for review or broken
1146 - Blue waiting for review
1147 - Yellow waiting for you to reply to review
1148 - Green LGTM'ed
1149 - Magenta in the commit queue
1150 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001151
1152 Also see 'git cl comments'.
1153 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 parser.add_option('--field',
1155 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001156 parser.add_option('-f', '--fast', action='store_true',
1157 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001158 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001159 if args:
1160 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001161
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001162 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001163 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001164 if options.field.startswith('desc'):
1165 print cl.GetDescription()
1166 elif options.field == 'id':
1167 issueid = cl.GetIssue()
1168 if issueid:
1169 print issueid
1170 elif options.field == 'patch':
1171 patchset = cl.GetPatchset()
1172 if patchset:
1173 print patchset
1174 elif options.field == 'url':
1175 url = cl.GetIssueURL()
1176 if url:
1177 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001178 return 0
1179
1180 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1181 if not branches:
1182 print('No local branch found.')
1183 return 0
1184
1185 changes = (Changelist(branchref=b) for b in branches.splitlines())
1186 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1187 alignment = max(5, max(len(b) for b in branches))
1188 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001189 # Adhoc thread pool to request data concurrently.
1190 output = Queue.Queue()
1191
1192 # Silence upload.py otherwise it becomes unweldly.
1193 upload.verbosity = 0
1194
1195 if not options.fast:
1196 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001197 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001198 c = Changelist(branchref=b)
1199 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001200 props = {}
1201 r = None
1202 if i:
1203 try:
1204 props = c.GetIssueProperties()
1205 r = c.GetApprovingReviewers() if i else None
1206 except urllib2.HTTPError:
1207 # The issue probably doesn't exist anymore.
1208 i += ' (broken)'
1209
1210 msgs = props.get('messages') or []
1211
1212 if not i:
1213 color = Fore.WHITE
1214 elif props.get('closed'):
1215 # Issue is closed.
1216 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001217 elif props.get('commit'):
1218 # Issue is in the commit queue.
1219 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001220 elif r:
1221 # Was LGTM'ed.
1222 color = Fore.GREEN
1223 elif not msgs:
1224 # No message was sent.
1225 color = Fore.RED
1226 elif msgs[-1]['sender'] != props.get('owner_email'):
1227 color = Fore.YELLOW
1228 else:
1229 color = Fore.BLUE
1230 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001231
1232 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1233 for t in threads:
1234 t.daemon = True
1235 t.start()
1236 else:
1237 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1238 for b in branches:
1239 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001240 url = c.GetIssueURL()
1241 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001242
1243 tmp = {}
1244 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001245 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001246 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001247 b, i, color = output.get()
1248 tmp[b] = (i, color)
1249 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001250 reset = Fore.RESET
1251 if not sys.stdout.isatty():
1252 color = ''
1253 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001254 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001255 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001256
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001257 cl = Changelist()
1258 print
1259 print 'Current branch:',
1260 if not cl.GetIssue():
1261 print 'no issue assigned.'
1262 return 0
1263 print cl.GetBranch()
1264 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1265 print 'Issue description:'
1266 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267 return 0
1268
1269
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001270def colorize_CMDstatus_doc():
1271 """To be called once in main() to add colors to git cl status help."""
1272 colors = [i for i in dir(Fore) if i[0].isupper()]
1273
1274 def colorize_line(line):
1275 for color in colors:
1276 if color in line.upper():
1277 # Extract whitespaces first and the leading '-'.
1278 indent = len(line) - len(line.lstrip(' ')) + 1
1279 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1280 return line
1281
1282 lines = CMDstatus.__doc__.splitlines()
1283 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1284
1285
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001286@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001287def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001288 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001289
1290 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001291 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001292 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293
1294 cl = Changelist()
1295 if len(args) > 0:
1296 try:
1297 issue = int(args[0])
1298 except ValueError:
1299 DieWithError('Pass a number to set the issue or none to list it.\n'
1300 'Maybe you want to run git cl status?')
1301 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001302 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303 return 0
1304
1305
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001306def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001307 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001308 (_, args) = parser.parse_args(args)
1309 if args:
1310 parser.error('Unsupported argument: %s' % args)
1311
1312 cl = Changelist()
1313 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001314 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001315 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001316 if message['disapproval']:
1317 color = Fore.RED
1318 elif message['approval']:
1319 color = Fore.GREEN
1320 elif message['sender'] == data['owner_email']:
1321 color = Fore.MAGENTA
1322 else:
1323 color = Fore.BLUE
1324 print '\n%s%s %s%s' % (
1325 color, message['date'].split('.', 1)[0], message['sender'],
1326 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001327 if message['text'].strip():
1328 print '\n'.join(' ' + l for l in message['text'].splitlines())
1329 return 0
1330
1331
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001332def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001333 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001334 cl = Changelist()
1335 if not cl.GetIssue():
1336 DieWithError('This branch has no associated changelist.')
1337 description = ChangeDescription(cl.GetDescription())
1338 description.prompt()
1339 cl.UpdateDescription(description.description)
1340 return 0
1341
1342
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001343def CreateDescriptionFromLog(args):
1344 """Pulls out the commit log to use as a base for the CL description."""
1345 log_args = []
1346 if len(args) == 1 and not args[0].endswith('.'):
1347 log_args = [args[0] + '..']
1348 elif len(args) == 1 and args[0].endswith('...'):
1349 log_args = [args[0][:-1]]
1350 elif len(args) == 2:
1351 log_args = [args[0] + '..' + args[1]]
1352 else:
1353 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001354 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355
1356
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001357def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001358 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001359 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001361 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001362 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001363 (options, args) = parser.parse_args(args)
1364
ukai@chromium.org259e4682012-10-25 07:36:33 +00001365 if not options.force and is_dirty_git_tree('presubmit'):
1366 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001367 return 1
1368
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001369 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001370 if args:
1371 base_branch = args[0]
1372 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001373 # Default to diffing against the common ancestor of the upstream branch.
1374 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001375
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001376 cl.RunHook(
1377 committing=not options.upload,
1378 may_prompt=False,
1379 verbose=options.verbose,
1380 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001381 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001382
1383
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001384def AddChangeIdToCommitMessage(options, args):
1385 """Re-commits using the current message, assumes the commit hook is in
1386 place.
1387 """
1388 log_desc = options.message or CreateDescriptionFromLog(args)
1389 git_command = ['commit', '--amend', '-m', log_desc]
1390 RunGit(git_command)
1391 new_log_desc = CreateDescriptionFromLog(args)
1392 if CHANGE_ID in new_log_desc:
1393 print 'git-cl: Added Change-Id to commit message.'
1394 else:
1395 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1396
1397
ukai@chromium.orge8077812012-02-03 03:41:46 +00001398def GerritUpload(options, args, cl):
1399 """upload the current branch to gerrit."""
1400 # We assume the remote called "origin" is the one we want.
1401 # It is probably not worthwhile to support different workflows.
1402 remote = 'origin'
1403 branch = 'master'
1404 if options.target_branch:
1405 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001406
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001407 change_desc = ChangeDescription(
1408 options.message or CreateDescriptionFromLog(args))
1409 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001410 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001412 if CHANGE_ID not in change_desc.description:
1413 AddChangeIdToCommitMessage(options, args)
1414 if options.reviewers:
1415 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416
ukai@chromium.orge8077812012-02-03 03:41:46 +00001417 receive_options = []
1418 cc = cl.GetCCList().split(',')
1419 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001420 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001421 cc = filter(None, cc)
1422 if cc:
1423 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001424 if change_desc.get_reviewers():
1425 receive_options.extend(
1426 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427
ukai@chromium.orge8077812012-02-03 03:41:46 +00001428 git_command = ['push']
1429 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001430 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001431 ' '.join(receive_options))
1432 git_command += [remote, 'HEAD:refs/for/' + branch]
1433 RunGit(git_command)
1434 # TODO(ukai): parse Change-Id: and set issue number?
1435 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001436
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001437
ukai@chromium.orge8077812012-02-03 03:41:46 +00001438def RietveldUpload(options, args, cl):
1439 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1441 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001442 if options.emulate_svn_auto_props:
1443 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001444
1445 change_desc = None
1446
pgervais@chromium.org91141372014-01-09 23:27:20 +00001447 if options.email is not None:
1448 upload_args.extend(['--email', options.email])
1449
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001450 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001451 if options.title:
1452 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001453 if options.message:
1454 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001455 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001456 print ("This branch is associated with issue %s. "
1457 "Adding patch to that issue." % cl.GetIssue())
1458 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001459 if options.title:
1460 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001461 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001462 change_desc = ChangeDescription(message)
1463 if options.reviewers:
1464 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001465 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001466 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001467
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001468 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001469 print "Description is empty; aborting."
1470 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001471
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001472 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001473 if change_desc.get_reviewers():
1474 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001475 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001476 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001477 DieWithError("Must specify reviewers to send email.")
1478 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001479
1480 # We check this before applying rietveld.private assuming that in
1481 # rietveld.cc only addresses which we can send private CLs to are listed
1482 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1483 # --private is specified explicitly on the command line.
1484 if options.private:
1485 logging.warn('rietveld.cc is ignored since private flag is specified. '
1486 'You need to review and add them manually if necessary.')
1487 cc = cl.GetCCListWithoutDefault()
1488 else:
1489 cc = cl.GetCCList()
1490 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001491 if cc:
1492 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001493
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001494 if options.private or settings.GetDefaultPrivateFlag() == "True":
1495 upload_args.append('--private')
1496
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001497 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001498 if not options.find_copies:
1499 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001500
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001501 # Include the upstream repo's URL in the change -- this is useful for
1502 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001503 remote_url = cl.GetGitBaseUrlFromConfig()
1504 if not remote_url:
1505 if settings.GetIsGitSvn():
1506 # URL is dependent on the current directory.
1507 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1508 if data:
1509 keys = dict(line.split(': ', 1) for line in data.splitlines()
1510 if ': ' in line)
1511 remote_url = keys.get('URL', None)
1512 else:
1513 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1514 remote_url = (cl.GetRemoteUrl() + '@'
1515 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001516 if remote_url:
1517 upload_args.extend(['--base_url', remote_url])
1518
1519 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001520 upload_args = ['upload'] + upload_args + args
1521 logging.info('upload.RealMain(%s)', upload_args)
1522 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001523 issue = int(issue)
1524 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001525 except KeyboardInterrupt:
1526 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001527 except:
1528 # If we got an exception after the user typed a description for their
1529 # change, back up the description before re-raising.
1530 if change_desc:
1531 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1532 print '\nGot exception while uploading -- saving description to %s\n' \
1533 % backup_path
1534 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001535 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001536 backup_file.close()
1537 raise
1538
1539 if not cl.GetIssue():
1540 cl.SetIssue(issue)
1541 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001542
1543 if options.use_commit_queue:
1544 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001545 return 0
1546
1547
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001548def cleanup_list(l):
1549 """Fixes a list so that comma separated items are put as individual items.
1550
1551 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1552 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1553 """
1554 items = sum((i.split(',') for i in l), [])
1555 stripped_items = (i.strip() for i in items)
1556 return sorted(filter(None, stripped_items))
1557
1558
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001559@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001560def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001561 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001562 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1563 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001564 parser.add_option('--bypass-watchlists', action='store_true',
1565 dest='bypass_watchlists',
1566 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001567 parser.add_option('-f', action='store_true', dest='force',
1568 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001569 parser.add_option('-m', dest='message', help='message for patchset')
1570 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001571 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001572 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001573 help='reviewer email addresses')
1574 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001575 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001576 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001577 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001578 help='send email to reviewer immediately')
1579 parser.add_option("--emulate_svn_auto_props", action="store_true",
1580 dest="emulate_svn_auto_props",
1581 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001582 parser.add_option('-c', '--use-commit-queue', action='store_true',
1583 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001584 parser.add_option('--private', action='store_true',
1585 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001586 parser.add_option('--target_branch',
1587 help='When uploading to gerrit, remote branch to '
1588 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001589 parser.add_option('--email', default=None,
1590 help='email address to use to connect to Rietveld')
1591
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001592 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001593 (options, args) = parser.parse_args(args)
1594
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001595 if options.target_branch and not settings.GetIsGerrit():
1596 parser.error('Use --target_branch for non gerrit repository.')
1597
ukai@chromium.org259e4682012-10-25 07:36:33 +00001598 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001599 return 1
1600
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001601 options.reviewers = cleanup_list(options.reviewers)
1602 options.cc = cleanup_list(options.cc)
1603
ukai@chromium.orge8077812012-02-03 03:41:46 +00001604 cl = Changelist()
1605 if args:
1606 # TODO(ukai): is it ok for gerrit case?
1607 base_branch = args[0]
1608 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001609 # Default to diffing against common ancestor of upstream branch
1610 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001611 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001612
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001613 # Apply watchlists on upload.
1614 change = cl.GetChange(base_branch, None)
1615 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1616 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001617 if not options.bypass_watchlists:
1618 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001619
ukai@chromium.orge8077812012-02-03 03:41:46 +00001620 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001621 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001622 may_prompt=not options.force,
1623 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001624 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001625 if not hook_results.should_continue():
1626 return 1
1627 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001628 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001629
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001630 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001631 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001632 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001633 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001634 print ('The last upload made from this repository was patchset #%d but '
1635 'the most recent patchset on the server is #%d.'
1636 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001637 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1638 'from another machine or branch the patch you\'re uploading now '
1639 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001640 ask_for_data('About to upload; enter to confirm.')
1641
iannucci@chromium.org79540052012-10-19 23:15:26 +00001642 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001643 if settings.GetIsGerrit():
1644 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001645 ret = RietveldUpload(options, args, cl)
1646 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001647 git_set_branch_value('last-upload-hash',
1648 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001649
1650 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001651
1652
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001653def IsSubmoduleMergeCommit(ref):
1654 # When submodules are added to the repo, we expect there to be a single
1655 # non-git-svn merge commit at remote HEAD with a signature comment.
1656 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001657 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001658 return RunGit(cmd) != ''
1659
1660
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001661def SendUpstream(parser, args, cmd):
1662 """Common code for CmdPush and CmdDCommit
1663
1664 Squashed commit into a single.
1665 Updates changelog with metadata (e.g. pointer to review).
1666 Pushes/dcommits the code upstream.
1667 Updates review and closes.
1668 """
1669 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1670 help='bypass upload presubmit hook')
1671 parser.add_option('-m', dest='message',
1672 help="override review description")
1673 parser.add_option('-f', action='store_true', dest='force',
1674 help="force yes to questions (don't prompt)")
1675 parser.add_option('-c', dest='contributor',
1676 help="external contributor for patch (appended to " +
1677 "description and used as author for git). Should be " +
1678 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001679 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001680 (options, args) = parser.parse_args(args)
1681 cl = Changelist()
1682
1683 if not args or cmd == 'push':
1684 # Default to merging against our best guess of the upstream branch.
1685 args = [cl.GetUpstreamBranch()]
1686
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001687 if options.contributor:
1688 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1689 print "Please provide contibutor as 'First Last <email@example.com>'"
1690 return 1
1691
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001692 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001693 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001694
ukai@chromium.org259e4682012-10-25 07:36:33 +00001695 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001696 return 1
1697
1698 # This rev-list syntax means "show all commits not in my branch that
1699 # are in base_branch".
1700 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1701 base_branch]).splitlines()
1702 if upstream_commits:
1703 print ('Base branch "%s" has %d commits '
1704 'not in this branch.' % (base_branch, len(upstream_commits)))
1705 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1706 return 1
1707
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001708 # This is the revision `svn dcommit` will commit on top of.
1709 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1710 '--pretty=format:%H'])
1711
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001712 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001713 # If the base_head is a submodule merge commit, the first parent of the
1714 # base_head should be a git-svn commit, which is what we're interested in.
1715 base_svn_head = base_branch
1716 if base_has_submodules:
1717 base_svn_head += '^1'
1718
1719 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001720 if extra_commits:
1721 print ('This branch has %d additional commits not upstreamed yet.'
1722 % len(extra_commits.splitlines()))
1723 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1724 'before attempting to %s.' % (base_branch, cmd))
1725 return 1
1726
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001727 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001728 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001729 author = None
1730 if options.contributor:
1731 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001732 hook_results = cl.RunHook(
1733 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001734 may_prompt=not options.force,
1735 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001736 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001737 if not hook_results.should_continue():
1738 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001739
1740 if cmd == 'dcommit':
1741 # Check the tree status if the tree status URL is set.
1742 status = GetTreeStatus()
1743 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001744 print('The tree is closed. Please wait for it to reopen. Use '
1745 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001746 return 1
1747 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001748 print('Unable to determine tree status. Please verify manually and '
1749 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001750 else:
1751 breakpad.SendStack(
1752 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001753 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1754 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001755 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001756
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001757 change_desc = ChangeDescription(options.message)
1758 if not change_desc.description and cl.GetIssue():
1759 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001760
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001761 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001762 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001763 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001764 else:
1765 print 'No description set.'
1766 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1767 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001768
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001769 # Keep a separate copy for the commit message, because the commit message
1770 # contains the link to the Rietveld issue, while the Rietveld message contains
1771 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001772 # Keep a separate copy for the commit message.
1773 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001774 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001775
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001776 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001777 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001778 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001779 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001780 commit_desc.append_footer('Patch from %s.' % options.contributor)
1781
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001782 print('Description:')
1783 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001784
1785 branches = [base_branch, cl.GetBranchRef()]
1786 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001787 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001788 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001789
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001790 # We want to squash all this branch's commits into one commit with the proper
1791 # description. We do this by doing a "reset --soft" to the base branch (which
1792 # keeps the working copy the same), then dcommitting that. If origin/master
1793 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1794 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001795 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001796 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1797 # Delete the branches if they exist.
1798 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1799 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1800 result = RunGitWithCode(showref_cmd)
1801 if result[0] == 0:
1802 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001803
1804 # We might be in a directory that's present in this branch but not in the
1805 # trunk. Move up to the top of the tree so that git commands that expect a
1806 # valid CWD won't fail after we check out the merge branch.
1807 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1808 if rel_base_path:
1809 os.chdir(rel_base_path)
1810
1811 # Stuff our change into the merge branch.
1812 # We wrap in a try...finally block so if anything goes wrong,
1813 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001814 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001815 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001816 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1817 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001818 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001819 RunGit(
1820 [
1821 'commit', '--author', options.contributor,
1822 '-m', commit_desc.description,
1823 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001824 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001825 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001826 if base_has_submodules:
1827 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1828 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1829 RunGit(['checkout', CHERRY_PICK_BRANCH])
1830 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001831 if cmd == 'push':
1832 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001833 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001834 retcode, output = RunGitWithCode(
1835 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1836 logging.debug(output)
1837 else:
1838 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001839 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001840 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001841 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001842 finally:
1843 # And then swap back to the original branch and clean up.
1844 RunGit(['checkout', '-q', cl.GetBranch()])
1845 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001846 if base_has_submodules:
1847 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001848
1849 if cl.GetIssue():
1850 if cmd == 'dcommit' and 'Committed r' in output:
1851 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1852 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001853 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1854 for l in output.splitlines(False))
1855 match = filter(None, match)
1856 if len(match) != 1:
1857 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1858 output)
1859 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001860 else:
1861 return 1
1862 viewvc_url = settings.GetViewVCUrl()
1863 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001864 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001865 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001866 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001867 print ('Closing issue '
1868 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001869 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001870 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001871 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001872 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001873 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001874 if options.bypass_hooks:
1875 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
1876 else:
1877 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001878 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001879 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001880
1881 if retcode == 0:
1882 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1883 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001884 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001885
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001886 return 0
1887
1888
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001889@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001890def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001891 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001892 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001893 message = """This doesn't appear to be an SVN repository.
1894If your project has a git mirror with an upstream SVN master, you probably need
1895to run 'git svn init', see your project's git mirror documentation.
1896If your project has a true writeable upstream repository, you probably want
1897to run 'git cl push' instead.
1898Choose wisely, if you get this wrong, your commit might appear to succeed but
1899will instead be silently ignored."""
1900 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001901 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001902 return SendUpstream(parser, args, 'dcommit')
1903
1904
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001905@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001906def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001907 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001908 if settings.GetIsGitSvn():
1909 print('This appears to be an SVN repository.')
1910 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001911 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001912 return SendUpstream(parser, args, 'push')
1913
1914
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001915@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001916def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00001917 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001918 parser.add_option('-b', dest='newbranch',
1919 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001920 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001921 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001922 parser.add_option('-d', '--directory', action='store', metavar='DIR',
1923 help='Change to the directory DIR immediately, '
1924 'before doing anything else.')
1925 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001926 help='failed patches spew .rej files rather than '
1927 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001928 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1929 help="don't commit after patch applies")
1930 (options, args) = parser.parse_args(args)
1931 if len(args) != 1:
1932 parser.print_help()
1933 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001934 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001935
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001936 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001937 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001938
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001939 if options.newbranch:
1940 if options.force:
1941 RunGit(['branch', '-D', options.newbranch],
1942 stderr=subprocess2.PIPE, error_ok=True)
1943 RunGit(['checkout', '-b', options.newbranch,
1944 Changelist().GetUpstreamBranch()])
1945
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001946 return PatchIssue(issue_arg, options.reject, options.nocommit,
1947 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001948
1949
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001950def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001951 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001952 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001953 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001954 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001955 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001956 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001957 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001958 # Assume it's a URL to the patch. Default to https.
1959 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001960 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001961 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001962 DieWithError('Must pass an issue ID or full URL for '
1963 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001964 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001965 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001966 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001967
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001968 # Switch up to the top-level directory, if necessary, in preparation for
1969 # applying the patch.
1970 top = RunGit(['rev-parse', '--show-cdup']).strip()
1971 if top:
1972 os.chdir(top)
1973
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001974 # Git patches have a/ at the beginning of source paths. We strip that out
1975 # with a sed script rather than the -p flag to patch so we can feed either
1976 # Git or svn-style patches into the same apply command.
1977 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001978 try:
1979 patch_data = subprocess2.check_output(
1980 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1981 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001982 DieWithError('Git patch mungling failed.')
1983 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001984 env = os.environ.copy()
1985 # 'cat' is a magical git string that disables pagers on all platforms.
1986 env['GIT_PAGER'] = 'cat'
1987
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001988 # We use "git apply" to apply the patch instead of "patch" so that we can
1989 # pick up file adds.
1990 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001991 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001992 if directory:
1993 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001994 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001995 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001996 elif IsGitVersionAtLeast('1.7.12'):
1997 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001998 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001999 subprocess2.check_call(cmd, env=env,
2000 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002001 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002002 DieWithError('Failed to apply the patch')
2003
2004 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002005 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002006 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2007 cl = Changelist()
2008 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002009 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002010 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002011 else:
2012 print "Patch applied to index."
2013 return 0
2014
2015
2016def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002017 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002018 # Provide a wrapper for git svn rebase to help avoid accidental
2019 # git svn dcommit.
2020 # It's the only command that doesn't use parser at all since we just defer
2021 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002022 env = os.environ.copy()
2023 # 'cat' is a magical git string that disables pagers on all platforms.
2024 env['GIT_PAGER'] = 'cat'
2025
2026 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002027
2028
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002029def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002030 """Fetches the tree status and returns either 'open', 'closed',
2031 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002032 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002033 if url:
2034 status = urllib2.urlopen(url).read().lower()
2035 if status.find('closed') != -1 or status == '0':
2036 return 'closed'
2037 elif status.find('open') != -1 or status == '1':
2038 return 'open'
2039 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002040 return 'unset'
2041
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002042
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002043def GetTreeStatusReason():
2044 """Fetches the tree status from a json url and returns the message
2045 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002046 url = settings.GetTreeStatusUrl()
2047 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002048 connection = urllib2.urlopen(json_url)
2049 status = json.loads(connection.read())
2050 connection.close()
2051 return status['message']
2052
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002053
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002054def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002055 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002056 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002057 status = GetTreeStatus()
2058 if 'unset' == status:
2059 print 'You must configure your tree status URL by running "git cl config".'
2060 return 2
2061
2062 print "The tree is %s" % status
2063 print
2064 print GetTreeStatusReason()
2065 if status != 'open':
2066 return 1
2067 return 0
2068
2069
maruel@chromium.org15192402012-09-06 12:38:29 +00002070def CMDtry(parser, args):
2071 """Triggers a try job through Rietveld."""
2072 group = optparse.OptionGroup(parser, "Try job options")
2073 group.add_option(
2074 "-b", "--bot", action="append",
2075 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2076 "times to specify multiple builders. ex: "
2077 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2078 "the try server waterfall for the builders name and the tests "
2079 "available. Can also be used to specify gtest_filter, e.g. "
2080 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2081 group.add_option(
2082 "-r", "--revision",
2083 help="Revision to use for the try job; default: the "
2084 "revision will be determined by the try server; see "
2085 "its waterfall for more info")
2086 group.add_option(
2087 "-c", "--clobber", action="store_true", default=False,
2088 help="Force a clobber before building; e.g. don't do an "
2089 "incremental build")
2090 group.add_option(
2091 "--project",
2092 help="Override which project to use. Projects are defined "
2093 "server-side to define what default bot set to use")
2094 group.add_option(
2095 "-t", "--testfilter", action="append", default=[],
2096 help=("Apply a testfilter to all the selected builders. Unless the "
2097 "builders configurations are similar, use multiple "
2098 "--bot <builder>:<test> arguments."))
2099 group.add_option(
2100 "-n", "--name", help="Try job name; default to current branch name")
2101 parser.add_option_group(group)
2102 options, args = parser.parse_args(args)
2103
2104 if args:
2105 parser.error('Unknown arguments: %s' % args)
2106
2107 cl = Changelist()
2108 if not cl.GetIssue():
2109 parser.error('Need to upload first')
2110
2111 if not options.name:
2112 options.name = cl.GetBranch()
2113
2114 # Process --bot and --testfilter.
2115 if not options.bot:
2116 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002117 change = cl.GetChange(
2118 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
2119 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002120 options.bot = presubmit_support.DoGetTrySlaves(
2121 change,
2122 change.LocalPaths(),
2123 settings.GetRoot(),
2124 None,
2125 None,
2126 options.verbose,
2127 sys.stdout)
2128 if not options.bot:
2129 parser.error('No default try builder to try, use --bot')
2130
2131 builders_and_tests = {}
stip@chromium.org43064fd2013-12-18 20:07:44 +00002132 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2133 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2134
2135 for bot in old_style:
maruel@chromium.org15192402012-09-06 12:38:29 +00002136 if ':' in bot:
2137 builder, tests = bot.split(':', 1)
2138 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2139 elif ',' in bot:
2140 parser.error('Specify one bot per --bot flag')
2141 else:
2142 builders_and_tests.setdefault(bot, []).append('defaulttests')
2143
stip@chromium.org43064fd2013-12-18 20:07:44 +00002144 for bot, tests in new_style:
2145 builders_and_tests.setdefault(bot, []).extend(tests)
2146
maruel@chromium.org15192402012-09-06 12:38:29 +00002147 if options.testfilter:
2148 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2149 builders_and_tests = dict(
2150 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2151 if t != ['compile'])
2152
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002153 if any('triggered' in b for b in builders_and_tests):
2154 print >> sys.stderr, (
2155 'ERROR You are trying to send a job to a triggered bot. This type of'
2156 ' bot requires an\ninitial job from a parent (usually a builder). '
2157 'Instead send your job to the parent.\n'
2158 'Bot list: %s' % builders_and_tests)
2159 return 1
2160
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002161 patchset = cl.GetMostRecentPatchset()
2162 if patchset and patchset != cl.GetPatchset():
2163 print(
2164 '\nWARNING Mismatch between local config and server. Did a previous '
2165 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2166 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002167 try:
2168 cl.RpcServer().trigger_try_jobs(
2169 cl.GetIssue(), patchset, options.name, options.clobber,
2170 options.revision, builders_and_tests)
2171 except urllib2.HTTPError, e:
2172 if e.code == 404:
2173 print('404 from rietveld; '
2174 'did you mean to use "git try" instead of "git cl try"?')
2175 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002176 print('Tried jobs on:')
2177 length = max(len(builder) for builder in builders_and_tests)
2178 for builder in sorted(builders_and_tests):
2179 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002180 return 0
2181
2182
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002183@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002184def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002185 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002186 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002187 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002188 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002189
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002190 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002191 if args:
2192 # One arg means set upstream branch.
2193 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2194 cl = Changelist()
2195 print "Upstream branch set to " + cl.GetUpstreamBranch()
2196 else:
2197 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002198 return 0
2199
2200
thestig@chromium.org00858c82013-12-02 23:08:03 +00002201def CMDweb(parser, args):
2202 """Opens the current CL in the web browser."""
2203 _, args = parser.parse_args(args)
2204 if args:
2205 parser.error('Unrecognized args: %s' % ' '.join(args))
2206
2207 issue_url = Changelist().GetIssueURL()
2208 if not issue_url:
2209 print >> sys.stderr, 'ERROR No issue to open'
2210 return 1
2211
2212 webbrowser.open(issue_url)
2213 return 0
2214
2215
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002216def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002217 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002218 _, args = parser.parse_args(args)
2219 if args:
2220 parser.error('Unrecognized args: %s' % ' '.join(args))
2221 cl = Changelist()
2222 cl.SetFlag('commit', '1')
2223 return 0
2224
2225
groby@chromium.org411034a2013-02-26 15:12:01 +00002226def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002227 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002228 _, args = parser.parse_args(args)
2229 if args:
2230 parser.error('Unrecognized args: %s' % ' '.join(args))
2231 cl = Changelist()
2232 # Ensure there actually is an issue to close.
2233 cl.GetDescription()
2234 cl.CloseIssue()
2235 return 0
2236
2237
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002238def CMDdiff(parser, args):
2239 """shows differences between local tree and last upload."""
2240 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002241 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002242 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002243 if not issue:
2244 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002245 TMP_BRANCH = 'git-cl-diff'
2246 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2247
2248 # Create a new branch based on the merge-base
2249 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2250 try:
2251 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002252 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002253 if rtn != 0:
2254 return rtn
2255
2256 # Switch back to starting brand and diff against the temporary
2257 # branch containing the latest rietveld patch.
2258 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2259 finally:
2260 RunGit(['checkout', '-q', branch])
2261 RunGit(['branch', '-D', TMP_BRANCH])
2262
2263 return 0
2264
2265
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002266def CMDowners(parser, args):
2267 """interactively find the owners for reviewing"""
2268 parser.add_option(
2269 '--no-color',
2270 action='store_true',
2271 help='Use this option to disable color output')
2272 options, args = parser.parse_args(args)
2273
2274 author = RunGit(['config', 'user.email']).strip() or None
2275
2276 cl = Changelist()
2277
2278 if args:
2279 if len(args) > 1:
2280 parser.error('Unknown args')
2281 base_branch = args[0]
2282 else:
2283 # Default to diffing against the common ancestor of the upstream branch.
2284 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2285
2286 change = cl.GetChange(base_branch, None)
2287 return owners_finder.OwnersFinder(
2288 [f.LocalPath() for f in
2289 cl.GetChange(base_branch, None).AffectedFiles()],
2290 change.RepositoryRoot(), author,
2291 fopen=file, os_path=os.path, glob=glob.glob,
2292 disable_color=options.no_color).run()
2293
2294
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002295def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002296 """Runs clang-format on the diff."""
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002297 CLANG_EXTS = ['.cc', '.cpp', '.h']
2298 parser.add_option('--full', action='store_true', default=False)
2299 opts, args = parser.parse_args(args)
2300 if args:
2301 parser.error('Unrecognized args: %s' % ' '.join(args))
2302
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002303 # git diff generates paths against the root of the repository. Change
2304 # to that directory so clang-format can find files even within subdirs.
2305 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
2306 if rel_base_path:
2307 os.chdir(rel_base_path)
2308
digit@chromium.org29e47272013-05-17 17:01:46 +00002309 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002310 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002311 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002312 # Only list the names of modified files.
2313 diff_cmd.append('--name-only')
2314 else:
2315 # Only generate context-less patches.
2316 diff_cmd.append('-U0')
2317
2318 # Grab the merge-base commit, i.e. the upstream commit of the current
2319 # branch when it was created or the last time it was rebased. This is
2320 # to cover the case where the user may have called "git fetch origin",
2321 # moving the origin branch to a newer commit, but hasn't rebased yet.
2322 upstream_commit = None
2323 cl = Changelist()
2324 upstream_branch = cl.GetUpstreamBranch()
2325 if upstream_branch:
2326 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2327 upstream_commit = upstream_commit.strip()
2328
2329 if not upstream_commit:
2330 DieWithError('Could not find base commit for this branch. '
2331 'Are you in detached state?')
2332
2333 diff_cmd.append(upstream_commit)
2334
2335 # Handle source file filtering.
2336 diff_cmd.append('--')
2337 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2338 diff_output = RunGit(diff_cmd)
2339
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002340 top_dir = RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n')
2341
digit@chromium.org29e47272013-05-17 17:01:46 +00002342 if opts.full:
2343 # diff_output is a list of files to send to clang-format.
2344 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002345 if not files:
2346 print "Nothing to format."
2347 return 0
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002348 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files,
2349 cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002350 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002351 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002352 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2353 'clang-format-diff.py')
2354 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002355 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
adam.treat@samsung.com16d2ad62013-09-27 14:54:04 +00002356 cmd = [sys.executable, cfd_path, '-p0', '-style', 'Chromium']
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002357
2358 # Newer versions of clang-format-diff.py require an explicit -i flag
2359 # to apply the edits to files, otherwise it just displays a diff.
2360 # Probe the usage string to verify if this is needed.
2361 help_text = RunCommand([sys.executable, cfd_path, '-h'])
2362 if '[-i]' in help_text:
2363 cmd.append('-i')
2364
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002365 RunCommand(cmd, stdin=diff_output, cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002366
2367 return 0
2368
2369
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002370class OptionParser(optparse.OptionParser):
2371 """Creates the option parse and add --verbose support."""
2372 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002373 optparse.OptionParser.__init__(
2374 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002375 self.add_option(
2376 '-v', '--verbose', action='count', default=0,
2377 help='Use 2 times for more debugging info')
2378
2379 def parse_args(self, args=None, values=None):
2380 options, args = optparse.OptionParser.parse_args(self, args, values)
2381 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2382 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2383 return options, args
2384
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002385
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002386def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002387 if sys.hexversion < 0x02060000:
2388 print >> sys.stderr, (
2389 '\nYour python version %s is unsupported, please upgrade.\n' %
2390 sys.version.split(' ', 1)[0])
2391 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002392
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002393 # Reload settings.
2394 global settings
2395 settings = Settings()
2396
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002397 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002398 dispatcher = subcommand.CommandDispatcher(__name__)
2399 try:
2400 return dispatcher.execute(OptionParser(), argv)
2401 except urllib2.HTTPError, e:
2402 if e.code != 500:
2403 raise
2404 DieWithError(
2405 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2406 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002407
2408
2409if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002410 # These affect sys.stdout so do it outside of main() to simplify mocks in
2411 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002412 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002413 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002414 sys.exit(main(sys.argv[1:]))