blob: bad7c57ac83b666c04e8c59d49add3575fd173d2 [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.com5626a922015-02-26 14:03:30 +0000557 def GetRunPostUploadHook(self):
558 run_post_upload_hook = self._GetRietveldConfig(
559 'run-post-upload-hook', error_ok=True)
560 return run_post_upload_hook == "True"
561
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000562 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000563 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000564
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000565 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000566 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000567
ukai@chromium.orge8077812012-02-03 03:41:46 +0000568 def GetIsGerrit(self):
569 """Return true if this repo is assosiated with gerrit code review system."""
570 if self.is_gerrit is None:
571 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
572 return self.is_gerrit
573
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000574 def GetGitEditor(self):
575 """Return the editor specified in the git config, or None if none is."""
576 if self.git_editor is None:
577 self.git_editor = self._GetConfig('core.editor', error_ok=True)
578 return self.git_editor or None
579
thestig@chromium.org44202a22014-03-11 19:22:18 +0000580 def GetLintRegex(self):
581 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
582 DEFAULT_LINT_REGEX)
583
584 def GetLintIgnoreRegex(self):
585 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
586 DEFAULT_LINT_IGNORE_REGEX)
587
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000588 def GetProject(self):
589 if not self.project:
590 self.project = self._GetRietveldConfig('project', error_ok=True)
591 return self.project
592
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000593 def GetForceHttpsCommitUrl(self):
594 if not self.force_https_commit_url:
595 self.force_https_commit_url = self._GetRietveldConfig(
596 'force-https-commit-url', error_ok=True)
597 return self.force_https_commit_url
598
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000599 def GetPendingRefPrefix(self):
600 if not self.pending_ref_prefix:
601 self.pending_ref_prefix = self._GetRietveldConfig(
602 'pending-ref-prefix', error_ok=True)
603 return self.pending_ref_prefix
604
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000605 def _GetRietveldConfig(self, param, **kwargs):
606 return self._GetConfig('rietveld.' + param, **kwargs)
607
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 def _GetConfig(self, param, **kwargs):
609 self.LazyUpdateIfNeeded()
610 return RunGit(['config', param], **kwargs).strip()
611
612
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000613def ShortBranchName(branch):
614 """Convert a name like 'refs/heads/foo' to just 'foo'."""
615 return branch.replace('refs/heads/', '')
616
617
618class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000619 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000620 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000621 global settings
622 if not settings:
623 # Happens when git_cl.py is used as a utility library.
624 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000625 settings.GetDefaultServerUrl()
626 self.branchref = branchref
627 if self.branchref:
628 self.branch = ShortBranchName(self.branchref)
629 else:
630 self.branch = None
631 self.rietveld_server = None
632 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000633 self.lookedup_issue = False
634 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000635 self.has_description = False
636 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000637 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000638 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000639 self.cc = None
640 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000641 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000642 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000643 self._remote = None
644 self._rpc_server = None
645
646 @property
647 def auth_config(self):
648 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000649
650 def GetCCList(self):
651 """Return the users cc'd on this CL.
652
653 Return is a string suitable for passing to gcl with the --cc flag.
654 """
655 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000656 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000657 more_cc = ','.join(self.watchers)
658 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
659 return self.cc
660
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000661 def GetCCListWithoutDefault(self):
662 """Return the users cc'd on this CL excluding default ones."""
663 if self.cc is None:
664 self.cc = ','.join(self.watchers)
665 return self.cc
666
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000667 def SetWatchers(self, watchers):
668 """Set the list of email addresses that should be cc'd based on the changed
669 files in this CL.
670 """
671 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000672
673 def GetBranch(self):
674 """Returns the short branch name, e.g. 'master'."""
675 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000676 branchref = RunGit(['symbolic-ref', 'HEAD'],
677 stderr=subprocess2.VOID, error_ok=True).strip()
678 if not branchref:
679 return None
680 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000681 self.branch = ShortBranchName(self.branchref)
682 return self.branch
683
684 def GetBranchRef(self):
685 """Returns the full branch name, e.g. 'refs/heads/master'."""
686 self.GetBranch() # Poke the lazy loader.
687 return self.branchref
688
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000689 @staticmethod
690 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000691 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000692 e.g. 'origin', 'refs/heads/master'
693 """
694 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000695 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
696 error_ok=True).strip()
697 if upstream_branch:
698 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
699 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000700 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
701 error_ok=True).strip()
702 if upstream_branch:
703 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000704 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000705 # Fall back on trying a git-svn upstream branch.
706 if settings.GetIsGitSvn():
707 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000708 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000709 # Else, try to guess the origin remote.
710 remote_branches = RunGit(['branch', '-r']).split()
711 if 'origin/master' in remote_branches:
712 # Fall back on origin/master if it exits.
713 remote = 'origin'
714 upstream_branch = 'refs/heads/master'
715 elif 'origin/trunk' in remote_branches:
716 # Fall back on origin/trunk if it exists. Generally a shared
717 # git-svn clone
718 remote = 'origin'
719 upstream_branch = 'refs/heads/trunk'
720 else:
721 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000722Either pass complete "git diff"-style arguments, like
723 git cl upload origin/master
724or verify this branch is set up to track another (via the --track argument to
725"git checkout -b ...").""")
726
727 return remote, upstream_branch
728
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000729 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000730 upstream_branch = self.GetUpstreamBranch()
731 if not BranchExists(upstream_branch):
732 DieWithError('The upstream for the current branch (%s) does not exist '
733 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000734 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000735 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000736
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000737 def GetUpstreamBranch(self):
738 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000739 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000740 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000741 upstream_branch = upstream_branch.replace('refs/heads/',
742 'refs/remotes/%s/' % remote)
743 upstream_branch = upstream_branch.replace('refs/branch-heads/',
744 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000745 self.upstream_branch = upstream_branch
746 return self.upstream_branch
747
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000748 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000749 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000750 remote, branch = None, self.GetBranch()
751 seen_branches = set()
752 while branch not in seen_branches:
753 seen_branches.add(branch)
754 remote, branch = self.FetchUpstreamTuple(branch)
755 branch = ShortBranchName(branch)
756 if remote != '.' or branch.startswith('refs/remotes'):
757 break
758 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000759 remotes = RunGit(['remote'], error_ok=True).split()
760 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000761 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000762 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000763 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000764 logging.warning('Could not determine which remote this change is '
765 'associated with, so defaulting to "%s". This may '
766 'not be what you want. You may prevent this message '
767 'by running "git svn info" as documented here: %s',
768 self._remote,
769 GIT_INSTRUCTIONS_URL)
770 else:
771 logging.warn('Could not determine which remote this change is '
772 'associated with. You may prevent this message by '
773 'running "git svn info" as documented here: %s',
774 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000775 branch = 'HEAD'
776 if branch.startswith('refs/remotes'):
777 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000778 elif branch.startswith('refs/branch-heads/'):
779 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000780 else:
781 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000782 return self._remote
783
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000784 def GitSanityChecks(self, upstream_git_obj):
785 """Checks git repo status and ensures diff is from local commits."""
786
sbc@chromium.org79706062015-01-14 21:18:12 +0000787 if upstream_git_obj is None:
788 if self.GetBranch() is None:
789 print >> sys.stderr, (
790 'ERROR: unable to dertermine current branch (detached HEAD?)')
791 else:
792 print >> sys.stderr, (
793 'ERROR: no upstream branch')
794 return False
795
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000796 # Verify the commit we're diffing against is in our current branch.
797 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
798 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
799 if upstream_sha != common_ancestor:
800 print >> sys.stderr, (
801 'ERROR: %s is not in the current branch. You may need to rebase '
802 'your tracking branch' % upstream_sha)
803 return False
804
805 # List the commits inside the diff, and verify they are all local.
806 commits_in_diff = RunGit(
807 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
808 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
809 remote_branch = remote_branch.strip()
810 if code != 0:
811 _, remote_branch = self.GetRemoteBranch()
812
813 commits_in_remote = RunGit(
814 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
815
816 common_commits = set(commits_in_diff) & set(commits_in_remote)
817 if common_commits:
818 print >> sys.stderr, (
819 'ERROR: Your diff contains %d commits already in %s.\n'
820 'Run "git log --oneline %s..HEAD" to get a list of commits in '
821 'the diff. If you are using a custom git flow, you can override'
822 ' the reference used for this check with "git config '
823 'gitcl.remotebranch <git-ref>".' % (
824 len(common_commits), remote_branch, upstream_git_obj))
825 return False
826 return True
827
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000828 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000829 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000830
831 Returns None if it is not set.
832 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000833 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
834 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000835
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000836 def GetGitSvnRemoteUrl(self):
837 """Return the configured git-svn remote URL parsed from git svn info.
838
839 Returns None if it is not set.
840 """
841 # URL is dependent on the current directory.
842 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
843 if data:
844 keys = dict(line.split(': ', 1) for line in data.splitlines()
845 if ': ' in line)
846 return keys.get('URL', None)
847 return None
848
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000849 def GetRemoteUrl(self):
850 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
851
852 Returns None if there is no remote.
853 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000854 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000855 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
856
857 # If URL is pointing to a local directory, it is probably a git cache.
858 if os.path.isdir(url):
859 url = RunGit(['config', 'remote.%s.url' % remote],
860 error_ok=True,
861 cwd=url).strip()
862 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000863
864 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000865 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000866 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000867 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000868 self.issue = int(issue) or None if issue else None
869 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000870 return self.issue
871
872 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000873 if not self.rietveld_server:
874 # If we're on a branch then get the server potentially associated
875 # with that branch.
876 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000877 rietveld_server_config = self._RietveldServer()
878 if rietveld_server_config:
879 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
880 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000881 if not self.rietveld_server:
882 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000883 return self.rietveld_server
884
885 def GetIssueURL(self):
886 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000887 if not self.GetIssue():
888 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000889 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
890
891 def GetDescription(self, pretty=False):
892 if not self.has_description:
893 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000894 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000895 try:
896 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000897 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000898 if e.code == 404:
899 DieWithError(
900 ('\nWhile fetching the description for issue %d, received a '
901 '404 (not found)\n'
902 'error. It is likely that you deleted this '
903 'issue on the server. If this is the\n'
904 'case, please run\n\n'
905 ' git cl issue 0\n\n'
906 'to clear the association with the deleted issue. Then run '
907 'this command again.') % issue)
908 else:
909 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000910 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000911 except urllib2.URLError as e:
912 print >> sys.stderr, (
913 'Warning: Failed to retrieve CL description due to network '
914 'failure.')
915 self.description = ''
916
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000917 self.has_description = True
918 if pretty:
919 wrapper = textwrap.TextWrapper()
920 wrapper.initial_indent = wrapper.subsequent_indent = ' '
921 return wrapper.fill(self.description)
922 return self.description
923
924 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000925 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000926 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000927 patchset = RunGit(['config', self._PatchsetSetting()],
928 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000929 self.patchset = int(patchset) or None if patchset else None
930 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000931 return self.patchset
932
933 def SetPatchset(self, patchset):
934 """Set this branch's patchset. If patchset=0, clears the patchset."""
935 if patchset:
936 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000937 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938 else:
939 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000940 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000941 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000942
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000943 def GetMostRecentPatchset(self):
944 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000945
946 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000947 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000948 '/download/issue%s_%s.diff' % (issue, patchset))
949
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000950 def GetIssueProperties(self):
951 if self._props is None:
952 issue = self.GetIssue()
953 if not issue:
954 self._props = {}
955 else:
956 self._props = self.RpcServer().get_issue_properties(issue, True)
957 return self._props
958
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000959 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000960 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000961
apavlov@chromium.orge4efd512014-11-05 09:05:29 +0000962 def AddComment(self, message):
963 return self.RpcServer().add_comment(self.GetIssue(), message)
964
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000965 def SetIssue(self, issue):
966 """Set this branch's issue. If issue=0, clears the issue."""
967 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000968 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000969 RunGit(['config', self._IssueSetting(), str(issue)])
970 if self.rietveld_server:
971 RunGit(['config', self._RietveldServer(), self.rietveld_server])
972 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000973 current_issue = self.GetIssue()
974 if current_issue:
975 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000976 self.issue = None
977 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000978
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000979 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000980 if not self.GitSanityChecks(upstream_branch):
981 DieWithError('\nGit sanity check failure')
982
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000983 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000984 if not root:
985 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000986 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000987
988 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000989 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000990 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000991 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000992 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000993 except subprocess2.CalledProcessError:
994 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000995 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000996 'This branch probably doesn\'t exist anymore. To reset the\n'
997 'tracking branch, please run\n'
998 ' git branch --set-upstream %s trunk\n'
999 'replacing trunk with origin/master or the relevant branch') %
1000 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001001
maruel@chromium.org52424302012-08-29 15:14:30 +00001002 issue = self.GetIssue()
1003 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001004 if issue:
1005 description = self.GetDescription()
1006 else:
1007 # If the change was never uploaded, use the log messages of all commits
1008 # up to the branch point, as git cl upload will prefill the description
1009 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001010 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1011 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001012
1013 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001014 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001015 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001016 name,
1017 description,
1018 absroot,
1019 files,
1020 issue,
1021 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001022 author,
1023 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001024
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001025 def GetStatus(self):
1026 """Apply a rough heuristic to give a simple summary of an issue's review
1027 or CQ status, assuming adherence to a common workflow.
1028
1029 Returns None if no issue for this branch, or one of the following keywords:
1030 * 'error' - error from review tool (including deleted issues)
1031 * 'unsent' - not sent for review
1032 * 'waiting' - waiting for review
1033 * 'reply' - waiting for owner to reply to review
1034 * 'lgtm' - LGTM from at least one approved reviewer
1035 * 'commit' - in the commit queue
1036 * 'closed' - closed
1037 """
1038 if not self.GetIssue():
1039 return None
1040
1041 try:
1042 props = self.GetIssueProperties()
1043 except urllib2.HTTPError:
1044 return 'error'
1045
1046 if props.get('closed'):
1047 # Issue is closed.
1048 return 'closed'
1049 if props.get('commit'):
1050 # Issue is in the commit queue.
1051 return 'commit'
1052
1053 try:
1054 reviewers = self.GetApprovingReviewers()
1055 except urllib2.HTTPError:
1056 return 'error'
1057
1058 if reviewers:
1059 # Was LGTM'ed.
1060 return 'lgtm'
1061
1062 messages = props.get('messages') or []
1063
1064 if not messages:
1065 # No message was sent.
1066 return 'unsent'
1067 if messages[-1]['sender'] != props.get('owner_email'):
1068 # Non-LGTM reply from non-owner
1069 return 'reply'
1070 return 'waiting'
1071
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001072 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001073 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001074
1075 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001076 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001077 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001078 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001079 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001080 except presubmit_support.PresubmitFailure, e:
1081 DieWithError(
1082 ('%s\nMaybe your depot_tools is out of date?\n'
1083 'If all fails, contact maruel@') % e)
1084
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001085 def UpdateDescription(self, description):
1086 self.description = description
1087 return self.RpcServer().update_description(
1088 self.GetIssue(), self.description)
1089
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001091 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001092 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001093
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001094 def SetFlag(self, flag, value):
1095 """Patchset must match."""
1096 if not self.GetPatchset():
1097 DieWithError('The patchset needs to match. Send another patchset.')
1098 try:
1099 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001100 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001101 except urllib2.HTTPError, e:
1102 if e.code == 404:
1103 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1104 if e.code == 403:
1105 DieWithError(
1106 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1107 'match?') % (self.GetIssue(), self.GetPatchset()))
1108 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001109
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001110 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001111 """Returns an upload.RpcServer() to access this review's rietveld instance.
1112 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001113 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001114 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001115 self.GetRietveldServer(),
1116 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001117 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001118
1119 def _IssueSetting(self):
1120 """Return the git setting that stores this change's issue."""
1121 return 'branch.%s.rietveldissue' % self.GetBranch()
1122
1123 def _PatchsetSetting(self):
1124 """Return the git setting that stores this change's most recent patchset."""
1125 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1126
1127 def _RietveldServer(self):
1128 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001129 branch = self.GetBranch()
1130 if branch:
1131 return 'branch.%s.rietveldserver' % branch
1132 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001133
1134
1135def GetCodereviewSettingsInteractively():
1136 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001137 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 server = settings.GetDefaultServerUrl(error_ok=True)
1139 prompt = 'Rietveld server (host[:port])'
1140 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001141 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142 if not server and not newserver:
1143 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001144 if newserver:
1145 newserver = gclient_utils.UpgradeToHttps(newserver)
1146 if newserver != server:
1147 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001149 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001150 prompt = caption
1151 if initial:
1152 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001153 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 if new_val == 'x':
1155 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001156 elif new_val:
1157 if is_url:
1158 new_val = gclient_utils.UpgradeToHttps(new_val)
1159 if new_val != initial:
1160 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001161
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001162 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001163 SetProperty(settings.GetDefaultPrivateFlag(),
1164 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001166 'tree-status-url', False)
1167 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001168 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001169 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1170 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001171
1172 # TODO: configure a default branch to diff against, rather than this
1173 # svn-based hackery.
1174
1175
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001176class ChangeDescription(object):
1177 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001178 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001179 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001180
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001181 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001182 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001183
agable@chromium.org42c20792013-09-12 17:34:49 +00001184 @property # www.logilab.org/ticket/89786
1185 def description(self): # pylint: disable=E0202
1186 return '\n'.join(self._description_lines)
1187
1188 def set_description(self, desc):
1189 if isinstance(desc, basestring):
1190 lines = desc.splitlines()
1191 else:
1192 lines = [line.rstrip() for line in desc]
1193 while lines and not lines[0]:
1194 lines.pop(0)
1195 while lines and not lines[-1]:
1196 lines.pop(-1)
1197 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001198
piman@chromium.org336f9122014-09-04 02:16:55 +00001199 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001200 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001201 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001202 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001203 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001204 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001205
agable@chromium.org42c20792013-09-12 17:34:49 +00001206 # Get the set of R= and TBR= lines and remove them from the desciption.
1207 regexp = re.compile(self.R_LINE)
1208 matches = [regexp.match(line) for line in self._description_lines]
1209 new_desc = [l for i, l in enumerate(self._description_lines)
1210 if not matches[i]]
1211 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001212
agable@chromium.org42c20792013-09-12 17:34:49 +00001213 # Construct new unified R= and TBR= lines.
1214 r_names = []
1215 tbr_names = []
1216 for match in matches:
1217 if not match:
1218 continue
1219 people = cleanup_list([match.group(2).strip()])
1220 if match.group(1) == 'TBR':
1221 tbr_names.extend(people)
1222 else:
1223 r_names.extend(people)
1224 for name in r_names:
1225 if name not in reviewers:
1226 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001227 if add_owners_tbr:
1228 owners_db = owners.Database(change.RepositoryRoot(),
1229 fopen=file, os_path=os.path, glob=glob.glob)
1230 all_reviewers = set(tbr_names + reviewers)
1231 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1232 all_reviewers)
1233 tbr_names.extend(owners_db.reviewers_for(missing_files,
1234 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001235 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1236 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1237
1238 # Put the new lines in the description where the old first R= line was.
1239 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1240 if 0 <= line_loc < len(self._description_lines):
1241 if new_tbr_line:
1242 self._description_lines.insert(line_loc, new_tbr_line)
1243 if new_r_line:
1244 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001245 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001246 if new_r_line:
1247 self.append_footer(new_r_line)
1248 if new_tbr_line:
1249 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001250
1251 def prompt(self):
1252 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001253 self.set_description([
1254 '# Enter a description of the change.',
1255 '# This will be displayed on the codereview site.',
1256 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001257 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001258 '--------------------',
1259 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001260
agable@chromium.org42c20792013-09-12 17:34:49 +00001261 regexp = re.compile(self.BUG_LINE)
1262 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001263 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001264 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001265 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001266 if not content:
1267 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001268 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001269
1270 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001271 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1272 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001273 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001274 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001275
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001276 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001277 if self._description_lines:
1278 # Add an empty line if either the last line or the new line isn't a tag.
1279 last_line = self._description_lines[-1]
1280 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1281 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1282 self._description_lines.append('')
1283 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001284
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001285 def get_reviewers(self):
1286 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001287 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1288 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001289 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001290
1291
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001292def get_approving_reviewers(props):
1293 """Retrieves the reviewers that approved a CL from the issue properties with
1294 messages.
1295
1296 Note that the list may contain reviewers that are not committer, thus are not
1297 considered by the CQ.
1298 """
1299 return sorted(
1300 set(
1301 message['sender']
1302 for message in props['messages']
1303 if message['approval'] and message['sender'] in props['reviewers']
1304 )
1305 )
1306
1307
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001308def FindCodereviewSettingsFile(filename='codereview.settings'):
1309 """Finds the given file starting in the cwd and going up.
1310
1311 Only looks up to the top of the repository unless an
1312 'inherit-review-settings-ok' file exists in the root of the repository.
1313 """
1314 inherit_ok_file = 'inherit-review-settings-ok'
1315 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001316 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001317 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1318 root = '/'
1319 while True:
1320 if filename in os.listdir(cwd):
1321 if os.path.isfile(os.path.join(cwd, filename)):
1322 return open(os.path.join(cwd, filename))
1323 if cwd == root:
1324 break
1325 cwd = os.path.dirname(cwd)
1326
1327
1328def LoadCodereviewSettingsFromFile(fileobj):
1329 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001330 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001331
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001332 def SetProperty(name, setting, unset_error_ok=False):
1333 fullname = 'rietveld.' + name
1334 if setting in keyvals:
1335 RunGit(['config', fullname, keyvals[setting]])
1336 else:
1337 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1338
1339 SetProperty('server', 'CODE_REVIEW_SERVER')
1340 # Only server setting is required. Other settings can be absent.
1341 # In that case, we ignore errors raised during option deletion attempt.
1342 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001343 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001344 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1345 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001346 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001347 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001348 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1349 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001350 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001351 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001352 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001353 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1354 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001356 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001357 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001358
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001359 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1360 #should be of the form
1361 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1362 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1363 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1364 keyvals['ORIGIN_URL_CONFIG']])
1365
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001367def urlretrieve(source, destination):
1368 """urllib is broken for SSL connections via a proxy therefore we
1369 can't use urllib.urlretrieve()."""
1370 with open(destination, 'w') as f:
1371 f.write(urllib2.urlopen(source).read())
1372
1373
ukai@chromium.org712d6102013-11-27 00:52:58 +00001374def hasSheBang(fname):
1375 """Checks fname is a #! script."""
1376 with open(fname) as f:
1377 return f.read(2).startswith('#!')
1378
1379
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001380def DownloadHooks(force):
1381 """downloads hooks
1382
1383 Args:
1384 force: True to update hooks. False to install hooks if not present.
1385 """
1386 if not settings.GetIsGerrit():
1387 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001388 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001389 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1390 if not os.access(dst, os.X_OK):
1391 if os.path.exists(dst):
1392 if not force:
1393 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001394 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001395 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001396 if not hasSheBang(dst):
1397 DieWithError('Not a script: %s\n'
1398 'You need to download from\n%s\n'
1399 'into .git/hooks/commit-msg and '
1400 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001401 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1402 except Exception:
1403 if os.path.exists(dst):
1404 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001405 DieWithError('\nFailed to download hooks.\n'
1406 'You need to download from\n%s\n'
1407 'into .git/hooks/commit-msg and '
1408 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001409
1410
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001411@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001413 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001415 parser.add_option('--activate-update', action='store_true',
1416 help='activate auto-updating [rietveld] section in '
1417 '.git/config')
1418 parser.add_option('--deactivate-update', action='store_true',
1419 help='deactivate auto-updating [rietveld] section in '
1420 '.git/config')
1421 options, args = parser.parse_args(args)
1422
1423 if options.deactivate_update:
1424 RunGit(['config', 'rietveld.autoupdate', 'false'])
1425 return
1426
1427 if options.activate_update:
1428 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1429 return
1430
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431 if len(args) == 0:
1432 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001433 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001434 return 0
1435
1436 url = args[0]
1437 if not url.endswith('codereview.settings'):
1438 url = os.path.join(url, 'codereview.settings')
1439
1440 # Load code review settings and download hooks (if available).
1441 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001442 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443 return 0
1444
1445
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001446def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001447 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001448 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1449 branch = ShortBranchName(branchref)
1450 _, args = parser.parse_args(args)
1451 if not args:
1452 print("Current base-url:")
1453 return RunGit(['config', 'branch.%s.base-url' % branch],
1454 error_ok=False).strip()
1455 else:
1456 print("Setting base-url to %s" % args[0])
1457 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1458 error_ok=False).strip()
1459
1460
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001461def color_for_status(status):
1462 """Maps a Changelist status to color, for CMDstatus and other tools."""
1463 return {
1464 'unsent': Fore.RED,
1465 'waiting': Fore.BLUE,
1466 'reply': Fore.YELLOW,
1467 'lgtm': Fore.GREEN,
1468 'commit': Fore.MAGENTA,
1469 'closed': Fore.CYAN,
1470 'error': Fore.WHITE,
1471 }.get(status, Fore.WHITE)
1472
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001473def fetch_cl_status(branch, auth_config=None):
1474 """Fetches information for an issue and returns (branch, issue, status)."""
1475 cl = Changelist(branchref=branch, auth_config=auth_config)
1476 url = cl.GetIssueURL()
1477 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001478
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001479 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001480 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001481 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001482
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001483 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001484
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001485def get_cl_statuses(
1486 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001487 """Returns a blocking iterable of (branch, issue, color) for given branches.
1488
1489 If fine_grained is true, this will fetch CL statuses from the server.
1490 Otherwise, simply indicate if there's a matching url for the given branches.
1491
1492 If max_processes is specified, it is used as the maximum number of processes
1493 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1494 spawned.
1495 """
1496 # Silence upload.py otherwise it becomes unwieldly.
1497 upload.verbosity = 0
1498
1499 if fine_grained:
1500 # Process one branch synchronously to work through authentication, then
1501 # spawn processes to process all the other branches in parallel.
1502 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001503 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1504 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001505
1506 branches_to_fetch = branches[1:]
1507 pool = ThreadPool(
1508 min(max_processes, len(branches_to_fetch))
1509 if max_processes is not None
1510 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001511 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001512 yield x
1513 else:
1514 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1515 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001516 cl = Changelist(branchref=b, auth_config=auth_config)
1517 url = cl.GetIssueURL()
1518 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001519
rmistry@google.com2dd99862015-06-22 12:22:18 +00001520
1521def upload_branch_deps(cl, args):
1522 """Uploads CLs of local branches that are dependents of the current branch.
1523
1524 If the local branch dependency tree looks like:
1525 test1 -> test2.1 -> test3.1
1526 -> test3.2
1527 -> test2.2 -> test3.3
1528
1529 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1530 run on the dependent branches in this order:
1531 test2.1, test3.1, test3.2, test2.2, test3.3
1532
1533 Note: This function does not rebase your local dependent branches. Use it when
1534 you make a change to the parent branch that will not conflict with its
1535 dependent branches, and you would like their dependencies updated in
1536 Rietveld.
1537 """
1538 if git_common.is_dirty_git_tree('upload-branch-deps'):
1539 return 1
1540
1541 root_branch = cl.GetBranch()
1542 if root_branch is None:
1543 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1544 'Get on a branch!')
1545 if not cl.GetIssue() or not cl.GetPatchset():
1546 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1547 'patchset dependencies without an uploaded CL.')
1548
1549 branches = RunGit(['for-each-ref',
1550 '--format=%(refname:short) %(upstream:short)',
1551 'refs/heads'])
1552 if not branches:
1553 print('No local branches found.')
1554 return 0
1555
1556 # Create a dictionary of all local branches to the branches that are dependent
1557 # on it.
1558 tracked_to_dependents = collections.defaultdict(list)
1559 for b in branches.splitlines():
1560 tokens = b.split()
1561 if len(tokens) == 2:
1562 branch_name, tracked = tokens
1563 tracked_to_dependents[tracked].append(branch_name)
1564
1565 print
1566 print 'The dependent local branches of %s are:' % root_branch
1567 dependents = []
1568 def traverse_dependents_preorder(branch, padding=''):
1569 dependents_to_process = tracked_to_dependents.get(branch, [])
1570 padding += ' '
1571 for dependent in dependents_to_process:
1572 print '%s%s' % (padding, dependent)
1573 dependents.append(dependent)
1574 traverse_dependents_preorder(dependent, padding)
1575 traverse_dependents_preorder(root_branch)
1576 print
1577
1578 if not dependents:
1579 print 'There are no dependent local branches for %s' % root_branch
1580 return 0
1581
1582 print ('This command will checkout all dependent branches and run '
1583 '"git cl upload".')
1584 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1585
1586 # Add a default patchset title to all upload calls.
1587 args.extend(['-t', 'Updated patchset dependency'])
1588 # Record all dependents that failed to upload.
1589 failures = {}
1590 # Go through all dependents, checkout the branch and upload.
1591 try:
1592 for dependent_branch in dependents:
1593 print
1594 print '--------------------------------------'
1595 print 'Running "git cl upload" from %s:' % dependent_branch
1596 RunGit(['checkout', '-q', dependent_branch])
1597 print
1598 try:
1599 if CMDupload(OptionParser(), args) != 0:
1600 print 'Upload failed for %s!' % dependent_branch
1601 failures[dependent_branch] = 1
1602 except: # pylint: disable=W0702
1603 failures[dependent_branch] = 1
1604 print
1605 finally:
1606 # Swap back to the original root branch.
1607 RunGit(['checkout', '-q', root_branch])
1608
1609 print
1610 print 'Upload complete for dependent branches!'
1611 for dependent_branch in dependents:
1612 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1613 print ' %s : %s' % (dependent_branch, upload_status)
1614 print
1615
1616 return 0
1617
1618
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001619def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001620 """Show status of changelists.
1621
1622 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001623 - Red not sent for review or broken
1624 - Blue waiting for review
1625 - Yellow waiting for you to reply to review
1626 - Green LGTM'ed
1627 - Magenta in the commit queue
1628 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001629
1630 Also see 'git cl comments'.
1631 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001632 parser.add_option('--field',
1633 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001634 parser.add_option('-f', '--fast', action='store_true',
1635 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001636 parser.add_option(
1637 '-j', '--maxjobs', action='store', type=int,
1638 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001639
1640 auth.add_auth_options(parser)
1641 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001642 if args:
1643 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001644 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001645
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001646 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001647 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001648 if options.field.startswith('desc'):
1649 print cl.GetDescription()
1650 elif options.field == 'id':
1651 issueid = cl.GetIssue()
1652 if issueid:
1653 print issueid
1654 elif options.field == 'patch':
1655 patchset = cl.GetPatchset()
1656 if patchset:
1657 print patchset
1658 elif options.field == 'url':
1659 url = cl.GetIssueURL()
1660 if url:
1661 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001662 return 0
1663
1664 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1665 if not branches:
1666 print('No local branch found.')
1667 return 0
1668
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001669 changes = (
1670 Changelist(branchref=b, auth_config=auth_config)
1671 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001672 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001673 alignment = max(5, max(len(b) for b in branches))
1674 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001675 output = get_cl_statuses(branches,
1676 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001677 max_processes=options.maxjobs,
1678 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001679
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001680 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001681 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001682 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001683 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001684 b, i, status = output.next()
1685 branch_statuses[b] = (i, status)
1686 issue_url, status = branch_statuses.pop(branch)
1687 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001688 reset = Fore.RESET
1689 if not sys.stdout.isatty():
1690 color = ''
1691 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001692 status_str = '(%s)' % status if status else ''
1693 print ' %*s : %s%s %s%s' % (
1694 alignment, ShortBranchName(branch), color, issue_url, status_str,
1695 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001696
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001697 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001698 print
1699 print 'Current branch:',
1700 if not cl.GetIssue():
1701 print 'no issue assigned.'
1702 return 0
1703 print cl.GetBranch()
1704 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001705 if not options.fast:
1706 print 'Issue description:'
1707 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001708 return 0
1709
1710
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001711def colorize_CMDstatus_doc():
1712 """To be called once in main() to add colors to git cl status help."""
1713 colors = [i for i in dir(Fore) if i[0].isupper()]
1714
1715 def colorize_line(line):
1716 for color in colors:
1717 if color in line.upper():
1718 # Extract whitespaces first and the leading '-'.
1719 indent = len(line) - len(line.lstrip(' ')) + 1
1720 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1721 return line
1722
1723 lines = CMDstatus.__doc__.splitlines()
1724 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1725
1726
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001727@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001728def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001729 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001730
1731 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001732 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001733 parser.add_option('-r', '--reverse', action='store_true',
1734 help='Lookup the branch(es) for the specified issues. If '
1735 'no issues are specified, all branches with mapped '
1736 'issues will be listed.')
1737 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001738
dnj@chromium.org406c4402015-03-03 17:22:28 +00001739 if options.reverse:
1740 branches = RunGit(['for-each-ref', 'refs/heads',
1741 '--format=%(refname:short)']).splitlines()
1742
1743 # Reverse issue lookup.
1744 issue_branch_map = {}
1745 for branch in branches:
1746 cl = Changelist(branchref=branch)
1747 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1748 if not args:
1749 args = sorted(issue_branch_map.iterkeys())
1750 for issue in args:
1751 if not issue:
1752 continue
1753 print 'Branch for issue number %s: %s' % (
1754 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1755 else:
1756 cl = Changelist()
1757 if len(args) > 0:
1758 try:
1759 issue = int(args[0])
1760 except ValueError:
1761 DieWithError('Pass a number to set the issue or none to list it.\n'
1762 'Maybe you want to run git cl status?')
1763 cl.SetIssue(issue)
1764 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001765 return 0
1766
1767
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001768def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001769 """Shows or posts review comments for any changelist."""
1770 parser.add_option('-a', '--add-comment', dest='comment',
1771 help='comment to add to an issue')
1772 parser.add_option('-i', dest='issue',
1773 help="review issue id (defaults to current issue)")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001774 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001775 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001776 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001777
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001778 issue = None
1779 if options.issue:
1780 try:
1781 issue = int(options.issue)
1782 except ValueError:
1783 DieWithError('A review issue id is expected to be a number')
1784
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001785 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001786
1787 if options.comment:
1788 cl.AddComment(options.comment)
1789 return 0
1790
1791 data = cl.GetIssueProperties()
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001792 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001793 if message['disapproval']:
1794 color = Fore.RED
1795 elif message['approval']:
1796 color = Fore.GREEN
1797 elif message['sender'] == data['owner_email']:
1798 color = Fore.MAGENTA
1799 else:
1800 color = Fore.BLUE
1801 print '\n%s%s %s%s' % (
1802 color, message['date'].split('.', 1)[0], message['sender'],
1803 Fore.RESET)
1804 if message['text'].strip():
1805 print '\n'.join(' ' + l for l in message['text'].splitlines())
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001806 return 0
1807
1808
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001809def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001810 """Brings up the editor for the current CL's description."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001811 auth.add_auth_options(parser)
1812 options, _ = parser.parse_args(args)
1813 auth_config = auth.extract_auth_config_from_options(options)
1814 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001815 if not cl.GetIssue():
1816 DieWithError('This branch has no associated changelist.')
1817 description = ChangeDescription(cl.GetDescription())
1818 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00001819 if cl.GetDescription() != description.description:
1820 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001821 return 0
1822
1823
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001824def CreateDescriptionFromLog(args):
1825 """Pulls out the commit log to use as a base for the CL description."""
1826 log_args = []
1827 if len(args) == 1 and not args[0].endswith('.'):
1828 log_args = [args[0] + '..']
1829 elif len(args) == 1 and args[0].endswith('...'):
1830 log_args = [args[0][:-1]]
1831 elif len(args) == 2:
1832 log_args = [args[0] + '..' + args[1]]
1833 else:
1834 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001835 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001836
1837
thestig@chromium.org44202a22014-03-11 19:22:18 +00001838def CMDlint(parser, args):
1839 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001840 parser.add_option('--filter', action='append', metavar='-x,+y',
1841 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001842 auth.add_auth_options(parser)
1843 options, args = parser.parse_args(args)
1844 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001845
1846 # Access to a protected member _XX of a client class
1847 # pylint: disable=W0212
1848 try:
1849 import cpplint
1850 import cpplint_chromium
1851 except ImportError:
1852 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1853 return 1
1854
1855 # Change the current working directory before calling lint so that it
1856 # shows the correct base.
1857 previous_cwd = os.getcwd()
1858 os.chdir(settings.GetRoot())
1859 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001860 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001861 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1862 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001863 if not files:
1864 print "Cannot lint an empty CL"
1865 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001866
1867 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001868 command = args + files
1869 if options.filter:
1870 command = ['--filter=' + ','.join(options.filter)] + command
1871 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001872
1873 white_regex = re.compile(settings.GetLintRegex())
1874 black_regex = re.compile(settings.GetLintIgnoreRegex())
1875 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1876 for filename in filenames:
1877 if white_regex.match(filename):
1878 if black_regex.match(filename):
1879 print "Ignoring file %s" % filename
1880 else:
1881 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1882 extra_check_functions)
1883 else:
1884 print "Skipping file %s" % filename
1885 finally:
1886 os.chdir(previous_cwd)
1887 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1888 if cpplint._cpplint_state.error_count != 0:
1889 return 1
1890 return 0
1891
1892
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001893def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001894 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001895 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001896 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001897 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001898 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001899 auth.add_auth_options(parser)
1900 options, args = parser.parse_args(args)
1901 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001902
sbc@chromium.org71437c02015-04-09 19:29:40 +00001903 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00001904 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001905 return 1
1906
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001907 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001908 if args:
1909 base_branch = args[0]
1910 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001911 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001912 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001913
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001914 cl.RunHook(
1915 committing=not options.upload,
1916 may_prompt=False,
1917 verbose=options.verbose,
1918 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001919 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001920
1921
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001922def AddChangeIdToCommitMessage(options, args):
1923 """Re-commits using the current message, assumes the commit hook is in
1924 place.
1925 """
1926 log_desc = options.message or CreateDescriptionFromLog(args)
1927 git_command = ['commit', '--amend', '-m', log_desc]
1928 RunGit(git_command)
1929 new_log_desc = CreateDescriptionFromLog(args)
1930 if CHANGE_ID in new_log_desc:
1931 print 'git-cl: Added Change-Id to commit message.'
1932 else:
1933 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1934
1935
piman@chromium.org336f9122014-09-04 02:16:55 +00001936def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001937 """upload the current branch to gerrit."""
1938 # We assume the remote called "origin" is the one we want.
1939 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001940 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00001941
1942 remote, remote_branch = cl.GetRemoteBranch()
1943 branch = GetTargetRef(remote, remote_branch, options.target_branch,
1944 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001945
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001946 change_desc = ChangeDescription(
1947 options.message or CreateDescriptionFromLog(args))
1948 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001949 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001950 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001951
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001952 if options.squash:
1953 # Try to get the message from a previous upload.
1954 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
1955 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
1956 if not message:
1957 if not options.force:
1958 change_desc.prompt()
1959
1960 if CHANGE_ID not in change_desc.description:
1961 # Run the commit-msg hook without modifying the head commit by writing
1962 # the commit message to a temporary file and running the hook over it,
1963 # then reading the file back in.
1964 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
1965 'commit-msg')
1966 file_handle, msg_file = tempfile.mkstemp(text=True,
1967 prefix='commit_msg')
1968 try:
1969 try:
1970 with os.fdopen(file_handle, 'w') as fileobj:
1971 fileobj.write(change_desc.description)
1972 finally:
1973 os.close(file_handle)
1974 RunCommand([commit_msg_hook, msg_file])
1975 change_desc.set_description(gclient_utils.FileRead(msg_file))
1976 finally:
1977 os.remove(msg_file)
1978
1979 if not change_desc.description:
1980 print "Description is empty; aborting."
1981 return 1
1982
1983 message = change_desc.description
1984
1985 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1986 if remote is '.':
1987 # If our upstream branch is local, we base our squashed commit on its
1988 # squashed version.
1989 parent = ('refs/heads/git_cl_uploads/' +
1990 scm.GIT.ShortBranchName(upstream_branch))
1991
1992 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
1993 # will create additional CLs when uploading.
1994 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
1995 RunGitSilent(['rev-parse', parent + ':'])):
1996 print 'Upload upstream branch ' + upstream_branch + ' first.'
1997 return 1
1998 else:
1999 parent = cl.GetCommonAncestorWithUpstream()
2000
2001 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2002 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2003 '-m', message]).strip()
2004 else:
2005 if CHANGE_ID not in change_desc.description:
2006 AddChangeIdToCommitMessage(options, args)
2007 ref_to_push = 'HEAD'
2008 parent = '%s/%s' % (gerrit_remote, branch)
2009
2010 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2011 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002012 if len(commits) > 1:
2013 print('WARNING: This will upload %d commits. Run the following command '
2014 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002015 print('git log %s..%s' % (parent, ref_to_push))
2016 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002017 'commit.')
2018 ask_for_data('About to upload; enter to confirm.')
2019
piman@chromium.org336f9122014-09-04 02:16:55 +00002020 if options.reviewers or options.tbr_owners:
2021 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002022
ukai@chromium.orge8077812012-02-03 03:41:46 +00002023 receive_options = []
2024 cc = cl.GetCCList().split(',')
2025 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002026 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002027 cc = filter(None, cc)
2028 if cc:
2029 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002030 if change_desc.get_reviewers():
2031 receive_options.extend(
2032 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002033
ukai@chromium.orge8077812012-02-03 03:41:46 +00002034 git_command = ['push']
2035 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002036 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002037 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002038 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002039 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002040
2041 if options.squash:
2042 head = RunGit(['rev-parse', 'HEAD']).strip()
2043 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2044
ukai@chromium.orge8077812012-02-03 03:41:46 +00002045 # TODO(ukai): parse Change-Id: and set issue number?
2046 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002047
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002048
wittman@chromium.org455dc922015-01-26 20:15:50 +00002049def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2050 """Computes the remote branch ref to use for the CL.
2051
2052 Args:
2053 remote (str): The git remote for the CL.
2054 remote_branch (str): The git remote branch for the CL.
2055 target_branch (str): The target branch specified by the user.
2056 pending_prefix (str): The pending prefix from the settings.
2057 """
2058 if not (remote and remote_branch):
2059 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002060
wittman@chromium.org455dc922015-01-26 20:15:50 +00002061 if target_branch:
2062 # Cannonicalize branch references to the equivalent local full symbolic
2063 # refs, which are then translated into the remote full symbolic refs
2064 # below.
2065 if '/' not in target_branch:
2066 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2067 else:
2068 prefix_replacements = (
2069 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2070 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2071 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2072 )
2073 match = None
2074 for regex, replacement in prefix_replacements:
2075 match = re.search(regex, target_branch)
2076 if match:
2077 remote_branch = target_branch.replace(match.group(0), replacement)
2078 break
2079 if not match:
2080 # This is a branch path but not one we recognize; use as-is.
2081 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002082 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2083 # Handle the refs that need to land in different refs.
2084 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002085
wittman@chromium.org455dc922015-01-26 20:15:50 +00002086 # Create the true path to the remote branch.
2087 # Does the following translation:
2088 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2089 # * refs/remotes/origin/master -> refs/heads/master
2090 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2091 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2092 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2093 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2094 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2095 'refs/heads/')
2096 elif remote_branch.startswith('refs/remotes/branch-heads'):
2097 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2098 # If a pending prefix exists then replace refs/ with it.
2099 if pending_prefix:
2100 remote_branch = remote_branch.replace('refs/', pending_prefix)
2101 return remote_branch
2102
2103
piman@chromium.org336f9122014-09-04 02:16:55 +00002104def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002105 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002106 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2107 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002108 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002109 if options.emulate_svn_auto_props:
2110 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002111
2112 change_desc = None
2113
pgervais@chromium.org91141372014-01-09 23:27:20 +00002114 if options.email is not None:
2115 upload_args.extend(['--email', options.email])
2116
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002117 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002118 if options.title:
2119 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002120 if options.message:
2121 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002122 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002123 print ("This branch is associated with issue %s. "
2124 "Adding patch to that issue." % cl.GetIssue())
2125 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002126 if options.title:
2127 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002128 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002129 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002130 if options.reviewers or options.tbr_owners:
2131 change_desc.update_reviewers(options.reviewers,
2132 options.tbr_owners,
2133 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002134 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002135 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002136
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002137 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002138 print "Description is empty; aborting."
2139 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002140
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002141 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002142 if change_desc.get_reviewers():
2143 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002144 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002145 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002146 DieWithError("Must specify reviewers to send email.")
2147 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002148
2149 # We check this before applying rietveld.private assuming that in
2150 # rietveld.cc only addresses which we can send private CLs to are listed
2151 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2152 # --private is specified explicitly on the command line.
2153 if options.private:
2154 logging.warn('rietveld.cc is ignored since private flag is specified. '
2155 'You need to review and add them manually if necessary.')
2156 cc = cl.GetCCListWithoutDefault()
2157 else:
2158 cc = cl.GetCCList()
2159 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002160 if cc:
2161 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002162
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002163 if options.private or settings.GetDefaultPrivateFlag() == "True":
2164 upload_args.append('--private')
2165
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002166 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002167 if not options.find_copies:
2168 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002169
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002170 # Include the upstream repo's URL in the change -- this is useful for
2171 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002172 remote_url = cl.GetGitBaseUrlFromConfig()
2173 if not remote_url:
2174 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002175 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002176 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002177 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2178 remote_url = (cl.GetRemoteUrl() + '@'
2179 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002180 if remote_url:
2181 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002182 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002183 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2184 settings.GetPendingRefPrefix())
2185 if target_ref:
2186 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002187
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002188 # Look for dependent patchsets. See crbug.com/480453 for more details.
2189 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2190 upstream_branch = ShortBranchName(upstream_branch)
2191 if remote is '.':
2192 # A local branch is being tracked.
2193 local_branch = ShortBranchName(upstream_branch)
2194 auth_config = auth.extract_auth_config_from_options(options)
2195 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2196 branch_cl_issue_url = branch_cl.GetIssueURL()
2197 branch_cl_issue = branch_cl.GetIssue()
2198 branch_cl_patchset = branch_cl.GetPatchset()
2199 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2200 upload_args.extend(
2201 ['--depends_on_patchset', '%s:%s' % (
2202 branch_cl_issue, branch_cl_patchset)])
2203 print
2204 print ('The current branch (%s) is tracking a local branch (%s) with '
rmistry@google.coma6dee742015-06-25 00:13:06 +00002205 'an associated CL.') % (cl.GetBranch(), local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002206 print 'Adding %s/#ps%s as a dependency patchset.' % (
2207 branch_cl_issue_url, branch_cl_patchset)
2208 print
2209
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002210 project = settings.GetProject()
2211 if project:
2212 upload_args.extend(['--project', project])
2213
rmistry@google.comef966222015-04-07 11:15:01 +00002214 if options.cq_dry_run:
2215 upload_args.extend(['--cq_dry_run'])
2216
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002217 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002218 upload_args = ['upload'] + upload_args + args
2219 logging.info('upload.RealMain(%s)', upload_args)
2220 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002221 issue = int(issue)
2222 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002223 except KeyboardInterrupt:
2224 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002225 except:
2226 # If we got an exception after the user typed a description for their
2227 # change, back up the description before re-raising.
2228 if change_desc:
2229 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2230 print '\nGot exception while uploading -- saving description to %s\n' \
2231 % backup_path
2232 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002233 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002234 backup_file.close()
2235 raise
2236
2237 if not cl.GetIssue():
2238 cl.SetIssue(issue)
2239 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002240
2241 if options.use_commit_queue:
2242 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002243 return 0
2244
2245
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002246def cleanup_list(l):
2247 """Fixes a list so that comma separated items are put as individual items.
2248
2249 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2250 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2251 """
2252 items = sum((i.split(',') for i in l), [])
2253 stripped_items = (i.strip() for i in items)
2254 return sorted(filter(None, stripped_items))
2255
2256
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002257@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002258def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002259 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00002260 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2261 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002262 parser.add_option('--bypass-watchlists', action='store_true',
2263 dest='bypass_watchlists',
2264 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002265 parser.add_option('-f', action='store_true', dest='force',
2266 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002267 parser.add_option('-m', dest='message', help='message for patchset')
2268 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002269 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002270 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002271 help='reviewer email addresses')
2272 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002273 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002274 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002275 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002276 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002277 parser.add_option('--emulate_svn_auto_props',
2278 '--emulate-svn-auto-props',
2279 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002280 dest="emulate_svn_auto_props",
2281 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002282 parser.add_option('-c', '--use-commit-queue', action='store_true',
2283 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002284 parser.add_option('--private', action='store_true',
2285 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002286 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002287 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002288 metavar='TARGET',
2289 help='Apply CL to remote ref TARGET. ' +
2290 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002291 parser.add_option('--squash', action='store_true',
2292 help='Squash multiple commits into one (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002293 parser.add_option('--email', default=None,
2294 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002295 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2296 help='add a set of OWNERS to TBR')
rmistry@google.comef966222015-04-07 11:15:01 +00002297 parser.add_option('--cq-dry-run', dest='cq_dry_run', action='store_true',
2298 help='Send the patchset to do a CQ dry run right after '
2299 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002300 parser.add_option('--dependencies', action='store_true',
2301 help='Uploads CLs of all the local branches that depend on '
2302 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002303
rmistry@google.com2dd99862015-06-22 12:22:18 +00002304 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002305 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002306 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002307 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002308 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002309
sbc@chromium.org71437c02015-04-09 19:29:40 +00002310 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002311 return 1
2312
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002313 options.reviewers = cleanup_list(options.reviewers)
2314 options.cc = cleanup_list(options.cc)
2315
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002316 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002317 if args:
2318 # TODO(ukai): is it ok for gerrit case?
2319 base_branch = args[0]
2320 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002321 if cl.GetBranch() is None:
2322 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2323
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002324 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002325 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002326 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002327
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002328 # Make sure authenticated to Rietveld before running expensive hooks. It is
2329 # a fast, best efforts check. Rietveld still can reject the authentication
2330 # during the actual upload.
2331 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2332 authenticator = auth.get_authenticator_for_host(
2333 cl.GetRietveldServer(), auth_config)
2334 if not authenticator.has_cached_credentials():
2335 raise auth.LoginRequiredError(cl.GetRietveldServer())
2336
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002337 # Apply watchlists on upload.
2338 change = cl.GetChange(base_branch, None)
2339 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2340 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002341 if not options.bypass_watchlists:
2342 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002343
ukai@chromium.orge8077812012-02-03 03:41:46 +00002344 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002345 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002346 # Set the reviewer list now so that presubmit checks can access it.
2347 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002348 change_description.update_reviewers(options.reviewers,
2349 options.tbr_owners,
2350 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002351 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002352 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002353 may_prompt=not options.force,
2354 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002355 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002356 if not hook_results.should_continue():
2357 return 1
2358 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002359 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002360
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002361 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002362 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002363 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002364 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002365 print ('The last upload made from this repository was patchset #%d but '
2366 'the most recent patchset on the server is #%d.'
2367 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002368 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2369 'from another machine or branch the patch you\'re uploading now '
2370 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002371 ask_for_data('About to upload; enter to confirm.')
2372
iannucci@chromium.org79540052012-10-19 23:15:26 +00002373 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002374 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00002375 return GerritUpload(options, args, cl, change)
2376 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002377 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002378 git_set_branch_value('last-upload-hash',
2379 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002380 # Run post upload hooks, if specified.
2381 if settings.GetRunPostUploadHook():
2382 presubmit_support.DoPostUploadExecuter(
2383 change,
2384 cl,
2385 settings.GetRoot(),
2386 options.verbose,
2387 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002388
rmistry@google.com2dd99862015-06-22 12:22:18 +00002389 # Upload all dependencies if specified.
2390 if options.dependencies:
2391 print
2392 print '--dependencies has been specified.'
2393 print 'All dependent local branches will be re-uploaded.'
2394 print
2395 # Remove the dependencies flag from args so that we do not end up in a
2396 # loop.
2397 orig_args.remove('--dependencies')
2398 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002399 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002400
2401
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002402def IsSubmoduleMergeCommit(ref):
2403 # When submodules are added to the repo, we expect there to be a single
2404 # non-git-svn merge commit at remote HEAD with a signature comment.
2405 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002406 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002407 return RunGit(cmd) != ''
2408
2409
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002410def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002411 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002412
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002413 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002414 Updates changelog with metadata (e.g. pointer to review).
2415 Pushes/dcommits the code upstream.
2416 Updates review and closes.
2417 """
2418 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2419 help='bypass upload presubmit hook')
2420 parser.add_option('-m', dest='message',
2421 help="override review description")
2422 parser.add_option('-f', action='store_true', dest='force',
2423 help="force yes to questions (don't prompt)")
2424 parser.add_option('-c', dest='contributor',
2425 help="external contributor for patch (appended to " +
2426 "description and used as author for git). Should be " +
2427 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002428 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002429 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002430 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002431 auth_config = auth.extract_auth_config_from_options(options)
2432
2433 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002434
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002435 current = cl.GetBranch()
2436 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2437 if not settings.GetIsGitSvn() and remote == '.':
2438 print
2439 print 'Attempting to push branch %r into another local branch!' % current
2440 print
2441 print 'Either reparent this branch on top of origin/master:'
2442 print ' git reparent-branch --root'
2443 print
2444 print 'OR run `git rebase-update` if you think the parent branch is already'
2445 print 'committed.'
2446 print
2447 print ' Current parent: %r' % upstream_branch
2448 return 1
2449
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002450 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002451 # Default to merging against our best guess of the upstream branch.
2452 args = [cl.GetUpstreamBranch()]
2453
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002454 if options.contributor:
2455 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2456 print "Please provide contibutor as 'First Last <email@example.com>'"
2457 return 1
2458
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002459 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002460 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002461
sbc@chromium.org71437c02015-04-09 19:29:40 +00002462 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002463 return 1
2464
2465 # This rev-list syntax means "show all commits not in my branch that
2466 # are in base_branch".
2467 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2468 base_branch]).splitlines()
2469 if upstream_commits:
2470 print ('Base branch "%s" has %d commits '
2471 'not in this branch.' % (base_branch, len(upstream_commits)))
2472 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2473 return 1
2474
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002475 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002476 svn_head = None
2477 if cmd == 'dcommit' or base_has_submodules:
2478 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2479 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002480
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002481 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002482 # If the base_head is a submodule merge commit, the first parent of the
2483 # base_head should be a git-svn commit, which is what we're interested in.
2484 base_svn_head = base_branch
2485 if base_has_submodules:
2486 base_svn_head += '^1'
2487
2488 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002489 if extra_commits:
2490 print ('This branch has %d additional commits not upstreamed yet.'
2491 % len(extra_commits.splitlines()))
2492 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2493 'before attempting to %s.' % (base_branch, cmd))
2494 return 1
2495
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002496 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002497 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002498 author = None
2499 if options.contributor:
2500 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002501 hook_results = cl.RunHook(
2502 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002503 may_prompt=not options.force,
2504 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002505 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002506 if not hook_results.should_continue():
2507 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002508
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002509 # Check the tree status if the tree status URL is set.
2510 status = GetTreeStatus()
2511 if 'closed' == status:
2512 print('The tree is closed. Please wait for it to reopen. Use '
2513 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2514 return 1
2515 elif 'unknown' == status:
2516 print('Unable to determine tree status. Please verify manually and '
2517 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2518 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002519 else:
2520 breakpad.SendStack(
2521 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002522 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2523 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002524 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002525
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002526 change_desc = ChangeDescription(options.message)
2527 if not change_desc.description and cl.GetIssue():
2528 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002529
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002530 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002531 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002532 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002533 else:
2534 print 'No description set.'
2535 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2536 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002537
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002538 # Keep a separate copy for the commit message, because the commit message
2539 # contains the link to the Rietveld issue, while the Rietveld message contains
2540 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002541 # Keep a separate copy for the commit message.
2542 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002543 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002544
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002545 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002546 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002547 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002548 # after it. Add a period on a new line to circumvent this. Also add a space
2549 # before the period to make sure that Gitiles continues to correctly resolve
2550 # the URL.
2551 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002552 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002553 commit_desc.append_footer('Patch from %s.' % options.contributor)
2554
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002555 print('Description:')
2556 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002557
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002558 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002559 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002560 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002561
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002562 # We want to squash all this branch's commits into one commit with the proper
2563 # description. We do this by doing a "reset --soft" to the base branch (which
2564 # keeps the working copy the same), then dcommitting that. If origin/master
2565 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2566 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002567 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002568 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2569 # Delete the branches if they exist.
2570 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2571 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2572 result = RunGitWithCode(showref_cmd)
2573 if result[0] == 0:
2574 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002575
2576 # We might be in a directory that's present in this branch but not in the
2577 # trunk. Move up to the top of the tree so that git commands that expect a
2578 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002579 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002580 if rel_base_path:
2581 os.chdir(rel_base_path)
2582
2583 # Stuff our change into the merge branch.
2584 # We wrap in a try...finally block so if anything goes wrong,
2585 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002586 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002587 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002588 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002589 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002590 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002591 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002592 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002593 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002594 RunGit(
2595 [
2596 'commit', '--author', options.contributor,
2597 '-m', commit_desc.description,
2598 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002599 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002600 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002601 if base_has_submodules:
2602 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2603 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2604 RunGit(['checkout', CHERRY_PICK_BRANCH])
2605 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002606 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002607 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002608 pending_prefix = settings.GetPendingRefPrefix()
2609 if not pending_prefix or branch.startswith(pending_prefix):
2610 # If not using refs/pending/heads/* at all, or target ref is already set
2611 # to pending, then push to the target ref directly.
2612 retcode, output = RunGitWithCode(
2613 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002614 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002615 else:
2616 # Cherry-pick the change on top of pending ref and then push it.
2617 assert branch.startswith('refs/'), branch
2618 assert pending_prefix[-1] == '/', pending_prefix
2619 pending_ref = pending_prefix + branch[len('refs/'):]
2620 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002621 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002622 if retcode == 0:
2623 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002624 else:
2625 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002626 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002627 'svn', 'dcommit',
2628 '-C%s' % options.similarity,
2629 '--no-rebase', '--rmdir',
2630 ]
2631 if settings.GetForceHttpsCommitUrl():
2632 # Allow forcing https commit URLs for some projects that don't allow
2633 # committing to http URLs (like Google Code).
2634 remote_url = cl.GetGitSvnRemoteUrl()
2635 if urlparse.urlparse(remote_url).scheme == 'http':
2636 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002637 cmd_args.append('--commit-url=%s' % remote_url)
2638 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002639 if 'Committed r' in output:
2640 revision = re.match(
2641 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2642 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002643 finally:
2644 # And then swap back to the original branch and clean up.
2645 RunGit(['checkout', '-q', cl.GetBranch()])
2646 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002647 if base_has_submodules:
2648 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002649
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002650 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002651 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002652 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002653
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002654 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002655 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002656 try:
2657 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2658 # We set pushed_to_pending to False, since it made it all the way to the
2659 # real ref.
2660 pushed_to_pending = False
2661 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002662 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002663
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002664 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002665 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002666 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002667 if not to_pending:
2668 if viewvc_url and revision:
2669 change_desc.append_footer(
2670 'Committed: %s%s' % (viewvc_url, revision))
2671 elif revision:
2672 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002673 print ('Closing issue '
2674 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002675 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002676 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002677 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002678 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002679 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002680 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002681 if options.bypass_hooks:
2682 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2683 else:
2684 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002685 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002686 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002687
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002688 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002689 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2690 print 'The commit is in the pending queue (%s).' % pending_ref
2691 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002692 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002693 'footer.' % branch)
2694
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002695 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2696 if os.path.isfile(hook):
2697 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002698
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002699 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002700
2701
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002702def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2703 print
2704 print 'Waiting for commit to be landed on %s...' % real_ref
2705 print '(If you are impatient, you may Ctrl-C once without harm)'
2706 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2707 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2708
2709 loop = 0
2710 while True:
2711 sys.stdout.write('fetching (%d)... \r' % loop)
2712 sys.stdout.flush()
2713 loop += 1
2714
2715 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2716 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2717 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2718 for commit in commits.splitlines():
2719 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2720 print 'Found commit on %s' % real_ref
2721 return commit
2722
2723 current_rev = to_rev
2724
2725
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002726def PushToGitPending(remote, pending_ref, upstream_ref):
2727 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2728
2729 Returns:
2730 (retcode of last operation, output log of last operation).
2731 """
2732 assert pending_ref.startswith('refs/'), pending_ref
2733 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2734 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2735 code = 0
2736 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002737 max_attempts = 3
2738 attempts_left = max_attempts
2739 while attempts_left:
2740 if attempts_left != max_attempts:
2741 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2742 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002743
2744 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002745 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002746 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002747 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002748 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002749 print 'Fetch failed with exit code %d.' % code
2750 if out.strip():
2751 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002752 continue
2753
2754 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002755 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002756 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002757 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002758 if code:
2759 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002760 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2761 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002762 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2763 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002764 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002765 return code, out
2766
2767 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002768 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002769 code, out = RunGitWithCode(
2770 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2771 if code == 0:
2772 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002773 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002774 return code, out
2775
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002776 print 'Push failed with exit code %d.' % code
2777 if out.strip():
2778 print out.strip()
2779 if IsFatalPushFailure(out):
2780 print (
2781 'Fatal push error. Make sure your .netrc credentials and git '
2782 'user.email are correct and you have push access to the repo.')
2783 return code, out
2784
2785 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002786 return code, out
2787
2788
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002789def IsFatalPushFailure(push_stdout):
2790 """True if retrying push won't help."""
2791 return '(prohibited by Gerrit)' in push_stdout
2792
2793
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002794@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002795def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002796 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002797 if not settings.GetIsGitSvn():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002798 if get_footer_svn_id():
2799 # If it looks like previous commits were mirrored with git-svn.
2800 message = """This repository appears to be a git-svn mirror, but no
2801upstream SVN master is set. You probably need to run 'git auto-svn' once."""
2802 else:
2803 message = """This doesn't appear to be an SVN repository.
2804If your project has a true, writeable git repository, you probably want to run
2805'git cl land' instead.
2806If your project has a git mirror of an upstream SVN master, you probably need
2807to run 'git svn init'.
2808
2809Using the wrong command might cause your commit to appear to succeed, and the
2810review to be closed, without actually landing upstream. If you choose to
2811proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002812 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002813 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002814 return SendUpstream(parser, args, 'dcommit')
2815
2816
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002817@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002818def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002819 """Commits the current changelist via git."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002820 if settings.GetIsGitSvn() or get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002821 print('This appears to be an SVN repository.')
2822 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002823 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00002824 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002825 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002826
2827
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002828@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002829def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002830 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002831 parser.add_option('-b', dest='newbranch',
2832 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002833 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002834 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002835 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2836 help='Change to the directory DIR immediately, '
2837 'before doing anything else.')
2838 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002839 help='failed patches spew .rej files rather than '
2840 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002841 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2842 help="don't commit after patch applies")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002843 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002844 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002845 auth_config = auth.extract_auth_config_from_options(options)
2846
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002847 if len(args) != 1:
2848 parser.print_help()
2849 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002850 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002851
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002852 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002853 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002854 return 1
2855
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002856 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002857 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002858
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002859 if options.newbranch:
2860 if options.force:
2861 RunGit(['branch', '-D', options.newbranch],
2862 stderr=subprocess2.PIPE, error_ok=True)
2863 RunGit(['checkout', '-b', options.newbranch,
2864 Changelist().GetUpstreamBranch()])
2865
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002866 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002867 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002868
2869
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002870def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00002871 # PatchIssue should never be called with a dirty tree. It is up to the
2872 # caller to check this, but just in case we assert here since the
2873 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002874 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002875
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002876 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002877 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002878 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002879 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002880 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002881 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002882 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002883 # Assume it's a URL to the patch. Default to https.
2884 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00002885 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002886 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002887 DieWithError('Must pass an issue ID or full URL for '
2888 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00002889 issue = int(match.group(2))
2890 cl = Changelist(issue=issue, auth_config=auth_config)
2891 cl.rietveld_server = match.group(1)
2892 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002893 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002894
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002895 # Switch up to the top-level directory, if necessary, in preparation for
2896 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002897 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002898 if top:
2899 os.chdir(top)
2900
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002901 # Git patches have a/ at the beginning of source paths. We strip that out
2902 # with a sed script rather than the -p flag to patch so we can feed either
2903 # Git or svn-style patches into the same apply command.
2904 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002905 try:
2906 patch_data = subprocess2.check_output(
2907 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2908 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002909 DieWithError('Git patch mungling failed.')
2910 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002911
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002912 # We use "git apply" to apply the patch instead of "patch" so that we can
2913 # pick up file adds.
2914 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002915 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002916 if directory:
2917 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002918 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002919 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002920 elif IsGitVersionAtLeast('1.7.12'):
2921 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002922 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002923 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002924 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002925 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00002926 print 'Failed to apply the patch'
2927 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002928
2929 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002930 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00002931 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
2932 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00002933 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2934 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002935 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002936 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002937 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002938 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002939 else:
2940 print "Patch applied to index."
2941 return 0
2942
2943
2944def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002945 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002946 # Provide a wrapper for git svn rebase to help avoid accidental
2947 # git svn dcommit.
2948 # It's the only command that doesn't use parser at all since we just defer
2949 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002950
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002951 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002952
2953
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002954def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002955 """Fetches the tree status and returns either 'open', 'closed',
2956 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002957 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002958 if url:
2959 status = urllib2.urlopen(url).read().lower()
2960 if status.find('closed') != -1 or status == '0':
2961 return 'closed'
2962 elif status.find('open') != -1 or status == '1':
2963 return 'open'
2964 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002965 return 'unset'
2966
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002967
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002968def GetTreeStatusReason():
2969 """Fetches the tree status from a json url and returns the message
2970 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002971 url = settings.GetTreeStatusUrl()
2972 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002973 connection = urllib2.urlopen(json_url)
2974 status = json.loads(connection.read())
2975 connection.close()
2976 return status['message']
2977
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002978
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002979def GetBuilderMaster(bot_list):
2980 """For a given builder, fetch the master from AE if available."""
2981 map_url = 'https://builders-map.appspot.com/'
2982 try:
2983 master_map = json.load(urllib2.urlopen(map_url))
2984 except urllib2.URLError as e:
2985 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2986 (map_url, e))
2987 except ValueError as e:
2988 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2989 if not master_map:
2990 return None, 'Failed to build master map.'
2991
2992 result_master = ''
2993 for bot in bot_list:
2994 builder = bot.split(':', 1)[0]
2995 master_list = master_map.get(builder, [])
2996 if not master_list:
2997 return None, ('No matching master for builder %s.' % builder)
2998 elif len(master_list) > 1:
2999 return None, ('The builder name %s exists in multiple masters %s.' %
3000 (builder, master_list))
3001 else:
3002 cur_master = master_list[0]
3003 if not result_master:
3004 result_master = cur_master
3005 elif result_master != cur_master:
3006 return None, 'The builders do not belong to the same master.'
3007 return result_master, None
3008
3009
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003010def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003011 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003012 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003013 status = GetTreeStatus()
3014 if 'unset' == status:
3015 print 'You must configure your tree status URL by running "git cl config".'
3016 return 2
3017
3018 print "The tree is %s" % status
3019 print
3020 print GetTreeStatusReason()
3021 if status != 'open':
3022 return 1
3023 return 0
3024
3025
maruel@chromium.org15192402012-09-06 12:38:29 +00003026def CMDtry(parser, args):
3027 """Triggers a try job through Rietveld."""
3028 group = optparse.OptionGroup(parser, "Try job options")
3029 group.add_option(
3030 "-b", "--bot", action="append",
3031 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3032 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003033 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003034 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003035 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003036 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003037 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003038 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003039 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003040 "-r", "--revision",
3041 help="Revision to use for the try job; default: the "
3042 "revision will be determined by the try server; see "
3043 "its waterfall for more info")
3044 group.add_option(
3045 "-c", "--clobber", action="store_true", default=False,
3046 help="Force a clobber before building; e.g. don't do an "
3047 "incremental build")
3048 group.add_option(
3049 "--project",
3050 help="Override which project to use. Projects are defined "
3051 "server-side to define what default bot set to use")
3052 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003053 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003054 group.add_option(
3055 "--use-buildbucket", action="store_true", default=False,
3056 help="Use buildbucket to trigger try jobs.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003057 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003058 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003059 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003060 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003061
3062 if args:
3063 parser.error('Unknown arguments: %s' % args)
3064
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003065 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003066 if not cl.GetIssue():
3067 parser.error('Need to upload first')
3068
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003069 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003070 if props.get('closed'):
3071 parser.error('Cannot send tryjobs for a closed CL')
3072
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003073 if props.get('private'):
3074 parser.error('Cannot use trybots with private issue')
3075
maruel@chromium.org15192402012-09-06 12:38:29 +00003076 if not options.name:
3077 options.name = cl.GetBranch()
3078
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003079 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003080 options.master, err_msg = GetBuilderMaster(options.bot)
3081 if err_msg:
3082 parser.error('Tryserver master cannot be found because: %s\n'
3083 'Please manually specify the tryserver master'
3084 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003085
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003086 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003087 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003088 if not options.bot:
3089 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003090
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003091 # Get try masters from PRESUBMIT.py files.
3092 masters = presubmit_support.DoGetTryMasters(
3093 change,
3094 change.LocalPaths(),
3095 settings.GetRoot(),
3096 None,
3097 None,
3098 options.verbose,
3099 sys.stdout)
3100 if masters:
3101 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003102
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003103 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3104 options.bot = presubmit_support.DoGetTrySlaves(
3105 change,
3106 change.LocalPaths(),
3107 settings.GetRoot(),
3108 None,
3109 None,
3110 options.verbose,
3111 sys.stdout)
3112 if not options.bot:
3113 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003114
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003115 builders_and_tests = {}
3116 # TODO(machenbach): The old style command-line options don't support
3117 # multiple try masters yet.
3118 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3119 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3120
3121 for bot in old_style:
3122 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003123 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003124 elif ',' in bot:
3125 parser.error('Specify one bot per --bot flag')
3126 else:
3127 builders_and_tests.setdefault(bot, []).append('defaulttests')
3128
3129 for bot, tests in new_style:
3130 builders_and_tests.setdefault(bot, []).extend(tests)
3131
3132 # Return a master map with one master to be backwards compatible. The
3133 # master name defaults to an empty string, which will cause the master
3134 # not to be set on rietveld (deprecated).
3135 return {options.master: builders_and_tests}
3136
3137 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003138
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003139 for builders in masters.itervalues():
3140 if any('triggered' in b for b in builders):
3141 print >> sys.stderr, (
3142 'ERROR You are trying to send a job to a triggered bot. This type of'
3143 ' bot requires an\ninitial job from a parent (usually a builder). '
3144 'Instead send your job to the parent.\n'
3145 'Bot list: %s' % builders)
3146 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003147
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003148 patchset = cl.GetMostRecentPatchset()
3149 if patchset and patchset != cl.GetPatchset():
3150 print(
3151 '\nWARNING Mismatch between local config and server. Did a previous '
3152 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3153 'Continuing using\npatchset %s.\n' % patchset)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003154 if options.use_buildbucket:
3155 try:
3156 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3157 except BuildbucketResponseException as ex:
3158 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003159 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003160 except Exception as e:
3161 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3162 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3163 e, stacktrace)
3164 return 1
3165 else:
3166 try:
3167 cl.RpcServer().trigger_distributed_try_jobs(
3168 cl.GetIssue(), patchset, options.name, options.clobber,
3169 options.revision, masters)
3170 except urllib2.HTTPError as e:
3171 if e.code == 404:
3172 print('404 from rietveld; '
3173 'did you mean to use "git try" instead of "git cl try"?')
3174 return 1
3175 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003176
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003177 for (master, builders) in sorted(masters.iteritems()):
3178 if master:
3179 print 'Master: %s' % master
3180 length = max(len(builder) for builder in builders)
3181 for builder in sorted(builders):
3182 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003183 return 0
3184
3185
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003186@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003187def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003188 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003189 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003190 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003191 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003192
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003193 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003194 if args:
3195 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003196 branch = cl.GetBranch()
3197 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003198 cl = Changelist()
3199 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003200
3201 # Clear configured merge-base, if there is one.
3202 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003203 else:
3204 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003205 return 0
3206
3207
thestig@chromium.org00858c82013-12-02 23:08:03 +00003208def CMDweb(parser, args):
3209 """Opens the current CL in the web browser."""
3210 _, args = parser.parse_args(args)
3211 if args:
3212 parser.error('Unrecognized args: %s' % ' '.join(args))
3213
3214 issue_url = Changelist().GetIssueURL()
3215 if not issue_url:
3216 print >> sys.stderr, 'ERROR No issue to open'
3217 return 1
3218
3219 webbrowser.open(issue_url)
3220 return 0
3221
3222
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003223def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003224 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003225 auth.add_auth_options(parser)
3226 options, args = parser.parse_args(args)
3227 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003228 if args:
3229 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003230 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003231 props = cl.GetIssueProperties()
3232 if props.get('private'):
3233 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003234 cl.SetFlag('commit', '1')
3235 return 0
3236
3237
groby@chromium.org411034a2013-02-26 15:12:01 +00003238def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003239 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003240 auth.add_auth_options(parser)
3241 options, args = parser.parse_args(args)
3242 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003243 if args:
3244 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003245 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003246 # Ensure there actually is an issue to close.
3247 cl.GetDescription()
3248 cl.CloseIssue()
3249 return 0
3250
3251
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003252def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003253 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003254 auth.add_auth_options(parser)
3255 options, args = parser.parse_args(args)
3256 auth_config = auth.extract_auth_config_from_options(options)
3257 if args:
3258 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003259
3260 # Uncommitted (staged and unstaged) changes will be destroyed by
3261 # "git reset --hard" if there are merging conflicts in PatchIssue().
3262 # Staged changes would be committed along with the patch from last
3263 # upload, hence counted toward the "last upload" side in the final
3264 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003265 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003266 return 1
3267
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003268 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003269 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003270 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003271 if not issue:
3272 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003273 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003274 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003275
3276 # Create a new branch based on the merge-base
3277 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3278 try:
3279 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003280 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003281 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003282 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003283 return rtn
3284
wychen@chromium.org06928532015-02-03 02:11:29 +00003285 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003286 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003287 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003288 finally:
3289 RunGit(['checkout', '-q', branch])
3290 RunGit(['branch', '-D', TMP_BRANCH])
3291
3292 return 0
3293
3294
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003295def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003296 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003297 parser.add_option(
3298 '--no-color',
3299 action='store_true',
3300 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003301 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003302 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003303 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003304
3305 author = RunGit(['config', 'user.email']).strip() or None
3306
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003307 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003308
3309 if args:
3310 if len(args) > 1:
3311 parser.error('Unknown args')
3312 base_branch = args[0]
3313 else:
3314 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003315 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003316
3317 change = cl.GetChange(base_branch, None)
3318 return owners_finder.OwnersFinder(
3319 [f.LocalPath() for f in
3320 cl.GetChange(base_branch, None).AffectedFiles()],
3321 change.RepositoryRoot(), author,
3322 fopen=file, os_path=os.path, glob=glob.glob,
3323 disable_color=options.no_color).run()
3324
3325
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003326def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
3327 """Generates a diff command."""
3328 # Generate diff for the current branch's changes.
3329 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3330 upstream_commit, '--' ]
3331
3332 if args:
3333 for arg in args:
3334 if os.path.isdir(arg):
3335 diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
3336 elif os.path.isfile(arg):
3337 diff_cmd.append(arg)
3338 else:
3339 DieWithError('Argument "%s" is not a file or a directory' % arg)
3340 else:
3341 diff_cmd.extend('*' + ext for ext in extensions)
3342
3343 return diff_cmd
3344
3345
enne@chromium.org555cfe42014-01-29 18:21:39 +00003346@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003347def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003348 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003349 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003350 parser.add_option('--full', action='store_true',
3351 help='Reformat the full content of all touched files')
3352 parser.add_option('--dry-run', action='store_true',
3353 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003354 parser.add_option('--python', action='store_true',
3355 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003356 parser.add_option('--diff', action='store_true',
3357 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003358 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003359
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003360 # git diff generates paths against the root of the repository. Change
3361 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003362 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003363 if rel_base_path:
3364 os.chdir(rel_base_path)
3365
digit@chromium.org29e47272013-05-17 17:01:46 +00003366 # Grab the merge-base commit, i.e. the upstream commit of the current
3367 # branch when it was created or the last time it was rebased. This is
3368 # to cover the case where the user may have called "git fetch origin",
3369 # moving the origin branch to a newer commit, but hasn't rebased yet.
3370 upstream_commit = None
3371 cl = Changelist()
3372 upstream_branch = cl.GetUpstreamBranch()
3373 if upstream_branch:
3374 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3375 upstream_commit = upstream_commit.strip()
3376
3377 if not upstream_commit:
3378 DieWithError('Could not find base commit for this branch. '
3379 'Are you in detached state?')
3380
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003381 if opts.full:
3382 # Only list the names of modified files.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003383 diff_type = '--name-only'
enne@chromium.org555cfe42014-01-29 18:21:39 +00003384 else:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003385 # Only generate context-less patches.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003386 diff_type = '-U0'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003387
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003388 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, CLANG_EXTS)
digit@chromium.org29e47272013-05-17 17:01:46 +00003389 diff_output = RunGit(diff_cmd)
3390
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003391 top_dir = os.path.normpath(
3392 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3393
3394 # Locate the clang-format binary in the checkout
3395 try:
3396 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3397 except clang_format.NotFoundError, e:
3398 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003399
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003400 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3401 # formatted. This is used to block during the presubmit.
3402 return_value = 0
3403
digit@chromium.org29e47272013-05-17 17:01:46 +00003404 if opts.full:
3405 # diff_output is a list of files to send to clang-format.
3406 files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003407 if files:
3408 cmd = [clang_format_tool]
3409 if not opts.dry_run and not opts.diff:
3410 cmd.append('-i')
3411 stdout = RunCommand(cmd + files, cwd=top_dir)
3412 if opts.diff:
3413 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003414 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003415 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003416 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00003417 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003418 try:
3419 script = clang_format.FindClangFormatScriptInChromiumTree(
3420 'clang-format-diff.py')
3421 except clang_format.NotFoundError, e:
3422 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003423
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003424 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003425 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003426 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003427
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003428 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003429 if opts.diff:
3430 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003431 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003432 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003433
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003434 # Similar code to above, but using yapf on .py files rather than clang-format
3435 # on C/C++ files
3436 if opts.python:
3437 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, ['.py'])
3438 diff_output = RunGit(diff_cmd)
3439 yapf_tool = gclient_utils.FindExecutable('yapf')
3440 if yapf_tool is None:
3441 DieWithError('yapf not found in PATH')
3442
3443 if opts.full:
3444 files = diff_output.splitlines()
3445 if files:
3446 cmd = [yapf_tool]
3447 if not opts.dry_run and not opts.diff:
3448 cmd.append('-i')
3449 stdout = RunCommand(cmd + files, cwd=top_dir)
3450 if opts.diff:
3451 sys.stdout.write(stdout)
3452 else:
3453 # TODO(sbc): yapf --lines mode still has some issues.
3454 # https://github.com/google/yapf/issues/154
3455 DieWithError('--python currently only works with --full')
3456
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003457 # Build a diff command that only operates on dart files. dart's formatter
3458 # does not have the nice property of only operating on modified chunks, so
3459 # hard code full.
3460 dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3461 args, ['.dart'])
3462 dart_diff_output = RunGit(dart_diff_cmd)
3463 if dart_diff_output:
3464 try:
3465 command = [dart_format.FindDartFmtToolInChromiumTree()]
3466 if not opts.dry_run and not opts.diff:
3467 command.append('-w')
3468 command.extend(dart_diff_output.splitlines())
3469
3470 stdout = RunCommand(command, cwd=top_dir, env=env)
3471 if opts.dry_run and stdout:
3472 return_value = 2
3473 except dart_format.NotFoundError as e:
3474 print ('Unable to check dart code formatting. Dart SDK is not in ' +
3475 'this checkout.')
3476
3477 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003478
3479
maruel@chromium.org29404b52014-09-08 22:58:00 +00003480def CMDlol(parser, args):
3481 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003482 print zlib.decompress(base64.b64decode(
3483 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3484 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3485 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3486 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003487 return 0
3488
3489
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003490class OptionParser(optparse.OptionParser):
3491 """Creates the option parse and add --verbose support."""
3492 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003493 optparse.OptionParser.__init__(
3494 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003495 self.add_option(
3496 '-v', '--verbose', action='count', default=0,
3497 help='Use 2 times for more debugging info')
3498
3499 def parse_args(self, args=None, values=None):
3500 options, args = optparse.OptionParser.parse_args(self, args, values)
3501 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3502 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3503 return options, args
3504
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003505
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003506def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003507 if sys.hexversion < 0x02060000:
3508 print >> sys.stderr, (
3509 '\nYour python version %s is unsupported, please upgrade.\n' %
3510 sys.version.split(' ', 1)[0])
3511 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003512
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003513 # Reload settings.
3514 global settings
3515 settings = Settings()
3516
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003517 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003518 dispatcher = subcommand.CommandDispatcher(__name__)
3519 try:
3520 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003521 except auth.AuthenticationError as e:
3522 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003523 except urllib2.HTTPError, e:
3524 if e.code != 500:
3525 raise
3526 DieWithError(
3527 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3528 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003529 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003530
3531
3532if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003533 # These affect sys.stdout so do it outside of main() to simplify mocks in
3534 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003535 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003536 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003537 try:
3538 sys.exit(main(sys.argv[1:]))
3539 except KeyboardInterrupt:
3540 sys.stderr.write('interrupted\n')
3541 sys.exit(1)