blob: e46b66097520ee24b77202dac0c2c6b818f66406 [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
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
18import optparse
19import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000020import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000024import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000029import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000030import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000031import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000032
33try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000034 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000035except ImportError:
36 pass
37
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000038from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000039from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000040from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000041import auth
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000043import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000044import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000045import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000046import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000047import git_common
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +000048from git_footers import get_footer_svn_id
piman@chromium.org336f9122014-09-04 02:16:55 +000049import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000050import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000051import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000052import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000053import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000054import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000055import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import watchlists
57
maruel@chromium.org0633fb42013-08-16 20:06:14 +000058__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000060DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000061POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000062DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000063GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000064CHANGE_ID = 'Change-Id:'
rmistry@google.comc68112d2015-03-03 12:48:06 +000065REFS_THAT_ALIAS_TO_OTHER_REFS = {
66 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
67 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
68}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069
sheyang@google.com6ebaf782015-05-12 19:17:54 +000070# Buildbucket-related constants
71BUILDBUCKET_HOST = 'cr-buildbucket.appspot.com'
72
thestig@chromium.org44202a22014-03-11 19:22:18 +000073# Valid extensions for files we want to lint.
74DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
75DEFAULT_LINT_IGNORE_REGEX = r"$^"
76
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000077# Shortcut since it quickly becomes redundant.
78Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000079
maruel@chromium.orgddd59412011-11-30 14:20:38 +000080# Initialized in main()
81settings = None
82
83
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000084def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000085 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086 sys.exit(1)
87
88
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000089def GetNoGitPagerEnv():
90 env = os.environ.copy()
91 # 'cat' is a magical git string that disables pagers on all platforms.
92 env['GIT_PAGER'] = 'cat'
93 return env
94
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000095
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000096def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000097 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000098 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000099 except subprocess2.CalledProcessError as e:
100 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000101 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 'Command "%s" failed.\n%s' % (
104 ' '.join(args), error_message or e.stdout or ''))
105 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000106
107
108def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000109 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000110 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000111
112
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000113def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000114 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000115 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000116 if suppress_stderr:
117 stderr = subprocess2.VOID
118 else:
119 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000120 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000121 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000122 stdout=subprocess2.PIPE,
123 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000124 return code, out[0]
125 except ValueError:
126 # When the subprocess fails, it returns None. That triggers a ValueError
127 # when trying to unpack the return value into (out, code).
128 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000129
130
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000131def RunGitSilent(args):
132 """Returns stdout, suppresses stderr and ingores the return code."""
133 return RunGitWithCode(args, suppress_stderr=True)[1]
134
135
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000136def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000137 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 return (version.startswith(prefix) and
140 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000141
142
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000143def BranchExists(branch):
144 """Return True if specified branch exists."""
145 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
146 suppress_stderr=True)
147 return not code
148
149
maruel@chromium.org90541732011-04-01 17:54:18 +0000150def ask_for_data(prompt):
151 try:
152 return raw_input(prompt)
153 except KeyboardInterrupt:
154 # Hide the exception.
155 sys.exit(1)
156
157
iannucci@chromium.org79540052012-10-19 23:15:26 +0000158def git_set_branch_value(key, value):
159 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000160 if not branch:
161 return
162
163 cmd = ['config']
164 if isinstance(value, int):
165 cmd.append('--int')
166 git_key = 'branch.%s.%s' % (branch, key)
167 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000168
169
170def git_get_branch_default(key, default):
171 branch = Changelist().GetBranch()
172 if branch:
173 git_key = 'branch.%s.%s' % (branch, key)
174 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
175 try:
176 return int(stdout.strip())
177 except ValueError:
178 pass
179 return default
180
181
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000182def add_git_similarity(parser):
183 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000184 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000185 help='Sets the percentage that a pair of files need to match in order to'
186 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000187 parser.add_option(
188 '--find-copies', action='store_true',
189 help='Allows git to look for copies.')
190 parser.add_option(
191 '--no-find-copies', action='store_false', dest='find_copies',
192 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000193
194 old_parser_args = parser.parse_args
195 def Parse(args):
196 options, args = old_parser_args(args)
197
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000198 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000199 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000200 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 print('Note: Saving similarity of %d%% in git config.'
202 % options.similarity)
203 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000204
iannucci@chromium.org79540052012-10-19 23:15:26 +0000205 options.similarity = max(0, min(options.similarity, 100))
206
207 if options.find_copies is None:
208 options.find_copies = bool(
209 git_get_branch_default('git-find-copies', True))
210 else:
211 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000212
213 print('Using %d%% similarity for rename/copy detection. '
214 'Override with --similarity.' % options.similarity)
215
216 return options, args
217 parser.parse_args = Parse
218
219
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000220def _prefix_master(master):
221 """Convert user-specified master name to full master name.
222
223 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
224 name, while the developers always use shortened master name
225 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
226 function does the conversion for buildbucket migration.
227 """
228 prefix = 'master.'
229 if master.startswith(prefix):
230 return master
231 return '%s%s' % (prefix, master)
232
233
machenbach@chromium.org79e43ff2015-05-15 05:56:13 +0000234def trigger_try_jobs(auth_config, changelist, options, masters, category,
235 override_properties=None):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000236 rietveld_url = settings.GetDefaultServerUrl()
237 rietveld_host = urlparse.urlparse(rietveld_url).hostname
238 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
239 http = authenticator.authorize(httplib2.Http())
240 http.force_exception_to_status_code = True
241 issue_props = changelist.GetIssueProperties()
242 issue = changelist.GetIssue()
243 patchset = changelist.GetMostRecentPatchset()
244
245 buildbucket_put_url = (
246 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
247 hostname=BUILDBUCKET_HOST))
248 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
249 hostname=rietveld_host,
250 issue=issue,
251 patch=patchset)
252
253 batch_req_body = {'builds': []}
254 print_text = []
255 print_text.append('Tried jobs on:')
256 for master, builders_and_tests in sorted(masters.iteritems()):
257 print_text.append('Master: %s' % master)
258 bucket = _prefix_master(master)
259 for builder, tests in sorted(builders_and_tests.iteritems()):
260 print_text.append(' %s: %s' % (builder, tests))
261 parameters = {
262 'builder_name': builder,
263 'changes': [
264 {'author': {'email': issue_props['owner_email']}},
265 ],
266 'properties': {
267 'category': category,
268 'issue': issue,
269 'master': master,
270 'patch_project': issue_props['project'],
271 'patch_storage': 'rietveld',
272 'patchset': patchset,
273 'reason': options.name,
274 'revision': options.revision,
275 'rietveld': rietveld_url,
276 'testfilter': tests,
277 },
278 }
machenbach@chromium.org79e43ff2015-05-15 05:56:13 +0000279 if override_properties:
280 parameters['properties'].update(override_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000281 if options.clobber:
282 parameters['properties']['clobber'] = True
283 batch_req_body['builds'].append(
284 {
285 'bucket': bucket,
286 'parameters_json': json.dumps(parameters),
287 'tags': ['builder:%s' % builder,
288 'buildset:%s' % buildset,
289 'master:%s' % master,
290 'user_agent:git_cl_try']
291 }
292 )
293
294 for try_count in xrange(3):
295 response, content = http.request(
296 buildbucket_put_url,
297 'PUT',
298 body=json.dumps(batch_req_body),
299 headers={'Content-Type': 'application/json'},
300 )
301 content_json = None
302 try:
303 content_json = json.loads(content)
304 except ValueError:
305 pass
306
307 # Buildbucket could return an error even if status==200.
308 if content_json and content_json.get('error'):
309 msg = 'Error in response. Code: %d. Reason: %s. Message: %s.' % (
310 content_json['error'].get('code', ''),
311 content_json['error'].get('reason', ''),
312 content_json['error'].get('message', ''))
313 raise BuildbucketResponseException(msg)
314
315 if response.status == 200:
316 if not content_json:
317 raise BuildbucketResponseException(
318 'Buildbucket returns invalid json content: %s.\n'
319 'Please file bugs at crbug.com, label "Infra-BuildBucket".' %
320 content)
321 break
322 if response.status < 500 or try_count >= 2:
323 raise httplib2.HttpLib2Error(content)
324
325 # status >= 500 means transient failures.
326 logging.debug('Transient errors when triggering tryjobs. Will retry.')
327 time.sleep(0.5 + 1.5*try_count)
328
329 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000330
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000331
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000332def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
333 """Return the corresponding git ref if |base_url| together with |glob_spec|
334 matches the full |url|.
335
336 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
337 """
338 fetch_suburl, as_ref = glob_spec.split(':')
339 if allow_wildcards:
340 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
341 if glob_match:
342 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
343 # "branches/{472,597,648}/src:refs/remotes/svn/*".
344 branch_re = re.escape(base_url)
345 if glob_match.group(1):
346 branch_re += '/' + re.escape(glob_match.group(1))
347 wildcard = glob_match.group(2)
348 if wildcard == '*':
349 branch_re += '([^/]*)'
350 else:
351 # Escape and replace surrounding braces with parentheses and commas
352 # with pipe symbols.
353 wildcard = re.escape(wildcard)
354 wildcard = re.sub('^\\\\{', '(', wildcard)
355 wildcard = re.sub('\\\\,', '|', wildcard)
356 wildcard = re.sub('\\\\}$', ')', wildcard)
357 branch_re += wildcard
358 if glob_match.group(3):
359 branch_re += re.escape(glob_match.group(3))
360 match = re.match(branch_re, url)
361 if match:
362 return re.sub('\*$', match.group(1), as_ref)
363
364 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
365 if fetch_suburl:
366 full_url = base_url + '/' + fetch_suburl
367 else:
368 full_url = base_url
369 if full_url == url:
370 return as_ref
371 return None
372
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000373
iannucci@chromium.org79540052012-10-19 23:15:26 +0000374def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000375 """Prints statistics about the change to the user."""
376 # --no-ext-diff is broken in some versions of Git, so try to work around
377 # this by overriding the environment (but there is still a problem if the
378 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000379 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000380 if 'GIT_EXTERNAL_DIFF' in env:
381 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000382
383 if find_copies:
384 similarity_options = ['--find-copies-harder', '-l100000',
385 '-C%s' % similarity]
386 else:
387 similarity_options = ['-M%s' % similarity]
388
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000389 try:
390 stdout = sys.stdout.fileno()
391 except AttributeError:
392 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000393 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000394 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000395 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000396 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000397
398
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000399class BuildbucketResponseException(Exception):
400 pass
401
402
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000403class Settings(object):
404 def __init__(self):
405 self.default_server = None
406 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000407 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000408 self.is_git_svn = None
409 self.svn_branch = None
410 self.tree_status_url = None
411 self.viewvc_url = None
412 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000413 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000414 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000415 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000416 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000417 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000418
419 def LazyUpdateIfNeeded(self):
420 """Updates the settings from a codereview.settings file, if available."""
421 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000422 # The only value that actually changes the behavior is
423 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000424 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000425 error_ok=True
426 ).strip().lower()
427
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000428 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000429 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000430 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000431 # set updated to True to avoid infinite calling loop
432 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000433 self.updated = True
434 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000435 self.updated = True
436
437 def GetDefaultServerUrl(self, error_ok=False):
438 if not self.default_server:
439 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000440 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000441 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000442 if error_ok:
443 return self.default_server
444 if not self.default_server:
445 error_message = ('Could not find settings file. You must configure '
446 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000447 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000448 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000449 return self.default_server
450
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000451 @staticmethod
452 def GetRelativeRoot():
453 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000454
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000455 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000456 if self.root is None:
457 self.root = os.path.abspath(self.GetRelativeRoot())
458 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000459
460 def GetIsGitSvn(self):
461 """Return true if this repo looks like it's using git-svn."""
462 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000463 if self.GetPendingRefPrefix():
464 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
465 self.is_git_svn = False
466 else:
467 # If you have any "svn-remote.*" config keys, we think you're using svn.
468 self.is_git_svn = RunGitWithCode(
469 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000470 return self.is_git_svn
471
472 def GetSVNBranch(self):
473 if self.svn_branch is None:
474 if not self.GetIsGitSvn():
475 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
476
477 # Try to figure out which remote branch we're based on.
478 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000479 # 1) iterate through our branch history and find the svn URL.
480 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000481
482 # regexp matching the git-svn line that contains the URL.
483 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
484
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000485 # We don't want to go through all of history, so read a line from the
486 # pipe at a time.
487 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000488 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000489 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
490 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000491 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000492 for line in proc.stdout:
493 match = git_svn_re.match(line)
494 if match:
495 url = match.group(1)
496 proc.stdout.close() # Cut pipe.
497 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000498
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000499 if url:
500 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
501 remotes = RunGit(['config', '--get-regexp',
502 r'^svn-remote\..*\.url']).splitlines()
503 for remote in remotes:
504 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000505 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000506 remote = match.group(1)
507 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000508 rewrite_root = RunGit(
509 ['config', 'svn-remote.%s.rewriteRoot' % remote],
510 error_ok=True).strip()
511 if rewrite_root:
512 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000513 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000514 ['config', 'svn-remote.%s.fetch' % remote],
515 error_ok=True).strip()
516 if fetch_spec:
517 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
518 if self.svn_branch:
519 break
520 branch_spec = RunGit(
521 ['config', 'svn-remote.%s.branches' % remote],
522 error_ok=True).strip()
523 if branch_spec:
524 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
525 if self.svn_branch:
526 break
527 tag_spec = RunGit(
528 ['config', 'svn-remote.%s.tags' % remote],
529 error_ok=True).strip()
530 if tag_spec:
531 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
532 if self.svn_branch:
533 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000534
535 if not self.svn_branch:
536 DieWithError('Can\'t guess svn branch -- try specifying it on the '
537 'command line')
538
539 return self.svn_branch
540
541 def GetTreeStatusUrl(self, error_ok=False):
542 if not self.tree_status_url:
543 error_message = ('You must configure your tree status URL by running '
544 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000545 self.tree_status_url = self._GetRietveldConfig(
546 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000547 return self.tree_status_url
548
549 def GetViewVCUrl(self):
550 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000551 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000552 return self.viewvc_url
553
rmistry@google.com90752582014-01-14 21:04:50 +0000554 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000555 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000556
rmistry@google.com78948ed2015-07-08 23:09:57 +0000557 def GetIsSkipDependencyUpload(self, branch_name):
558 """Returns true if specified branch should skip dep uploads."""
559 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
560 error_ok=True)
561
rmistry@google.com5626a922015-02-26 14:03:30 +0000562 def GetRunPostUploadHook(self):
563 run_post_upload_hook = self._GetRietveldConfig(
564 'run-post-upload-hook', error_ok=True)
565 return run_post_upload_hook == "True"
566
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000567 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000568 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000569
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000570 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000571 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000572
ukai@chromium.orge8077812012-02-03 03:41:46 +0000573 def GetIsGerrit(self):
574 """Return true if this repo is assosiated with gerrit code review system."""
575 if self.is_gerrit is None:
576 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
577 return self.is_gerrit
578
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000579 def GetGitEditor(self):
580 """Return the editor specified in the git config, or None if none is."""
581 if self.git_editor is None:
582 self.git_editor = self._GetConfig('core.editor', error_ok=True)
583 return self.git_editor or None
584
thestig@chromium.org44202a22014-03-11 19:22:18 +0000585 def GetLintRegex(self):
586 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
587 DEFAULT_LINT_REGEX)
588
589 def GetLintIgnoreRegex(self):
590 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
591 DEFAULT_LINT_IGNORE_REGEX)
592
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000593 def GetProject(self):
594 if not self.project:
595 self.project = self._GetRietveldConfig('project', error_ok=True)
596 return self.project
597
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000598 def GetForceHttpsCommitUrl(self):
599 if not self.force_https_commit_url:
600 self.force_https_commit_url = self._GetRietveldConfig(
601 'force-https-commit-url', error_ok=True)
602 return self.force_https_commit_url
603
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000604 def GetPendingRefPrefix(self):
605 if not self.pending_ref_prefix:
606 self.pending_ref_prefix = self._GetRietveldConfig(
607 'pending-ref-prefix', error_ok=True)
608 return self.pending_ref_prefix
609
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000610 def _GetRietveldConfig(self, param, **kwargs):
611 return self._GetConfig('rietveld.' + param, **kwargs)
612
rmistry@google.com78948ed2015-07-08 23:09:57 +0000613 def _GetBranchConfig(self, branch_name, param, **kwargs):
614 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
615
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000616 def _GetConfig(self, param, **kwargs):
617 self.LazyUpdateIfNeeded()
618 return RunGit(['config', param], **kwargs).strip()
619
620
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000621def ShortBranchName(branch):
622 """Convert a name like 'refs/heads/foo' to just 'foo'."""
623 return branch.replace('refs/heads/', '')
624
625
626class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000627 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000628 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000629 global settings
630 if not settings:
631 # Happens when git_cl.py is used as a utility library.
632 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000633 settings.GetDefaultServerUrl()
634 self.branchref = branchref
635 if self.branchref:
636 self.branch = ShortBranchName(self.branchref)
637 else:
638 self.branch = None
639 self.rietveld_server = None
640 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000641 self.lookedup_issue = False
642 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000643 self.has_description = False
644 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000645 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000646 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000647 self.cc = None
648 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000649 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000650 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000651 self._remote = None
652 self._rpc_server = None
653
654 @property
655 def auth_config(self):
656 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000657
658 def GetCCList(self):
659 """Return the users cc'd on this CL.
660
661 Return is a string suitable for passing to gcl with the --cc flag.
662 """
663 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000664 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000665 more_cc = ','.join(self.watchers)
666 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
667 return self.cc
668
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000669 def GetCCListWithoutDefault(self):
670 """Return the users cc'd on this CL excluding default ones."""
671 if self.cc is None:
672 self.cc = ','.join(self.watchers)
673 return self.cc
674
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000675 def SetWatchers(self, watchers):
676 """Set the list of email addresses that should be cc'd based on the changed
677 files in this CL.
678 """
679 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000680
681 def GetBranch(self):
682 """Returns the short branch name, e.g. 'master'."""
683 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000684 branchref = RunGit(['symbolic-ref', 'HEAD'],
685 stderr=subprocess2.VOID, error_ok=True).strip()
686 if not branchref:
687 return None
688 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000689 self.branch = ShortBranchName(self.branchref)
690 return self.branch
691
692 def GetBranchRef(self):
693 """Returns the full branch name, e.g. 'refs/heads/master'."""
694 self.GetBranch() # Poke the lazy loader.
695 return self.branchref
696
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000697 @staticmethod
698 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000699 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000700 e.g. 'origin', 'refs/heads/master'
701 """
702 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000703 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
704 error_ok=True).strip()
705 if upstream_branch:
706 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
707 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000708 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
709 error_ok=True).strip()
710 if upstream_branch:
711 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000712 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000713 # Fall back on trying a git-svn upstream branch.
714 if settings.GetIsGitSvn():
715 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000716 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000717 # Else, try to guess the origin remote.
718 remote_branches = RunGit(['branch', '-r']).split()
719 if 'origin/master' in remote_branches:
720 # Fall back on origin/master if it exits.
721 remote = 'origin'
722 upstream_branch = 'refs/heads/master'
723 elif 'origin/trunk' in remote_branches:
724 # Fall back on origin/trunk if it exists. Generally a shared
725 # git-svn clone
726 remote = 'origin'
727 upstream_branch = 'refs/heads/trunk'
728 else:
729 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000730Either pass complete "git diff"-style arguments, like
731 git cl upload origin/master
732or verify this branch is set up to track another (via the --track argument to
733"git checkout -b ...").""")
734
735 return remote, upstream_branch
736
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000737 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000738 upstream_branch = self.GetUpstreamBranch()
739 if not BranchExists(upstream_branch):
740 DieWithError('The upstream for the current branch (%s) does not exist '
741 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000742 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000743 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000744
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000745 def GetUpstreamBranch(self):
746 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000747 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000748 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000749 upstream_branch = upstream_branch.replace('refs/heads/',
750 'refs/remotes/%s/' % remote)
751 upstream_branch = upstream_branch.replace('refs/branch-heads/',
752 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000753 self.upstream_branch = upstream_branch
754 return self.upstream_branch
755
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000756 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000757 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000758 remote, branch = None, self.GetBranch()
759 seen_branches = set()
760 while branch not in seen_branches:
761 seen_branches.add(branch)
762 remote, branch = self.FetchUpstreamTuple(branch)
763 branch = ShortBranchName(branch)
764 if remote != '.' or branch.startswith('refs/remotes'):
765 break
766 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000767 remotes = RunGit(['remote'], error_ok=True).split()
768 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000769 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000770 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000771 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000772 logging.warning('Could not determine which remote this change is '
773 'associated with, so defaulting to "%s". This may '
774 'not be what you want. You may prevent this message '
775 'by running "git svn info" as documented here: %s',
776 self._remote,
777 GIT_INSTRUCTIONS_URL)
778 else:
779 logging.warn('Could not determine which remote this change is '
780 'associated with. You may prevent this message by '
781 'running "git svn info" as documented here: %s',
782 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000783 branch = 'HEAD'
784 if branch.startswith('refs/remotes'):
785 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000786 elif branch.startswith('refs/branch-heads/'):
787 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000788 else:
789 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000790 return self._remote
791
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000792 def GitSanityChecks(self, upstream_git_obj):
793 """Checks git repo status and ensures diff is from local commits."""
794
sbc@chromium.org79706062015-01-14 21:18:12 +0000795 if upstream_git_obj is None:
796 if self.GetBranch() is None:
797 print >> sys.stderr, (
798 'ERROR: unable to dertermine current branch (detached HEAD?)')
799 else:
800 print >> sys.stderr, (
801 'ERROR: no upstream branch')
802 return False
803
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000804 # Verify the commit we're diffing against is in our current branch.
805 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
806 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
807 if upstream_sha != common_ancestor:
808 print >> sys.stderr, (
809 'ERROR: %s is not in the current branch. You may need to rebase '
810 'your tracking branch' % upstream_sha)
811 return False
812
813 # List the commits inside the diff, and verify they are all local.
814 commits_in_diff = RunGit(
815 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
816 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
817 remote_branch = remote_branch.strip()
818 if code != 0:
819 _, remote_branch = self.GetRemoteBranch()
820
821 commits_in_remote = RunGit(
822 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
823
824 common_commits = set(commits_in_diff) & set(commits_in_remote)
825 if common_commits:
826 print >> sys.stderr, (
827 'ERROR: Your diff contains %d commits already in %s.\n'
828 'Run "git log --oneline %s..HEAD" to get a list of commits in '
829 'the diff. If you are using a custom git flow, you can override'
830 ' the reference used for this check with "git config '
831 'gitcl.remotebranch <git-ref>".' % (
832 len(common_commits), remote_branch, upstream_git_obj))
833 return False
834 return True
835
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000836 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000837 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000838
839 Returns None if it is not set.
840 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000841 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
842 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000843
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000844 def GetGitSvnRemoteUrl(self):
845 """Return the configured git-svn remote URL parsed from git svn info.
846
847 Returns None if it is not set.
848 """
849 # URL is dependent on the current directory.
850 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
851 if data:
852 keys = dict(line.split(': ', 1) for line in data.splitlines()
853 if ': ' in line)
854 return keys.get('URL', None)
855 return None
856
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000857 def GetRemoteUrl(self):
858 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
859
860 Returns None if there is no remote.
861 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000862 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000863 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
864
865 # If URL is pointing to a local directory, it is probably a git cache.
866 if os.path.isdir(url):
867 url = RunGit(['config', 'remote.%s.url' % remote],
868 error_ok=True,
869 cwd=url).strip()
870 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000871
872 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000873 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000874 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000876 self.issue = int(issue) or None if issue else None
877 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000878 return self.issue
879
880 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000881 if not self.rietveld_server:
882 # If we're on a branch then get the server potentially associated
883 # with that branch.
884 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000885 rietveld_server_config = self._RietveldServer()
886 if rietveld_server_config:
887 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
888 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000889 if not self.rietveld_server:
890 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000891 return self.rietveld_server
892
893 def GetIssueURL(self):
894 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000895 if not self.GetIssue():
896 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000897 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
898
899 def GetDescription(self, pretty=False):
900 if not self.has_description:
901 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000902 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000903 try:
904 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000905 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000906 if e.code == 404:
907 DieWithError(
908 ('\nWhile fetching the description for issue %d, received a '
909 '404 (not found)\n'
910 'error. It is likely that you deleted this '
911 'issue on the server. If this is the\n'
912 'case, please run\n\n'
913 ' git cl issue 0\n\n'
914 'to clear the association with the deleted issue. Then run '
915 'this command again.') % issue)
916 else:
917 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000918 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000919 except urllib2.URLError as e:
920 print >> sys.stderr, (
921 'Warning: Failed to retrieve CL description due to network '
922 'failure.')
923 self.description = ''
924
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000925 self.has_description = True
926 if pretty:
927 wrapper = textwrap.TextWrapper()
928 wrapper.initial_indent = wrapper.subsequent_indent = ' '
929 return wrapper.fill(self.description)
930 return self.description
931
932 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000933 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000934 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000935 patchset = RunGit(['config', self._PatchsetSetting()],
936 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000937 self.patchset = int(patchset) or None if patchset else None
938 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000939 return self.patchset
940
941 def SetPatchset(self, patchset):
942 """Set this branch's patchset. If patchset=0, clears the patchset."""
943 if patchset:
944 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000945 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000946 else:
947 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000948 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000949 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000950
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000951 def GetMostRecentPatchset(self):
952 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000953
954 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000955 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000956 '/download/issue%s_%s.diff' % (issue, patchset))
957
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000958 def GetIssueProperties(self):
959 if self._props is None:
960 issue = self.GetIssue()
961 if not issue:
962 self._props = {}
963 else:
964 self._props = self.RpcServer().get_issue_properties(issue, True)
965 return self._props
966
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000967 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000968 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000969
apavlov@chromium.orge4efd512014-11-05 09:05:29 +0000970 def AddComment(self, message):
971 return self.RpcServer().add_comment(self.GetIssue(), message)
972
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000973 def SetIssue(self, issue):
974 """Set this branch's issue. If issue=0, clears the issue."""
975 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000976 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000977 RunGit(['config', self._IssueSetting(), str(issue)])
978 if self.rietveld_server:
979 RunGit(['config', self._RietveldServer(), self.rietveld_server])
980 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000981 current_issue = self.GetIssue()
982 if current_issue:
983 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000984 self.issue = None
985 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000986
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000987 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000988 if not self.GitSanityChecks(upstream_branch):
989 DieWithError('\nGit sanity check failure')
990
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000991 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000992 if not root:
993 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000994 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000995
996 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000997 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000998 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000999 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001000 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001001 except subprocess2.CalledProcessError:
1002 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001003 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001004 'This branch probably doesn\'t exist anymore. To reset the\n'
1005 'tracking branch, please run\n'
1006 ' git branch --set-upstream %s trunk\n'
1007 'replacing trunk with origin/master or the relevant branch') %
1008 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001009
maruel@chromium.org52424302012-08-29 15:14:30 +00001010 issue = self.GetIssue()
1011 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001012 if issue:
1013 description = self.GetDescription()
1014 else:
1015 # If the change was never uploaded, use the log messages of all commits
1016 # up to the branch point, as git cl upload will prefill the description
1017 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001018 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1019 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001020
1021 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001022 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001023 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001024 name,
1025 description,
1026 absroot,
1027 files,
1028 issue,
1029 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001030 author,
1031 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001032
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001033 def GetStatus(self):
1034 """Apply a rough heuristic to give a simple summary of an issue's review
1035 or CQ status, assuming adherence to a common workflow.
1036
1037 Returns None if no issue for this branch, or one of the following keywords:
1038 * 'error' - error from review tool (including deleted issues)
1039 * 'unsent' - not sent for review
1040 * 'waiting' - waiting for review
1041 * 'reply' - waiting for owner to reply to review
1042 * 'lgtm' - LGTM from at least one approved reviewer
1043 * 'commit' - in the commit queue
1044 * 'closed' - closed
1045 """
1046 if not self.GetIssue():
1047 return None
1048
1049 try:
1050 props = self.GetIssueProperties()
1051 except urllib2.HTTPError:
1052 return 'error'
1053
1054 if props.get('closed'):
1055 # Issue is closed.
1056 return 'closed'
1057 if props.get('commit'):
1058 # Issue is in the commit queue.
1059 return 'commit'
1060
1061 try:
1062 reviewers = self.GetApprovingReviewers()
1063 except urllib2.HTTPError:
1064 return 'error'
1065
1066 if reviewers:
1067 # Was LGTM'ed.
1068 return 'lgtm'
1069
1070 messages = props.get('messages') or []
1071
1072 if not messages:
1073 # No message was sent.
1074 return 'unsent'
1075 if messages[-1]['sender'] != props.get('owner_email'):
1076 # Non-LGTM reply from non-owner
1077 return 'reply'
1078 return 'waiting'
1079
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001080 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001081 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001082
1083 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001084 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001085 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001086 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001087 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001088 except presubmit_support.PresubmitFailure, e:
1089 DieWithError(
1090 ('%s\nMaybe your depot_tools is out of date?\n'
1091 'If all fails, contact maruel@') % e)
1092
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001093 def UpdateDescription(self, description):
1094 self.description = description
1095 return self.RpcServer().update_description(
1096 self.GetIssue(), self.description)
1097
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001098 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001099 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001100 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001101
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001102 def SetFlag(self, flag, value):
1103 """Patchset must match."""
1104 if not self.GetPatchset():
1105 DieWithError('The patchset needs to match. Send another patchset.')
1106 try:
1107 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001108 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001109 except urllib2.HTTPError, e:
1110 if e.code == 404:
1111 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1112 if e.code == 403:
1113 DieWithError(
1114 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1115 'match?') % (self.GetIssue(), self.GetPatchset()))
1116 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001117
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001118 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001119 """Returns an upload.RpcServer() to access this review's rietveld instance.
1120 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001121 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001122 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001123 self.GetRietveldServer(),
1124 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001125 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001126
1127 def _IssueSetting(self):
1128 """Return the git setting that stores this change's issue."""
1129 return 'branch.%s.rietveldissue' % self.GetBranch()
1130
1131 def _PatchsetSetting(self):
1132 """Return the git setting that stores this change's most recent patchset."""
1133 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1134
1135 def _RietveldServer(self):
1136 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001137 branch = self.GetBranch()
1138 if branch:
1139 return 'branch.%s.rietveldserver' % branch
1140 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141
1142
1143def GetCodereviewSettingsInteractively():
1144 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001145 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001146 server = settings.GetDefaultServerUrl(error_ok=True)
1147 prompt = 'Rietveld server (host[:port])'
1148 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001149 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001150 if not server and not newserver:
1151 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001152 if newserver:
1153 newserver = gclient_utils.UpgradeToHttps(newserver)
1154 if newserver != server:
1155 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001157 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001158 prompt = caption
1159 if initial:
1160 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001161 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001162 if new_val == 'x':
1163 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001164 elif new_val:
1165 if is_url:
1166 new_val = gclient_utils.UpgradeToHttps(new_val)
1167 if new_val != initial:
1168 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001170 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001171 SetProperty(settings.GetDefaultPrivateFlag(),
1172 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001174 'tree-status-url', False)
1175 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001176 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001177 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1178 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001179
1180 # TODO: configure a default branch to diff against, rather than this
1181 # svn-based hackery.
1182
1183
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001184class ChangeDescription(object):
1185 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001186 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001187 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001188
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001189 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001190 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001191
agable@chromium.org42c20792013-09-12 17:34:49 +00001192 @property # www.logilab.org/ticket/89786
1193 def description(self): # pylint: disable=E0202
1194 return '\n'.join(self._description_lines)
1195
1196 def set_description(self, desc):
1197 if isinstance(desc, basestring):
1198 lines = desc.splitlines()
1199 else:
1200 lines = [line.rstrip() for line in desc]
1201 while lines and not lines[0]:
1202 lines.pop(0)
1203 while lines and not lines[-1]:
1204 lines.pop(-1)
1205 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001206
piman@chromium.org336f9122014-09-04 02:16:55 +00001207 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001208 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001209 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001210 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001211 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001212 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001213
agable@chromium.org42c20792013-09-12 17:34:49 +00001214 # Get the set of R= and TBR= lines and remove them from the desciption.
1215 regexp = re.compile(self.R_LINE)
1216 matches = [regexp.match(line) for line in self._description_lines]
1217 new_desc = [l for i, l in enumerate(self._description_lines)
1218 if not matches[i]]
1219 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001220
agable@chromium.org42c20792013-09-12 17:34:49 +00001221 # Construct new unified R= and TBR= lines.
1222 r_names = []
1223 tbr_names = []
1224 for match in matches:
1225 if not match:
1226 continue
1227 people = cleanup_list([match.group(2).strip()])
1228 if match.group(1) == 'TBR':
1229 tbr_names.extend(people)
1230 else:
1231 r_names.extend(people)
1232 for name in r_names:
1233 if name not in reviewers:
1234 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001235 if add_owners_tbr:
1236 owners_db = owners.Database(change.RepositoryRoot(),
1237 fopen=file, os_path=os.path, glob=glob.glob)
1238 all_reviewers = set(tbr_names + reviewers)
1239 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1240 all_reviewers)
1241 tbr_names.extend(owners_db.reviewers_for(missing_files,
1242 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001243 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1244 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1245
1246 # Put the new lines in the description where the old first R= line was.
1247 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1248 if 0 <= line_loc < len(self._description_lines):
1249 if new_tbr_line:
1250 self._description_lines.insert(line_loc, new_tbr_line)
1251 if new_r_line:
1252 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001253 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001254 if new_r_line:
1255 self.append_footer(new_r_line)
1256 if new_tbr_line:
1257 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001258
1259 def prompt(self):
1260 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001261 self.set_description([
1262 '# Enter a description of the change.',
1263 '# This will be displayed on the codereview site.',
1264 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001265 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001266 '--------------------',
1267 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001268
agable@chromium.org42c20792013-09-12 17:34:49 +00001269 regexp = re.compile(self.BUG_LINE)
1270 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001271 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001272 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001273 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001274 if not content:
1275 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001276 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001277
1278 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001279 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1280 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001281 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001282 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001283
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001284 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001285 if self._description_lines:
1286 # Add an empty line if either the last line or the new line isn't a tag.
1287 last_line = self._description_lines[-1]
1288 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1289 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1290 self._description_lines.append('')
1291 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001292
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001293 def get_reviewers(self):
1294 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001295 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1296 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001297 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001298
1299
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001300def get_approving_reviewers(props):
1301 """Retrieves the reviewers that approved a CL from the issue properties with
1302 messages.
1303
1304 Note that the list may contain reviewers that are not committer, thus are not
1305 considered by the CQ.
1306 """
1307 return sorted(
1308 set(
1309 message['sender']
1310 for message in props['messages']
1311 if message['approval'] and message['sender'] in props['reviewers']
1312 )
1313 )
1314
1315
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001316def FindCodereviewSettingsFile(filename='codereview.settings'):
1317 """Finds the given file starting in the cwd and going up.
1318
1319 Only looks up to the top of the repository unless an
1320 'inherit-review-settings-ok' file exists in the root of the repository.
1321 """
1322 inherit_ok_file = 'inherit-review-settings-ok'
1323 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001324 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001325 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1326 root = '/'
1327 while True:
1328 if filename in os.listdir(cwd):
1329 if os.path.isfile(os.path.join(cwd, filename)):
1330 return open(os.path.join(cwd, filename))
1331 if cwd == root:
1332 break
1333 cwd = os.path.dirname(cwd)
1334
1335
1336def LoadCodereviewSettingsFromFile(fileobj):
1337 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001338 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001340 def SetProperty(name, setting, unset_error_ok=False):
1341 fullname = 'rietveld.' + name
1342 if setting in keyvals:
1343 RunGit(['config', fullname, keyvals[setting]])
1344 else:
1345 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1346
1347 SetProperty('server', 'CODE_REVIEW_SERVER')
1348 # Only server setting is required. Other settings can be absent.
1349 # In that case, we ignore errors raised during option deletion attempt.
1350 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001351 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001352 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1353 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001354 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001355 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001356 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1357 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001358 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001359 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001360 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001361 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1362 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001363
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001364 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001365 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001366
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001367 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1368 #should be of the form
1369 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1370 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1371 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1372 keyvals['ORIGIN_URL_CONFIG']])
1373
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001374
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001375def urlretrieve(source, destination):
1376 """urllib is broken for SSL connections via a proxy therefore we
1377 can't use urllib.urlretrieve()."""
1378 with open(destination, 'w') as f:
1379 f.write(urllib2.urlopen(source).read())
1380
1381
ukai@chromium.org712d6102013-11-27 00:52:58 +00001382def hasSheBang(fname):
1383 """Checks fname is a #! script."""
1384 with open(fname) as f:
1385 return f.read(2).startswith('#!')
1386
1387
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001388def DownloadHooks(force):
1389 """downloads hooks
1390
1391 Args:
1392 force: True to update hooks. False to install hooks if not present.
1393 """
1394 if not settings.GetIsGerrit():
1395 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001396 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001397 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1398 if not os.access(dst, os.X_OK):
1399 if os.path.exists(dst):
1400 if not force:
1401 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001402 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001403 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001404 if not hasSheBang(dst):
1405 DieWithError('Not a script: %s\n'
1406 'You need to download from\n%s\n'
1407 'into .git/hooks/commit-msg and '
1408 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001409 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1410 except Exception:
1411 if os.path.exists(dst):
1412 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001413 DieWithError('\nFailed to download hooks.\n'
1414 'You need to download from\n%s\n'
1415 'into .git/hooks/commit-msg and '
1416 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001417
1418
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001419@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001420def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001421 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001423 parser.add_option('--activate-update', action='store_true',
1424 help='activate auto-updating [rietveld] section in '
1425 '.git/config')
1426 parser.add_option('--deactivate-update', action='store_true',
1427 help='deactivate auto-updating [rietveld] section in '
1428 '.git/config')
1429 options, args = parser.parse_args(args)
1430
1431 if options.deactivate_update:
1432 RunGit(['config', 'rietveld.autoupdate', 'false'])
1433 return
1434
1435 if options.activate_update:
1436 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1437 return
1438
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001439 if len(args) == 0:
1440 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001441 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001442 return 0
1443
1444 url = args[0]
1445 if not url.endswith('codereview.settings'):
1446 url = os.path.join(url, 'codereview.settings')
1447
1448 # Load code review settings and download hooks (if available).
1449 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001450 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001451 return 0
1452
1453
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001454def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001455 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001456 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1457 branch = ShortBranchName(branchref)
1458 _, args = parser.parse_args(args)
1459 if not args:
1460 print("Current base-url:")
1461 return RunGit(['config', 'branch.%s.base-url' % branch],
1462 error_ok=False).strip()
1463 else:
1464 print("Setting base-url to %s" % args[0])
1465 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1466 error_ok=False).strip()
1467
1468
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001469def color_for_status(status):
1470 """Maps a Changelist status to color, for CMDstatus and other tools."""
1471 return {
1472 'unsent': Fore.RED,
1473 'waiting': Fore.BLUE,
1474 'reply': Fore.YELLOW,
1475 'lgtm': Fore.GREEN,
1476 'commit': Fore.MAGENTA,
1477 'closed': Fore.CYAN,
1478 'error': Fore.WHITE,
1479 }.get(status, Fore.WHITE)
1480
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001481def fetch_cl_status(branch, auth_config=None):
1482 """Fetches information for an issue and returns (branch, issue, status)."""
1483 cl = Changelist(branchref=branch, auth_config=auth_config)
1484 url = cl.GetIssueURL()
1485 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001486
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001487 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001488 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001489 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001490
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001491 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001492
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001493def get_cl_statuses(
1494 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001495 """Returns a blocking iterable of (branch, issue, color) for given branches.
1496
1497 If fine_grained is true, this will fetch CL statuses from the server.
1498 Otherwise, simply indicate if there's a matching url for the given branches.
1499
1500 If max_processes is specified, it is used as the maximum number of processes
1501 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1502 spawned.
1503 """
1504 # Silence upload.py otherwise it becomes unwieldly.
1505 upload.verbosity = 0
1506
1507 if fine_grained:
1508 # Process one branch synchronously to work through authentication, then
1509 # spawn processes to process all the other branches in parallel.
1510 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001511 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1512 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001513
1514 branches_to_fetch = branches[1:]
1515 pool = ThreadPool(
1516 min(max_processes, len(branches_to_fetch))
1517 if max_processes is not None
1518 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001519 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001520 yield x
1521 else:
1522 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1523 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001524 cl = Changelist(branchref=b, auth_config=auth_config)
1525 url = cl.GetIssueURL()
1526 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001527
rmistry@google.com2dd99862015-06-22 12:22:18 +00001528
1529def upload_branch_deps(cl, args):
1530 """Uploads CLs of local branches that are dependents of the current branch.
1531
1532 If the local branch dependency tree looks like:
1533 test1 -> test2.1 -> test3.1
1534 -> test3.2
1535 -> test2.2 -> test3.3
1536
1537 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1538 run on the dependent branches in this order:
1539 test2.1, test3.1, test3.2, test2.2, test3.3
1540
1541 Note: This function does not rebase your local dependent branches. Use it when
1542 you make a change to the parent branch that will not conflict with its
1543 dependent branches, and you would like their dependencies updated in
1544 Rietveld.
1545 """
1546 if git_common.is_dirty_git_tree('upload-branch-deps'):
1547 return 1
1548
1549 root_branch = cl.GetBranch()
1550 if root_branch is None:
1551 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1552 'Get on a branch!')
1553 if not cl.GetIssue() or not cl.GetPatchset():
1554 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1555 'patchset dependencies without an uploaded CL.')
1556
1557 branches = RunGit(['for-each-ref',
1558 '--format=%(refname:short) %(upstream:short)',
1559 'refs/heads'])
1560 if not branches:
1561 print('No local branches found.')
1562 return 0
1563
1564 # Create a dictionary of all local branches to the branches that are dependent
1565 # on it.
1566 tracked_to_dependents = collections.defaultdict(list)
1567 for b in branches.splitlines():
1568 tokens = b.split()
1569 if len(tokens) == 2:
1570 branch_name, tracked = tokens
1571 tracked_to_dependents[tracked].append(branch_name)
1572
1573 print
1574 print 'The dependent local branches of %s are:' % root_branch
1575 dependents = []
1576 def traverse_dependents_preorder(branch, padding=''):
1577 dependents_to_process = tracked_to_dependents.get(branch, [])
1578 padding += ' '
1579 for dependent in dependents_to_process:
1580 print '%s%s' % (padding, dependent)
1581 dependents.append(dependent)
1582 traverse_dependents_preorder(dependent, padding)
1583 traverse_dependents_preorder(root_branch)
1584 print
1585
1586 if not dependents:
1587 print 'There are no dependent local branches for %s' % root_branch
1588 return 0
1589
1590 print ('This command will checkout all dependent branches and run '
1591 '"git cl upload".')
1592 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1593
1594 # Add a default patchset title to all upload calls.
1595 args.extend(['-t', 'Updated patchset dependency'])
1596 # Record all dependents that failed to upload.
1597 failures = {}
1598 # Go through all dependents, checkout the branch and upload.
1599 try:
1600 for dependent_branch in dependents:
1601 print
1602 print '--------------------------------------'
1603 print 'Running "git cl upload" from %s:' % dependent_branch
1604 RunGit(['checkout', '-q', dependent_branch])
1605 print
1606 try:
1607 if CMDupload(OptionParser(), args) != 0:
1608 print 'Upload failed for %s!' % dependent_branch
1609 failures[dependent_branch] = 1
1610 except: # pylint: disable=W0702
1611 failures[dependent_branch] = 1
1612 print
1613 finally:
1614 # Swap back to the original root branch.
1615 RunGit(['checkout', '-q', root_branch])
1616
1617 print
1618 print 'Upload complete for dependent branches!'
1619 for dependent_branch in dependents:
1620 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1621 print ' %s : %s' % (dependent_branch, upload_status)
1622 print
1623
1624 return 0
1625
1626
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001627def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001628 """Show status of changelists.
1629
1630 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001631 - Red not sent for review or broken
1632 - Blue waiting for review
1633 - Yellow waiting for you to reply to review
1634 - Green LGTM'ed
1635 - Magenta in the commit queue
1636 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001637
1638 Also see 'git cl comments'.
1639 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001640 parser.add_option('--field',
1641 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001642 parser.add_option('-f', '--fast', action='store_true',
1643 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001644 parser.add_option(
1645 '-j', '--maxjobs', action='store', type=int,
1646 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001647
1648 auth.add_auth_options(parser)
1649 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001650 if args:
1651 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001652 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001653
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001654 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001655 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001656 if options.field.startswith('desc'):
1657 print cl.GetDescription()
1658 elif options.field == 'id':
1659 issueid = cl.GetIssue()
1660 if issueid:
1661 print issueid
1662 elif options.field == 'patch':
1663 patchset = cl.GetPatchset()
1664 if patchset:
1665 print patchset
1666 elif options.field == 'url':
1667 url = cl.GetIssueURL()
1668 if url:
1669 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001670 return 0
1671
1672 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1673 if not branches:
1674 print('No local branch found.')
1675 return 0
1676
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001677 changes = (
1678 Changelist(branchref=b, auth_config=auth_config)
1679 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001680 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001681 alignment = max(5, max(len(b) for b in branches))
1682 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001683 output = get_cl_statuses(branches,
1684 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001685 max_processes=options.maxjobs,
1686 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001687
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001688 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001689 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001690 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001691 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001692 b, i, status = output.next()
1693 branch_statuses[b] = (i, status)
1694 issue_url, status = branch_statuses.pop(branch)
1695 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001696 reset = Fore.RESET
1697 if not sys.stdout.isatty():
1698 color = ''
1699 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001700 status_str = '(%s)' % status if status else ''
1701 print ' %*s : %s%s %s%s' % (
1702 alignment, ShortBranchName(branch), color, issue_url, status_str,
1703 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001704
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001705 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001706 print
1707 print 'Current branch:',
1708 if not cl.GetIssue():
1709 print 'no issue assigned.'
1710 return 0
1711 print cl.GetBranch()
1712 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001713 if not options.fast:
1714 print 'Issue description:'
1715 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001716 return 0
1717
1718
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001719def colorize_CMDstatus_doc():
1720 """To be called once in main() to add colors to git cl status help."""
1721 colors = [i for i in dir(Fore) if i[0].isupper()]
1722
1723 def colorize_line(line):
1724 for color in colors:
1725 if color in line.upper():
1726 # Extract whitespaces first and the leading '-'.
1727 indent = len(line) - len(line.lstrip(' ')) + 1
1728 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1729 return line
1730
1731 lines = CMDstatus.__doc__.splitlines()
1732 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1733
1734
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001735@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001736def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001737 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001738
1739 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001740 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001741 parser.add_option('-r', '--reverse', action='store_true',
1742 help='Lookup the branch(es) for the specified issues. If '
1743 'no issues are specified, all branches with mapped '
1744 'issues will be listed.')
1745 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001746
dnj@chromium.org406c4402015-03-03 17:22:28 +00001747 if options.reverse:
1748 branches = RunGit(['for-each-ref', 'refs/heads',
1749 '--format=%(refname:short)']).splitlines()
1750
1751 # Reverse issue lookup.
1752 issue_branch_map = {}
1753 for branch in branches:
1754 cl = Changelist(branchref=branch)
1755 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1756 if not args:
1757 args = sorted(issue_branch_map.iterkeys())
1758 for issue in args:
1759 if not issue:
1760 continue
1761 print 'Branch for issue number %s: %s' % (
1762 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1763 else:
1764 cl = Changelist()
1765 if len(args) > 0:
1766 try:
1767 issue = int(args[0])
1768 except ValueError:
1769 DieWithError('Pass a number to set the issue or none to list it.\n'
1770 'Maybe you want to run git cl status?')
1771 cl.SetIssue(issue)
1772 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001773 return 0
1774
1775
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001776def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001777 """Shows or posts review comments for any changelist."""
1778 parser.add_option('-a', '--add-comment', dest='comment',
1779 help='comment to add to an issue')
1780 parser.add_option('-i', dest='issue',
1781 help="review issue id (defaults to current issue)")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001782 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001783 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001784 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001785
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001786 issue = None
1787 if options.issue:
1788 try:
1789 issue = int(options.issue)
1790 except ValueError:
1791 DieWithError('A review issue id is expected to be a number')
1792
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001793 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001794
1795 if options.comment:
1796 cl.AddComment(options.comment)
1797 return 0
1798
1799 data = cl.GetIssueProperties()
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001800 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001801 if message['disapproval']:
1802 color = Fore.RED
1803 elif message['approval']:
1804 color = Fore.GREEN
1805 elif message['sender'] == data['owner_email']:
1806 color = Fore.MAGENTA
1807 else:
1808 color = Fore.BLUE
1809 print '\n%s%s %s%s' % (
1810 color, message['date'].split('.', 1)[0], message['sender'],
1811 Fore.RESET)
1812 if message['text'].strip():
1813 print '\n'.join(' ' + l for l in message['text'].splitlines())
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001814 return 0
1815
1816
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001817def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001818 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00001819 parser.add_option('-d', '--display', action='store_true',
1820 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001821 auth.add_auth_options(parser)
1822 options, _ = parser.parse_args(args)
1823 auth_config = auth.extract_auth_config_from_options(options)
1824 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001825 if not cl.GetIssue():
1826 DieWithError('This branch has no associated changelist.')
1827 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00001828 if options.display:
1829 print description.description
1830 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001831 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00001832 if cl.GetDescription() != description.description:
1833 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001834 return 0
1835
1836
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001837def CreateDescriptionFromLog(args):
1838 """Pulls out the commit log to use as a base for the CL description."""
1839 log_args = []
1840 if len(args) == 1 and not args[0].endswith('.'):
1841 log_args = [args[0] + '..']
1842 elif len(args) == 1 and args[0].endswith('...'):
1843 log_args = [args[0][:-1]]
1844 elif len(args) == 2:
1845 log_args = [args[0] + '..' + args[1]]
1846 else:
1847 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001848 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001849
1850
thestig@chromium.org44202a22014-03-11 19:22:18 +00001851def CMDlint(parser, args):
1852 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001853 parser.add_option('--filter', action='append', metavar='-x,+y',
1854 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001855 auth.add_auth_options(parser)
1856 options, args = parser.parse_args(args)
1857 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001858
1859 # Access to a protected member _XX of a client class
1860 # pylint: disable=W0212
1861 try:
1862 import cpplint
1863 import cpplint_chromium
1864 except ImportError:
1865 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1866 return 1
1867
1868 # Change the current working directory before calling lint so that it
1869 # shows the correct base.
1870 previous_cwd = os.getcwd()
1871 os.chdir(settings.GetRoot())
1872 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001873 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001874 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1875 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001876 if not files:
1877 print "Cannot lint an empty CL"
1878 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001879
1880 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001881 command = args + files
1882 if options.filter:
1883 command = ['--filter=' + ','.join(options.filter)] + command
1884 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001885
1886 white_regex = re.compile(settings.GetLintRegex())
1887 black_regex = re.compile(settings.GetLintIgnoreRegex())
1888 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1889 for filename in filenames:
1890 if white_regex.match(filename):
1891 if black_regex.match(filename):
1892 print "Ignoring file %s" % filename
1893 else:
1894 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1895 extra_check_functions)
1896 else:
1897 print "Skipping file %s" % filename
1898 finally:
1899 os.chdir(previous_cwd)
1900 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1901 if cpplint._cpplint_state.error_count != 0:
1902 return 1
1903 return 0
1904
1905
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001906def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001907 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001908 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001909 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001910 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001911 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001912 auth.add_auth_options(parser)
1913 options, args = parser.parse_args(args)
1914 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001915
sbc@chromium.org71437c02015-04-09 19:29:40 +00001916 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00001917 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001918 return 1
1919
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001920 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001921 if args:
1922 base_branch = args[0]
1923 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001924 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001925 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001926
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001927 cl.RunHook(
1928 committing=not options.upload,
1929 may_prompt=False,
1930 verbose=options.verbose,
1931 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001932 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001933
1934
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001935def AddChangeIdToCommitMessage(options, args):
1936 """Re-commits using the current message, assumes the commit hook is in
1937 place.
1938 """
1939 log_desc = options.message or CreateDescriptionFromLog(args)
1940 git_command = ['commit', '--amend', '-m', log_desc]
1941 RunGit(git_command)
1942 new_log_desc = CreateDescriptionFromLog(args)
1943 if CHANGE_ID in new_log_desc:
1944 print 'git-cl: Added Change-Id to commit message.'
1945 else:
1946 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1947
1948
piman@chromium.org336f9122014-09-04 02:16:55 +00001949def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001950 """upload the current branch to gerrit."""
1951 # We assume the remote called "origin" is the one we want.
1952 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001953 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00001954
1955 remote, remote_branch = cl.GetRemoteBranch()
1956 branch = GetTargetRef(remote, remote_branch, options.target_branch,
1957 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001958
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001959 change_desc = ChangeDescription(
1960 options.message or CreateDescriptionFromLog(args))
1961 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001962 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001963 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001964
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001965 if options.squash:
1966 # Try to get the message from a previous upload.
1967 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
1968 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
1969 if not message:
1970 if not options.force:
1971 change_desc.prompt()
1972
1973 if CHANGE_ID not in change_desc.description:
1974 # Run the commit-msg hook without modifying the head commit by writing
1975 # the commit message to a temporary file and running the hook over it,
1976 # then reading the file back in.
1977 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
1978 'commit-msg')
1979 file_handle, msg_file = tempfile.mkstemp(text=True,
1980 prefix='commit_msg')
1981 try:
1982 try:
1983 with os.fdopen(file_handle, 'w') as fileobj:
1984 fileobj.write(change_desc.description)
1985 finally:
1986 os.close(file_handle)
1987 RunCommand([commit_msg_hook, msg_file])
1988 change_desc.set_description(gclient_utils.FileRead(msg_file))
1989 finally:
1990 os.remove(msg_file)
1991
1992 if not change_desc.description:
1993 print "Description is empty; aborting."
1994 return 1
1995
1996 message = change_desc.description
1997
1998 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1999 if remote is '.':
2000 # If our upstream branch is local, we base our squashed commit on its
2001 # squashed version.
2002 parent = ('refs/heads/git_cl_uploads/' +
2003 scm.GIT.ShortBranchName(upstream_branch))
2004
2005 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2006 # will create additional CLs when uploading.
2007 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2008 RunGitSilent(['rev-parse', parent + ':'])):
2009 print 'Upload upstream branch ' + upstream_branch + ' first.'
2010 return 1
2011 else:
2012 parent = cl.GetCommonAncestorWithUpstream()
2013
2014 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2015 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2016 '-m', message]).strip()
2017 else:
2018 if CHANGE_ID not in change_desc.description:
2019 AddChangeIdToCommitMessage(options, args)
2020 ref_to_push = 'HEAD'
2021 parent = '%s/%s' % (gerrit_remote, branch)
2022
2023 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2024 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002025 if len(commits) > 1:
2026 print('WARNING: This will upload %d commits. Run the following command '
2027 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002028 print('git log %s..%s' % (parent, ref_to_push))
2029 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002030 'commit.')
2031 ask_for_data('About to upload; enter to confirm.')
2032
piman@chromium.org336f9122014-09-04 02:16:55 +00002033 if options.reviewers or options.tbr_owners:
2034 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002035
ukai@chromium.orge8077812012-02-03 03:41:46 +00002036 receive_options = []
2037 cc = cl.GetCCList().split(',')
2038 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002039 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002040 cc = filter(None, cc)
2041 if cc:
2042 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002043 if change_desc.get_reviewers():
2044 receive_options.extend(
2045 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002046
ukai@chromium.orge8077812012-02-03 03:41:46 +00002047 git_command = ['push']
2048 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002049 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002050 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002051 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002052 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002053
2054 if options.squash:
2055 head = RunGit(['rev-parse', 'HEAD']).strip()
2056 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2057
ukai@chromium.orge8077812012-02-03 03:41:46 +00002058 # TODO(ukai): parse Change-Id: and set issue number?
2059 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002060
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002061
wittman@chromium.org455dc922015-01-26 20:15:50 +00002062def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2063 """Computes the remote branch ref to use for the CL.
2064
2065 Args:
2066 remote (str): The git remote for the CL.
2067 remote_branch (str): The git remote branch for the CL.
2068 target_branch (str): The target branch specified by the user.
2069 pending_prefix (str): The pending prefix from the settings.
2070 """
2071 if not (remote and remote_branch):
2072 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002073
wittman@chromium.org455dc922015-01-26 20:15:50 +00002074 if target_branch:
2075 # Cannonicalize branch references to the equivalent local full symbolic
2076 # refs, which are then translated into the remote full symbolic refs
2077 # below.
2078 if '/' not in target_branch:
2079 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2080 else:
2081 prefix_replacements = (
2082 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2083 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2084 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2085 )
2086 match = None
2087 for regex, replacement in prefix_replacements:
2088 match = re.search(regex, target_branch)
2089 if match:
2090 remote_branch = target_branch.replace(match.group(0), replacement)
2091 break
2092 if not match:
2093 # This is a branch path but not one we recognize; use as-is.
2094 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002095 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2096 # Handle the refs that need to land in different refs.
2097 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002098
wittman@chromium.org455dc922015-01-26 20:15:50 +00002099 # Create the true path to the remote branch.
2100 # Does the following translation:
2101 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2102 # * refs/remotes/origin/master -> refs/heads/master
2103 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2104 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2105 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2106 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2107 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2108 'refs/heads/')
2109 elif remote_branch.startswith('refs/remotes/branch-heads'):
2110 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2111 # If a pending prefix exists then replace refs/ with it.
2112 if pending_prefix:
2113 remote_branch = remote_branch.replace('refs/', pending_prefix)
2114 return remote_branch
2115
2116
piman@chromium.org336f9122014-09-04 02:16:55 +00002117def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002118 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002119 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2120 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002121 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002122 if options.emulate_svn_auto_props:
2123 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002124
2125 change_desc = None
2126
pgervais@chromium.org91141372014-01-09 23:27:20 +00002127 if options.email is not None:
2128 upload_args.extend(['--email', options.email])
2129
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002130 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002131 if options.title:
2132 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002133 if options.message:
2134 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002135 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002136 print ("This branch is associated with issue %s. "
2137 "Adding patch to that issue." % cl.GetIssue())
2138 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002139 if options.title:
2140 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002141 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002142 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002143 if options.reviewers or options.tbr_owners:
2144 change_desc.update_reviewers(options.reviewers,
2145 options.tbr_owners,
2146 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002147 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002148 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002149
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002150 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002151 print "Description is empty; aborting."
2152 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002153
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002154 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002155 if change_desc.get_reviewers():
2156 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002157 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002158 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002159 DieWithError("Must specify reviewers to send email.")
2160 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002161
2162 # We check this before applying rietveld.private assuming that in
2163 # rietveld.cc only addresses which we can send private CLs to are listed
2164 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2165 # --private is specified explicitly on the command line.
2166 if options.private:
2167 logging.warn('rietveld.cc is ignored since private flag is specified. '
2168 'You need to review and add them manually if necessary.')
2169 cc = cl.GetCCListWithoutDefault()
2170 else:
2171 cc = cl.GetCCList()
2172 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002173 if cc:
2174 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002175
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002176 if options.private or settings.GetDefaultPrivateFlag() == "True":
2177 upload_args.append('--private')
2178
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002179 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002180 if not options.find_copies:
2181 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002182
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002183 # Include the upstream repo's URL in the change -- this is useful for
2184 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002185 remote_url = cl.GetGitBaseUrlFromConfig()
2186 if not remote_url:
2187 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002188 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002189 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002190 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2191 remote_url = (cl.GetRemoteUrl() + '@'
2192 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002193 if remote_url:
2194 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002195 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002196 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2197 settings.GetPendingRefPrefix())
2198 if target_ref:
2199 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002200
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002201 # Look for dependent patchsets. See crbug.com/480453 for more details.
2202 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2203 upstream_branch = ShortBranchName(upstream_branch)
2204 if remote is '.':
2205 # A local branch is being tracked.
2206 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002207 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002208 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002209 print ('Skipping dependency patchset upload because git config '
2210 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002211 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002212 else:
2213 auth_config = auth.extract_auth_config_from_options(options)
2214 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2215 branch_cl_issue_url = branch_cl.GetIssueURL()
2216 branch_cl_issue = branch_cl.GetIssue()
2217 branch_cl_patchset = branch_cl.GetPatchset()
2218 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2219 upload_args.extend(
2220 ['--depends_on_patchset', '%s:%s' % (
2221 branch_cl_issue, branch_cl_patchset)])
2222 print
2223 print ('The current branch (%s) is tracking a local branch (%s) with '
2224 'an associated CL.') % (cl.GetBranch(), local_branch)
2225 print 'Adding %s/#ps%s as a dependency patchset.' % (
2226 branch_cl_issue_url, branch_cl_patchset)
2227 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002228
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002229 project = settings.GetProject()
2230 if project:
2231 upload_args.extend(['--project', project])
2232
rmistry@google.comef966222015-04-07 11:15:01 +00002233 if options.cq_dry_run:
2234 upload_args.extend(['--cq_dry_run'])
2235
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002236 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002237 upload_args = ['upload'] + upload_args + args
2238 logging.info('upload.RealMain(%s)', upload_args)
2239 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002240 issue = int(issue)
2241 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002242 except KeyboardInterrupt:
2243 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002244 except:
2245 # If we got an exception after the user typed a description for their
2246 # change, back up the description before re-raising.
2247 if change_desc:
2248 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2249 print '\nGot exception while uploading -- saving description to %s\n' \
2250 % backup_path
2251 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002252 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002253 backup_file.close()
2254 raise
2255
2256 if not cl.GetIssue():
2257 cl.SetIssue(issue)
2258 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002259
2260 if options.use_commit_queue:
2261 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002262 return 0
2263
2264
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002265def cleanup_list(l):
2266 """Fixes a list so that comma separated items are put as individual items.
2267
2268 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2269 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2270 """
2271 items = sum((i.split(',') for i in l), [])
2272 stripped_items = (i.strip() for i in items)
2273 return sorted(filter(None, stripped_items))
2274
2275
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002276@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002277def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002278 """Uploads the current changelist to codereview.
2279
2280 Can skip dependency patchset uploads for a branch by running:
2281 git config branch.branch_name.skip-deps-uploads True
2282 To unset run:
2283 git config --unset branch.branch_name.skip-deps-uploads
2284 Can also set the above globally by using the --global flag.
2285 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002286 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2287 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002288 parser.add_option('--bypass-watchlists', action='store_true',
2289 dest='bypass_watchlists',
2290 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002291 parser.add_option('-f', action='store_true', dest='force',
2292 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002293 parser.add_option('-m', dest='message', help='message for patchset')
2294 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002295 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002296 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002297 help='reviewer email addresses')
2298 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002299 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002300 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002301 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002302 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002303 parser.add_option('--emulate_svn_auto_props',
2304 '--emulate-svn-auto-props',
2305 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002306 dest="emulate_svn_auto_props",
2307 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002308 parser.add_option('-c', '--use-commit-queue', action='store_true',
2309 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002310 parser.add_option('--private', action='store_true',
2311 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002312 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002313 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002314 metavar='TARGET',
2315 help='Apply CL to remote ref TARGET. ' +
2316 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002317 parser.add_option('--squash', action='store_true',
2318 help='Squash multiple commits into one (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002319 parser.add_option('--email', default=None,
2320 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002321 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2322 help='add a set of OWNERS to TBR')
rmistry@google.comef966222015-04-07 11:15:01 +00002323 parser.add_option('--cq-dry-run', dest='cq_dry_run', action='store_true',
2324 help='Send the patchset to do a CQ dry run right after '
2325 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002326 parser.add_option('--dependencies', action='store_true',
2327 help='Uploads CLs of all the local branches that depend on '
2328 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002329
rmistry@google.com2dd99862015-06-22 12:22:18 +00002330 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002331 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002332 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002333 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002334 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002335
sbc@chromium.org71437c02015-04-09 19:29:40 +00002336 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002337 return 1
2338
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002339 options.reviewers = cleanup_list(options.reviewers)
2340 options.cc = cleanup_list(options.cc)
2341
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002342 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002343 if args:
2344 # TODO(ukai): is it ok for gerrit case?
2345 base_branch = args[0]
2346 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002347 if cl.GetBranch() is None:
2348 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2349
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002350 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002351 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002352 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002353
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002354 # Make sure authenticated to Rietveld before running expensive hooks. It is
2355 # a fast, best efforts check. Rietveld still can reject the authentication
2356 # during the actual upload.
2357 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2358 authenticator = auth.get_authenticator_for_host(
2359 cl.GetRietveldServer(), auth_config)
2360 if not authenticator.has_cached_credentials():
2361 raise auth.LoginRequiredError(cl.GetRietveldServer())
2362
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002363 # Apply watchlists on upload.
2364 change = cl.GetChange(base_branch, None)
2365 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2366 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002367 if not options.bypass_watchlists:
2368 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002369
ukai@chromium.orge8077812012-02-03 03:41:46 +00002370 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002371 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002372 # Set the reviewer list now so that presubmit checks can access it.
2373 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002374 change_description.update_reviewers(options.reviewers,
2375 options.tbr_owners,
2376 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002377 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002378 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002379 may_prompt=not options.force,
2380 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002381 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002382 if not hook_results.should_continue():
2383 return 1
2384 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002385 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002386
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002387 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002388 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002389 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002390 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002391 print ('The last upload made from this repository was patchset #%d but '
2392 'the most recent patchset on the server is #%d.'
2393 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002394 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2395 'from another machine or branch the patch you\'re uploading now '
2396 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002397 ask_for_data('About to upload; enter to confirm.')
2398
iannucci@chromium.org79540052012-10-19 23:15:26 +00002399 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002400 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00002401 return GerritUpload(options, args, cl, change)
2402 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002403 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002404 git_set_branch_value('last-upload-hash',
2405 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002406 # Run post upload hooks, if specified.
2407 if settings.GetRunPostUploadHook():
2408 presubmit_support.DoPostUploadExecuter(
2409 change,
2410 cl,
2411 settings.GetRoot(),
2412 options.verbose,
2413 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002414
rmistry@google.com2dd99862015-06-22 12:22:18 +00002415 # Upload all dependencies if specified.
2416 if options.dependencies:
2417 print
2418 print '--dependencies has been specified.'
2419 print 'All dependent local branches will be re-uploaded.'
2420 print
2421 # Remove the dependencies flag from args so that we do not end up in a
2422 # loop.
2423 orig_args.remove('--dependencies')
2424 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002425 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002426
2427
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002428def IsSubmoduleMergeCommit(ref):
2429 # When submodules are added to the repo, we expect there to be a single
2430 # non-git-svn merge commit at remote HEAD with a signature comment.
2431 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002432 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002433 return RunGit(cmd) != ''
2434
2435
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002436def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002437 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002438
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002439 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002440 Updates changelog with metadata (e.g. pointer to review).
2441 Pushes/dcommits the code upstream.
2442 Updates review and closes.
2443 """
2444 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2445 help='bypass upload presubmit hook')
2446 parser.add_option('-m', dest='message',
2447 help="override review description")
2448 parser.add_option('-f', action='store_true', dest='force',
2449 help="force yes to questions (don't prompt)")
2450 parser.add_option('-c', dest='contributor',
2451 help="external contributor for patch (appended to " +
2452 "description and used as author for git). Should be " +
2453 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002454 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002455 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002456 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002457 auth_config = auth.extract_auth_config_from_options(options)
2458
2459 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002460
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002461 current = cl.GetBranch()
2462 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2463 if not settings.GetIsGitSvn() and remote == '.':
2464 print
2465 print 'Attempting to push branch %r into another local branch!' % current
2466 print
2467 print 'Either reparent this branch on top of origin/master:'
2468 print ' git reparent-branch --root'
2469 print
2470 print 'OR run `git rebase-update` if you think the parent branch is already'
2471 print 'committed.'
2472 print
2473 print ' Current parent: %r' % upstream_branch
2474 return 1
2475
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002476 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002477 # Default to merging against our best guess of the upstream branch.
2478 args = [cl.GetUpstreamBranch()]
2479
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002480 if options.contributor:
2481 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2482 print "Please provide contibutor as 'First Last <email@example.com>'"
2483 return 1
2484
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002485 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002486 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002487
sbc@chromium.org71437c02015-04-09 19:29:40 +00002488 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002489 return 1
2490
2491 # This rev-list syntax means "show all commits not in my branch that
2492 # are in base_branch".
2493 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2494 base_branch]).splitlines()
2495 if upstream_commits:
2496 print ('Base branch "%s" has %d commits '
2497 'not in this branch.' % (base_branch, len(upstream_commits)))
2498 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2499 return 1
2500
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002501 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002502 svn_head = None
2503 if cmd == 'dcommit' or base_has_submodules:
2504 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2505 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002506
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002507 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002508 # If the base_head is a submodule merge commit, the first parent of the
2509 # base_head should be a git-svn commit, which is what we're interested in.
2510 base_svn_head = base_branch
2511 if base_has_submodules:
2512 base_svn_head += '^1'
2513
2514 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002515 if extra_commits:
2516 print ('This branch has %d additional commits not upstreamed yet.'
2517 % len(extra_commits.splitlines()))
2518 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2519 'before attempting to %s.' % (base_branch, cmd))
2520 return 1
2521
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002522 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002523 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002524 author = None
2525 if options.contributor:
2526 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002527 hook_results = cl.RunHook(
2528 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002529 may_prompt=not options.force,
2530 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002531 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002532 if not hook_results.should_continue():
2533 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002534
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002535 # Check the tree status if the tree status URL is set.
2536 status = GetTreeStatus()
2537 if 'closed' == status:
2538 print('The tree is closed. Please wait for it to reopen. Use '
2539 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2540 return 1
2541 elif 'unknown' == status:
2542 print('Unable to determine tree status. Please verify manually and '
2543 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2544 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002545 else:
2546 breakpad.SendStack(
2547 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002548 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2549 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002550 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002551
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002552 change_desc = ChangeDescription(options.message)
2553 if not change_desc.description and cl.GetIssue():
2554 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002555
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002556 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002557 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002558 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002559 else:
2560 print 'No description set.'
2561 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2562 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002563
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002564 # Keep a separate copy for the commit message, because the commit message
2565 # contains the link to the Rietveld issue, while the Rietveld message contains
2566 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002567 # Keep a separate copy for the commit message.
2568 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002569 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002570
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002571 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002572 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002573 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002574 # after it. Add a period on a new line to circumvent this. Also add a space
2575 # before the period to make sure that Gitiles continues to correctly resolve
2576 # the URL.
2577 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002578 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002579 commit_desc.append_footer('Patch from %s.' % options.contributor)
2580
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002581 print('Description:')
2582 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002583
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002584 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002585 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002586 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002587
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002588 # We want to squash all this branch's commits into one commit with the proper
2589 # description. We do this by doing a "reset --soft" to the base branch (which
2590 # keeps the working copy the same), then dcommitting that. If origin/master
2591 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2592 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002593 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002594 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2595 # Delete the branches if they exist.
2596 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2597 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2598 result = RunGitWithCode(showref_cmd)
2599 if result[0] == 0:
2600 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002601
2602 # We might be in a directory that's present in this branch but not in the
2603 # trunk. Move up to the top of the tree so that git commands that expect a
2604 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002605 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002606 if rel_base_path:
2607 os.chdir(rel_base_path)
2608
2609 # Stuff our change into the merge branch.
2610 # We wrap in a try...finally block so if anything goes wrong,
2611 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002612 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002613 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002614 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002615 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002616 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002617 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002618 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002619 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002620 RunGit(
2621 [
2622 'commit', '--author', options.contributor,
2623 '-m', commit_desc.description,
2624 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002625 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002626 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002627 if base_has_submodules:
2628 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2629 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2630 RunGit(['checkout', CHERRY_PICK_BRANCH])
2631 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002632 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002633 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002634 pending_prefix = settings.GetPendingRefPrefix()
2635 if not pending_prefix or branch.startswith(pending_prefix):
2636 # If not using refs/pending/heads/* at all, or target ref is already set
2637 # to pending, then push to the target ref directly.
2638 retcode, output = RunGitWithCode(
2639 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002640 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002641 else:
2642 # Cherry-pick the change on top of pending ref and then push it.
2643 assert branch.startswith('refs/'), branch
2644 assert pending_prefix[-1] == '/', pending_prefix
2645 pending_ref = pending_prefix + branch[len('refs/'):]
2646 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002647 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002648 if retcode == 0:
2649 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002650 else:
2651 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002652 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002653 'svn', 'dcommit',
2654 '-C%s' % options.similarity,
2655 '--no-rebase', '--rmdir',
2656 ]
2657 if settings.GetForceHttpsCommitUrl():
2658 # Allow forcing https commit URLs for some projects that don't allow
2659 # committing to http URLs (like Google Code).
2660 remote_url = cl.GetGitSvnRemoteUrl()
2661 if urlparse.urlparse(remote_url).scheme == 'http':
2662 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002663 cmd_args.append('--commit-url=%s' % remote_url)
2664 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002665 if 'Committed r' in output:
2666 revision = re.match(
2667 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2668 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002669 finally:
2670 # And then swap back to the original branch and clean up.
2671 RunGit(['checkout', '-q', cl.GetBranch()])
2672 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002673 if base_has_submodules:
2674 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002675
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002676 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002677 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002678 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002679
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002680 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002681 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002682 try:
2683 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2684 # We set pushed_to_pending to False, since it made it all the way to the
2685 # real ref.
2686 pushed_to_pending = False
2687 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002688 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002689
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002690 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002691 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002692 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002693 if not to_pending:
2694 if viewvc_url and revision:
2695 change_desc.append_footer(
2696 'Committed: %s%s' % (viewvc_url, revision))
2697 elif revision:
2698 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002699 print ('Closing issue '
2700 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002701 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002702 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002703 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002704 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002705 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002706 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002707 if options.bypass_hooks:
2708 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2709 else:
2710 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002711 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002712 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002713
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002714 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002715 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2716 print 'The commit is in the pending queue (%s).' % pending_ref
2717 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002718 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002719 'footer.' % branch)
2720
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002721 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2722 if os.path.isfile(hook):
2723 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002724
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002725 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002726
2727
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002728def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2729 print
2730 print 'Waiting for commit to be landed on %s...' % real_ref
2731 print '(If you are impatient, you may Ctrl-C once without harm)'
2732 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2733 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2734
2735 loop = 0
2736 while True:
2737 sys.stdout.write('fetching (%d)... \r' % loop)
2738 sys.stdout.flush()
2739 loop += 1
2740
2741 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2742 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2743 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2744 for commit in commits.splitlines():
2745 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2746 print 'Found commit on %s' % real_ref
2747 return commit
2748
2749 current_rev = to_rev
2750
2751
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002752def PushToGitPending(remote, pending_ref, upstream_ref):
2753 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2754
2755 Returns:
2756 (retcode of last operation, output log of last operation).
2757 """
2758 assert pending_ref.startswith('refs/'), pending_ref
2759 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2760 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2761 code = 0
2762 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002763 max_attempts = 3
2764 attempts_left = max_attempts
2765 while attempts_left:
2766 if attempts_left != max_attempts:
2767 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2768 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002769
2770 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002771 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002772 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002773 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002774 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002775 print 'Fetch failed with exit code %d.' % code
2776 if out.strip():
2777 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002778 continue
2779
2780 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002781 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002782 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002783 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002784 if code:
2785 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002786 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2787 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002788 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2789 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002790 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002791 return code, out
2792
2793 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002794 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002795 code, out = RunGitWithCode(
2796 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2797 if code == 0:
2798 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002799 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002800 return code, out
2801
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002802 print 'Push failed with exit code %d.' % code
2803 if out.strip():
2804 print out.strip()
2805 if IsFatalPushFailure(out):
2806 print (
2807 'Fatal push error. Make sure your .netrc credentials and git '
2808 'user.email are correct and you have push access to the repo.')
2809 return code, out
2810
2811 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002812 return code, out
2813
2814
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002815def IsFatalPushFailure(push_stdout):
2816 """True if retrying push won't help."""
2817 return '(prohibited by Gerrit)' in push_stdout
2818
2819
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002820@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002821def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002822 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002823 if not settings.GetIsGitSvn():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002824 if get_footer_svn_id():
2825 # If it looks like previous commits were mirrored with git-svn.
2826 message = """This repository appears to be a git-svn mirror, but no
2827upstream SVN master is set. You probably need to run 'git auto-svn' once."""
2828 else:
2829 message = """This doesn't appear to be an SVN repository.
2830If your project has a true, writeable git repository, you probably want to run
2831'git cl land' instead.
2832If your project has a git mirror of an upstream SVN master, you probably need
2833to run 'git svn init'.
2834
2835Using the wrong command might cause your commit to appear to succeed, and the
2836review to be closed, without actually landing upstream. If you choose to
2837proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002838 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002839 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002840 return SendUpstream(parser, args, 'dcommit')
2841
2842
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002843@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002844def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002845 """Commits the current changelist via git."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002846 if settings.GetIsGitSvn() or get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002847 print('This appears to be an SVN repository.')
2848 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002849 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00002850 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002851 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002852
2853
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002854@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002855def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002856 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002857 parser.add_option('-b', dest='newbranch',
2858 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002859 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002860 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002861 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2862 help='Change to the directory DIR immediately, '
2863 'before doing anything else.')
2864 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002865 help='failed patches spew .rej files rather than '
2866 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002867 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2868 help="don't commit after patch applies")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002869 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002870 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002871 auth_config = auth.extract_auth_config_from_options(options)
2872
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002873 if len(args) != 1:
2874 parser.print_help()
2875 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002876 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002877
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002878 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002879 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002880 return 1
2881
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002882 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002883 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002884
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002885 if options.newbranch:
2886 if options.force:
2887 RunGit(['branch', '-D', options.newbranch],
2888 stderr=subprocess2.PIPE, error_ok=True)
2889 RunGit(['checkout', '-b', options.newbranch,
2890 Changelist().GetUpstreamBranch()])
2891
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002892 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002893 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002894
2895
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002896def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00002897 # PatchIssue should never be called with a dirty tree. It is up to the
2898 # caller to check this, but just in case we assert here since the
2899 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002900 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002901
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002902 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002903 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002904 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002905 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002906 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002907 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002908 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002909 # Assume it's a URL to the patch. Default to https.
2910 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00002911 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002912 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002913 DieWithError('Must pass an issue ID or full URL for '
2914 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00002915 issue = int(match.group(2))
2916 cl = Changelist(issue=issue, auth_config=auth_config)
2917 cl.rietveld_server = match.group(1)
2918 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002919 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002920
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002921 # Switch up to the top-level directory, if necessary, in preparation for
2922 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002923 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002924 if top:
2925 os.chdir(top)
2926
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002927 # Git patches have a/ at the beginning of source paths. We strip that out
2928 # with a sed script rather than the -p flag to patch so we can feed either
2929 # Git or svn-style patches into the same apply command.
2930 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002931 try:
2932 patch_data = subprocess2.check_output(
2933 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2934 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002935 DieWithError('Git patch mungling failed.')
2936 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002937
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002938 # We use "git apply" to apply the patch instead of "patch" so that we can
2939 # pick up file adds.
2940 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002941 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002942 if directory:
2943 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002944 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002945 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002946 elif IsGitVersionAtLeast('1.7.12'):
2947 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002948 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002949 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002950 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002951 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00002952 print 'Failed to apply the patch'
2953 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002954
2955 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002956 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00002957 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
2958 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00002959 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2960 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002961 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002962 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002963 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002964 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002965 else:
2966 print "Patch applied to index."
2967 return 0
2968
2969
2970def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002971 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002972 # Provide a wrapper for git svn rebase to help avoid accidental
2973 # git svn dcommit.
2974 # It's the only command that doesn't use parser at all since we just defer
2975 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002976
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002977 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002978
2979
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002980def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002981 """Fetches the tree status and returns either 'open', 'closed',
2982 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002983 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002984 if url:
2985 status = urllib2.urlopen(url).read().lower()
2986 if status.find('closed') != -1 or status == '0':
2987 return 'closed'
2988 elif status.find('open') != -1 or status == '1':
2989 return 'open'
2990 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002991 return 'unset'
2992
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002993
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002994def GetTreeStatusReason():
2995 """Fetches the tree status from a json url and returns the message
2996 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002997 url = settings.GetTreeStatusUrl()
2998 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002999 connection = urllib2.urlopen(json_url)
3000 status = json.loads(connection.read())
3001 connection.close()
3002 return status['message']
3003
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003004
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003005def GetBuilderMaster(bot_list):
3006 """For a given builder, fetch the master from AE if available."""
3007 map_url = 'https://builders-map.appspot.com/'
3008 try:
3009 master_map = json.load(urllib2.urlopen(map_url))
3010 except urllib2.URLError as e:
3011 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3012 (map_url, e))
3013 except ValueError as e:
3014 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3015 if not master_map:
3016 return None, 'Failed to build master map.'
3017
3018 result_master = ''
3019 for bot in bot_list:
3020 builder = bot.split(':', 1)[0]
3021 master_list = master_map.get(builder, [])
3022 if not master_list:
3023 return None, ('No matching master for builder %s.' % builder)
3024 elif len(master_list) > 1:
3025 return None, ('The builder name %s exists in multiple masters %s.' %
3026 (builder, master_list))
3027 else:
3028 cur_master = master_list[0]
3029 if not result_master:
3030 result_master = cur_master
3031 elif result_master != cur_master:
3032 return None, 'The builders do not belong to the same master.'
3033 return result_master, None
3034
3035
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003036def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003037 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003038 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003039 status = GetTreeStatus()
3040 if 'unset' == status:
3041 print 'You must configure your tree status URL by running "git cl config".'
3042 return 2
3043
3044 print "The tree is %s" % status
3045 print
3046 print GetTreeStatusReason()
3047 if status != 'open':
3048 return 1
3049 return 0
3050
3051
maruel@chromium.org15192402012-09-06 12:38:29 +00003052def CMDtry(parser, args):
3053 """Triggers a try job through Rietveld."""
3054 group = optparse.OptionGroup(parser, "Try job options")
3055 group.add_option(
3056 "-b", "--bot", action="append",
3057 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3058 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003059 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003060 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003061 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003062 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003063 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003064 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003065 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003066 "-r", "--revision",
3067 help="Revision to use for the try job; default: the "
3068 "revision will be determined by the try server; see "
3069 "its waterfall for more info")
3070 group.add_option(
3071 "-c", "--clobber", action="store_true", default=False,
3072 help="Force a clobber before building; e.g. don't do an "
3073 "incremental build")
3074 group.add_option(
3075 "--project",
3076 help="Override which project to use. Projects are defined "
3077 "server-side to define what default bot set to use")
3078 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003079 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003080 group.add_option(
3081 "--use-buildbucket", action="store_true", default=False,
3082 help="Use buildbucket to trigger try jobs.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003083 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003084 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003085 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003086 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003087
3088 if args:
3089 parser.error('Unknown arguments: %s' % args)
3090
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003091 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003092 if not cl.GetIssue():
3093 parser.error('Need to upload first')
3094
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003095 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003096 if props.get('closed'):
3097 parser.error('Cannot send tryjobs for a closed CL')
3098
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003099 if props.get('private'):
3100 parser.error('Cannot use trybots with private issue')
3101
maruel@chromium.org15192402012-09-06 12:38:29 +00003102 if not options.name:
3103 options.name = cl.GetBranch()
3104
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003105 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003106 options.master, err_msg = GetBuilderMaster(options.bot)
3107 if err_msg:
3108 parser.error('Tryserver master cannot be found because: %s\n'
3109 'Please manually specify the tryserver master'
3110 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003111
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003112 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003113 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003114 if not options.bot:
3115 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003116
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003117 # Get try masters from PRESUBMIT.py files.
3118 masters = presubmit_support.DoGetTryMasters(
3119 change,
3120 change.LocalPaths(),
3121 settings.GetRoot(),
3122 None,
3123 None,
3124 options.verbose,
3125 sys.stdout)
3126 if masters:
3127 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003128
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003129 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3130 options.bot = presubmit_support.DoGetTrySlaves(
3131 change,
3132 change.LocalPaths(),
3133 settings.GetRoot(),
3134 None,
3135 None,
3136 options.verbose,
3137 sys.stdout)
3138 if not options.bot:
3139 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003140
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003141 builders_and_tests = {}
3142 # TODO(machenbach): The old style command-line options don't support
3143 # multiple try masters yet.
3144 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3145 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3146
3147 for bot in old_style:
3148 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003149 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003150 elif ',' in bot:
3151 parser.error('Specify one bot per --bot flag')
3152 else:
3153 builders_and_tests.setdefault(bot, []).append('defaulttests')
3154
3155 for bot, tests in new_style:
3156 builders_and_tests.setdefault(bot, []).extend(tests)
3157
3158 # Return a master map with one master to be backwards compatible. The
3159 # master name defaults to an empty string, which will cause the master
3160 # not to be set on rietveld (deprecated).
3161 return {options.master: builders_and_tests}
3162
3163 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003164
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003165 for builders in masters.itervalues():
3166 if any('triggered' in b for b in builders):
3167 print >> sys.stderr, (
3168 'ERROR You are trying to send a job to a triggered bot. This type of'
3169 ' bot requires an\ninitial job from a parent (usually a builder). '
3170 'Instead send your job to the parent.\n'
3171 'Bot list: %s' % builders)
3172 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003173
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003174 patchset = cl.GetMostRecentPatchset()
3175 if patchset and patchset != cl.GetPatchset():
3176 print(
3177 '\nWARNING Mismatch between local config and server. Did a previous '
3178 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3179 'Continuing using\npatchset %s.\n' % patchset)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003180 if options.use_buildbucket:
3181 try:
3182 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3183 except BuildbucketResponseException as ex:
3184 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003185 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003186 except Exception as e:
3187 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3188 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3189 e, stacktrace)
3190 return 1
3191 else:
3192 try:
3193 cl.RpcServer().trigger_distributed_try_jobs(
3194 cl.GetIssue(), patchset, options.name, options.clobber,
3195 options.revision, masters)
3196 except urllib2.HTTPError as e:
3197 if e.code == 404:
3198 print('404 from rietveld; '
3199 'did you mean to use "git try" instead of "git cl try"?')
3200 return 1
3201 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003202
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003203 for (master, builders) in sorted(masters.iteritems()):
3204 if master:
3205 print 'Master: %s' % master
3206 length = max(len(builder) for builder in builders)
3207 for builder in sorted(builders):
3208 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003209 return 0
3210
3211
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003212@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003213def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003214 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003215 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003216 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003217 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003218
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003219 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003220 if args:
3221 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003222 branch = cl.GetBranch()
3223 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003224 cl = Changelist()
3225 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003226
3227 # Clear configured merge-base, if there is one.
3228 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003229 else:
3230 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003231 return 0
3232
3233
thestig@chromium.org00858c82013-12-02 23:08:03 +00003234def CMDweb(parser, args):
3235 """Opens the current CL in the web browser."""
3236 _, args = parser.parse_args(args)
3237 if args:
3238 parser.error('Unrecognized args: %s' % ' '.join(args))
3239
3240 issue_url = Changelist().GetIssueURL()
3241 if not issue_url:
3242 print >> sys.stderr, 'ERROR No issue to open'
3243 return 1
3244
3245 webbrowser.open(issue_url)
3246 return 0
3247
3248
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003249def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003250 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003251 auth.add_auth_options(parser)
3252 options, args = parser.parse_args(args)
3253 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003254 if args:
3255 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003256 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003257 props = cl.GetIssueProperties()
3258 if props.get('private'):
3259 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003260 cl.SetFlag('commit', '1')
3261 return 0
3262
3263
groby@chromium.org411034a2013-02-26 15:12:01 +00003264def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003265 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003266 auth.add_auth_options(parser)
3267 options, args = parser.parse_args(args)
3268 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003269 if args:
3270 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003271 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003272 # Ensure there actually is an issue to close.
3273 cl.GetDescription()
3274 cl.CloseIssue()
3275 return 0
3276
3277
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003278def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003279 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003280 auth.add_auth_options(parser)
3281 options, args = parser.parse_args(args)
3282 auth_config = auth.extract_auth_config_from_options(options)
3283 if args:
3284 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003285
3286 # Uncommitted (staged and unstaged) changes will be destroyed by
3287 # "git reset --hard" if there are merging conflicts in PatchIssue().
3288 # Staged changes would be committed along with the patch from last
3289 # upload, hence counted toward the "last upload" side in the final
3290 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003291 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003292 return 1
3293
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003294 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003295 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003296 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003297 if not issue:
3298 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003299 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003300 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003301
3302 # Create a new branch based on the merge-base
3303 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3304 try:
3305 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003306 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003307 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003308 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003309 return rtn
3310
wychen@chromium.org06928532015-02-03 02:11:29 +00003311 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003312 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003313 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003314 finally:
3315 RunGit(['checkout', '-q', branch])
3316 RunGit(['branch', '-D', TMP_BRANCH])
3317
3318 return 0
3319
3320
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003321def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003322 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003323 parser.add_option(
3324 '--no-color',
3325 action='store_true',
3326 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003327 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003328 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003329 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003330
3331 author = RunGit(['config', 'user.email']).strip() or None
3332
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003333 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003334
3335 if args:
3336 if len(args) > 1:
3337 parser.error('Unknown args')
3338 base_branch = args[0]
3339 else:
3340 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003341 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003342
3343 change = cl.GetChange(base_branch, None)
3344 return owners_finder.OwnersFinder(
3345 [f.LocalPath() for f in
3346 cl.GetChange(base_branch, None).AffectedFiles()],
3347 change.RepositoryRoot(), author,
3348 fopen=file, os_path=os.path, glob=glob.glob,
3349 disable_color=options.no_color).run()
3350
3351
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003352def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
3353 """Generates a diff command."""
3354 # Generate diff for the current branch's changes.
3355 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3356 upstream_commit, '--' ]
3357
3358 if args:
3359 for arg in args:
3360 if os.path.isdir(arg):
3361 diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
3362 elif os.path.isfile(arg):
3363 diff_cmd.append(arg)
3364 else:
3365 DieWithError('Argument "%s" is not a file or a directory' % arg)
3366 else:
3367 diff_cmd.extend('*' + ext for ext in extensions)
3368
3369 return diff_cmd
3370
3371
enne@chromium.org555cfe42014-01-29 18:21:39 +00003372@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003373def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003374 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003375 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003376 parser.add_option('--full', action='store_true',
3377 help='Reformat the full content of all touched files')
3378 parser.add_option('--dry-run', action='store_true',
3379 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003380 parser.add_option('--python', action='store_true',
3381 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003382 parser.add_option('--diff', action='store_true',
3383 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003384 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003385
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003386 # git diff generates paths against the root of the repository. Change
3387 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003388 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003389 if rel_base_path:
3390 os.chdir(rel_base_path)
3391
digit@chromium.org29e47272013-05-17 17:01:46 +00003392 # Grab the merge-base commit, i.e. the upstream commit of the current
3393 # branch when it was created or the last time it was rebased. This is
3394 # to cover the case where the user may have called "git fetch origin",
3395 # moving the origin branch to a newer commit, but hasn't rebased yet.
3396 upstream_commit = None
3397 cl = Changelist()
3398 upstream_branch = cl.GetUpstreamBranch()
3399 if upstream_branch:
3400 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3401 upstream_commit = upstream_commit.strip()
3402
3403 if not upstream_commit:
3404 DieWithError('Could not find base commit for this branch. '
3405 'Are you in detached state?')
3406
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003407 if opts.full:
3408 # Only list the names of modified files.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003409 diff_type = '--name-only'
enne@chromium.org555cfe42014-01-29 18:21:39 +00003410 else:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003411 # Only generate context-less patches.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003412 diff_type = '-U0'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003413
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003414 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, CLANG_EXTS)
digit@chromium.org29e47272013-05-17 17:01:46 +00003415 diff_output = RunGit(diff_cmd)
3416
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003417 top_dir = os.path.normpath(
3418 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3419
3420 # Locate the clang-format binary in the checkout
3421 try:
3422 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3423 except clang_format.NotFoundError, e:
3424 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003425
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003426 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3427 # formatted. This is used to block during the presubmit.
3428 return_value = 0
3429
digit@chromium.org29e47272013-05-17 17:01:46 +00003430 if opts.full:
3431 # diff_output is a list of files to send to clang-format.
3432 files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003433 if files:
3434 cmd = [clang_format_tool]
3435 if not opts.dry_run and not opts.diff:
3436 cmd.append('-i')
3437 stdout = RunCommand(cmd + files, cwd=top_dir)
3438 if opts.diff:
3439 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003440 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003441 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003442 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00003443 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003444 try:
3445 script = clang_format.FindClangFormatScriptInChromiumTree(
3446 'clang-format-diff.py')
3447 except clang_format.NotFoundError, e:
3448 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003449
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003450 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003451 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003452 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003453
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003454 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003455 if opts.diff:
3456 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003457 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003458 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003459
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003460 # Similar code to above, but using yapf on .py files rather than clang-format
3461 # on C/C++ files
3462 if opts.python:
3463 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, ['.py'])
3464 diff_output = RunGit(diff_cmd)
3465 yapf_tool = gclient_utils.FindExecutable('yapf')
3466 if yapf_tool is None:
3467 DieWithError('yapf not found in PATH')
3468
3469 if opts.full:
3470 files = diff_output.splitlines()
3471 if files:
3472 cmd = [yapf_tool]
3473 if not opts.dry_run and not opts.diff:
3474 cmd.append('-i')
3475 stdout = RunCommand(cmd + files, cwd=top_dir)
3476 if opts.diff:
3477 sys.stdout.write(stdout)
3478 else:
3479 # TODO(sbc): yapf --lines mode still has some issues.
3480 # https://github.com/google/yapf/issues/154
3481 DieWithError('--python currently only works with --full')
3482
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003483 # Build a diff command that only operates on dart files. dart's formatter
3484 # does not have the nice property of only operating on modified chunks, so
3485 # hard code full.
3486 dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3487 args, ['.dart'])
3488 dart_diff_output = RunGit(dart_diff_cmd)
3489 if dart_diff_output:
3490 try:
3491 command = [dart_format.FindDartFmtToolInChromiumTree()]
3492 if not opts.dry_run and not opts.diff:
3493 command.append('-w')
3494 command.extend(dart_diff_output.splitlines())
3495
3496 stdout = RunCommand(command, cwd=top_dir, env=env)
3497 if opts.dry_run and stdout:
3498 return_value = 2
3499 except dart_format.NotFoundError as e:
3500 print ('Unable to check dart code formatting. Dart SDK is not in ' +
3501 'this checkout.')
3502
3503 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003504
3505
maruel@chromium.org29404b52014-09-08 22:58:00 +00003506def CMDlol(parser, args):
3507 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003508 print zlib.decompress(base64.b64decode(
3509 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3510 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3511 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3512 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003513 return 0
3514
3515
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003516class OptionParser(optparse.OptionParser):
3517 """Creates the option parse and add --verbose support."""
3518 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003519 optparse.OptionParser.__init__(
3520 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003521 self.add_option(
3522 '-v', '--verbose', action='count', default=0,
3523 help='Use 2 times for more debugging info')
3524
3525 def parse_args(self, args=None, values=None):
3526 options, args = optparse.OptionParser.parse_args(self, args, values)
3527 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3528 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3529 return options, args
3530
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003531
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003532def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003533 if sys.hexversion < 0x02060000:
3534 print >> sys.stderr, (
3535 '\nYour python version %s is unsupported, please upgrade.\n' %
3536 sys.version.split(' ', 1)[0])
3537 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003538
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003539 # Reload settings.
3540 global settings
3541 settings = Settings()
3542
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003543 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003544 dispatcher = subcommand.CommandDispatcher(__name__)
3545 try:
3546 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003547 except auth.AuthenticationError as e:
3548 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003549 except urllib2.HTTPError, e:
3550 if e.code != 500:
3551 raise
3552 DieWithError(
3553 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3554 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003555 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003556
3557
3558if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003559 # These affect sys.stdout so do it outside of main() to simplify mocks in
3560 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003561 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003562 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003563 try:
3564 sys.exit(main(sys.argv[1:]))
3565 except KeyboardInterrupt:
3566 sys.stderr.write('interrupted\n')
3567 sys.exit(1)