blob: 288e281b1170aa16220eb5fe6527f638dc1d6b5d [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 '
2205 'an open CL.') % (cl.GetBranch(), local_branch)
2206 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
2548 # after it. Add a period on a new line to circumvent this.
2549 commit_desc.append_footer('Review URL: %s.' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002550 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002551 commit_desc.append_footer('Patch from %s.' % options.contributor)
2552
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002553 print('Description:')
2554 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002555
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002556 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002557 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002558 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002559
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002560 # We want to squash all this branch's commits into one commit with the proper
2561 # description. We do this by doing a "reset --soft" to the base branch (which
2562 # keeps the working copy the same), then dcommitting that. If origin/master
2563 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2564 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002565 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002566 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2567 # Delete the branches if they exist.
2568 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2569 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2570 result = RunGitWithCode(showref_cmd)
2571 if result[0] == 0:
2572 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002573
2574 # We might be in a directory that's present in this branch but not in the
2575 # trunk. Move up to the top of the tree so that git commands that expect a
2576 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002577 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002578 if rel_base_path:
2579 os.chdir(rel_base_path)
2580
2581 # Stuff our change into the merge branch.
2582 # We wrap in a try...finally block so if anything goes wrong,
2583 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002584 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002585 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002586 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002587 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002588 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002589 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002590 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002591 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002592 RunGit(
2593 [
2594 'commit', '--author', options.contributor,
2595 '-m', commit_desc.description,
2596 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002597 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002598 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002599 if base_has_submodules:
2600 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2601 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2602 RunGit(['checkout', CHERRY_PICK_BRANCH])
2603 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002604 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002605 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002606 pending_prefix = settings.GetPendingRefPrefix()
2607 if not pending_prefix or branch.startswith(pending_prefix):
2608 # If not using refs/pending/heads/* at all, or target ref is already set
2609 # to pending, then push to the target ref directly.
2610 retcode, output = RunGitWithCode(
2611 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002612 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002613 else:
2614 # Cherry-pick the change on top of pending ref and then push it.
2615 assert branch.startswith('refs/'), branch
2616 assert pending_prefix[-1] == '/', pending_prefix
2617 pending_ref = pending_prefix + branch[len('refs/'):]
2618 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002619 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002620 if retcode == 0:
2621 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002622 else:
2623 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002624 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002625 'svn', 'dcommit',
2626 '-C%s' % options.similarity,
2627 '--no-rebase', '--rmdir',
2628 ]
2629 if settings.GetForceHttpsCommitUrl():
2630 # Allow forcing https commit URLs for some projects that don't allow
2631 # committing to http URLs (like Google Code).
2632 remote_url = cl.GetGitSvnRemoteUrl()
2633 if urlparse.urlparse(remote_url).scheme == 'http':
2634 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002635 cmd_args.append('--commit-url=%s' % remote_url)
2636 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002637 if 'Committed r' in output:
2638 revision = re.match(
2639 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2640 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002641 finally:
2642 # And then swap back to the original branch and clean up.
2643 RunGit(['checkout', '-q', cl.GetBranch()])
2644 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002645 if base_has_submodules:
2646 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002647
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002648 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002649 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002650 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002651
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002652 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002653 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002654 try:
2655 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2656 # We set pushed_to_pending to False, since it made it all the way to the
2657 # real ref.
2658 pushed_to_pending = False
2659 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002660 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002661
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002662 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002663 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002664 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002665 if not to_pending:
2666 if viewvc_url and revision:
2667 change_desc.append_footer(
2668 'Committed: %s%s' % (viewvc_url, revision))
2669 elif revision:
2670 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002671 print ('Closing issue '
2672 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002673 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002674 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002675 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002676 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002677 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002678 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002679 if options.bypass_hooks:
2680 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2681 else:
2682 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002683 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002684 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002685
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002686 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002687 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2688 print 'The commit is in the pending queue (%s).' % pending_ref
2689 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002690 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002691 'footer.' % branch)
2692
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002693 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2694 if os.path.isfile(hook):
2695 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002696
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002697 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002698
2699
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002700def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2701 print
2702 print 'Waiting for commit to be landed on %s...' % real_ref
2703 print '(If you are impatient, you may Ctrl-C once without harm)'
2704 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2705 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2706
2707 loop = 0
2708 while True:
2709 sys.stdout.write('fetching (%d)... \r' % loop)
2710 sys.stdout.flush()
2711 loop += 1
2712
2713 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2714 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2715 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2716 for commit in commits.splitlines():
2717 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2718 print 'Found commit on %s' % real_ref
2719 return commit
2720
2721 current_rev = to_rev
2722
2723
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002724def PushToGitPending(remote, pending_ref, upstream_ref):
2725 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2726
2727 Returns:
2728 (retcode of last operation, output log of last operation).
2729 """
2730 assert pending_ref.startswith('refs/'), pending_ref
2731 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2732 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2733 code = 0
2734 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002735 max_attempts = 3
2736 attempts_left = max_attempts
2737 while attempts_left:
2738 if attempts_left != max_attempts:
2739 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2740 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002741
2742 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002743 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002744 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002745 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002746 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002747 print 'Fetch failed with exit code %d.' % code
2748 if out.strip():
2749 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002750 continue
2751
2752 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002753 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002754 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002755 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002756 if code:
2757 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002758 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2759 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002760 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2761 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002762 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002763 return code, out
2764
2765 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002766 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002767 code, out = RunGitWithCode(
2768 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2769 if code == 0:
2770 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002771 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002772 return code, out
2773
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002774 print 'Push failed with exit code %d.' % code
2775 if out.strip():
2776 print out.strip()
2777 if IsFatalPushFailure(out):
2778 print (
2779 'Fatal push error. Make sure your .netrc credentials and git '
2780 'user.email are correct and you have push access to the repo.')
2781 return code, out
2782
2783 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002784 return code, out
2785
2786
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002787def IsFatalPushFailure(push_stdout):
2788 """True if retrying push won't help."""
2789 return '(prohibited by Gerrit)' in push_stdout
2790
2791
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002792@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002793def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002794 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002795 if not settings.GetIsGitSvn():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002796 if get_footer_svn_id():
2797 # If it looks like previous commits were mirrored with git-svn.
2798 message = """This repository appears to be a git-svn mirror, but no
2799upstream SVN master is set. You probably need to run 'git auto-svn' once."""
2800 else:
2801 message = """This doesn't appear to be an SVN repository.
2802If your project has a true, writeable git repository, you probably want to run
2803'git cl land' instead.
2804If your project has a git mirror of an upstream SVN master, you probably need
2805to run 'git svn init'.
2806
2807Using the wrong command might cause your commit to appear to succeed, and the
2808review to be closed, without actually landing upstream. If you choose to
2809proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002810 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002811 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002812 return SendUpstream(parser, args, 'dcommit')
2813
2814
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002815@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002816def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002817 """Commits the current changelist via git."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002818 if settings.GetIsGitSvn() or get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002819 print('This appears to be an SVN repository.')
2820 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002821 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00002822 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002823 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002824
2825
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002826@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002827def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002828 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002829 parser.add_option('-b', dest='newbranch',
2830 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002831 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002832 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002833 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2834 help='Change to the directory DIR immediately, '
2835 'before doing anything else.')
2836 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002837 help='failed patches spew .rej files rather than '
2838 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002839 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2840 help="don't commit after patch applies")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002841 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002842 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002843 auth_config = auth.extract_auth_config_from_options(options)
2844
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002845 if len(args) != 1:
2846 parser.print_help()
2847 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002848 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002849
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002850 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002851 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002852 return 1
2853
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002854 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002855 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002856
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002857 if options.newbranch:
2858 if options.force:
2859 RunGit(['branch', '-D', options.newbranch],
2860 stderr=subprocess2.PIPE, error_ok=True)
2861 RunGit(['checkout', '-b', options.newbranch,
2862 Changelist().GetUpstreamBranch()])
2863
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002864 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002865 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002866
2867
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002868def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00002869 # PatchIssue should never be called with a dirty tree. It is up to the
2870 # caller to check this, but just in case we assert here since the
2871 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002872 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002873
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002874 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002875 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002876 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002877 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002878 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002879 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002880 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002881 # Assume it's a URL to the patch. Default to https.
2882 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00002883 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002884 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002885 DieWithError('Must pass an issue ID or full URL for '
2886 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00002887 issue = int(match.group(2))
2888 cl = Changelist(issue=issue, auth_config=auth_config)
2889 cl.rietveld_server = match.group(1)
2890 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002891 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002892
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002893 # Switch up to the top-level directory, if necessary, in preparation for
2894 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002895 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002896 if top:
2897 os.chdir(top)
2898
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002899 # Git patches have a/ at the beginning of source paths. We strip that out
2900 # with a sed script rather than the -p flag to patch so we can feed either
2901 # Git or svn-style patches into the same apply command.
2902 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002903 try:
2904 patch_data = subprocess2.check_output(
2905 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2906 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002907 DieWithError('Git patch mungling failed.')
2908 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002909
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002910 # We use "git apply" to apply the patch instead of "patch" so that we can
2911 # pick up file adds.
2912 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002913 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002914 if directory:
2915 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002916 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002917 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002918 elif IsGitVersionAtLeast('1.7.12'):
2919 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002920 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002921 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002922 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002923 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00002924 print 'Failed to apply the patch'
2925 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002926
2927 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002928 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00002929 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
2930 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00002931 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2932 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002933 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002934 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002935 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002936 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002937 else:
2938 print "Patch applied to index."
2939 return 0
2940
2941
2942def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002943 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002944 # Provide a wrapper for git svn rebase to help avoid accidental
2945 # git svn dcommit.
2946 # It's the only command that doesn't use parser at all since we just defer
2947 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002948
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002949 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002950
2951
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002952def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002953 """Fetches the tree status and returns either 'open', 'closed',
2954 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002955 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002956 if url:
2957 status = urllib2.urlopen(url).read().lower()
2958 if status.find('closed') != -1 or status == '0':
2959 return 'closed'
2960 elif status.find('open') != -1 or status == '1':
2961 return 'open'
2962 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002963 return 'unset'
2964
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002965
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002966def GetTreeStatusReason():
2967 """Fetches the tree status from a json url and returns the message
2968 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002969 url = settings.GetTreeStatusUrl()
2970 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002971 connection = urllib2.urlopen(json_url)
2972 status = json.loads(connection.read())
2973 connection.close()
2974 return status['message']
2975
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002976
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002977def GetBuilderMaster(bot_list):
2978 """For a given builder, fetch the master from AE if available."""
2979 map_url = 'https://builders-map.appspot.com/'
2980 try:
2981 master_map = json.load(urllib2.urlopen(map_url))
2982 except urllib2.URLError as e:
2983 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2984 (map_url, e))
2985 except ValueError as e:
2986 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2987 if not master_map:
2988 return None, 'Failed to build master map.'
2989
2990 result_master = ''
2991 for bot in bot_list:
2992 builder = bot.split(':', 1)[0]
2993 master_list = master_map.get(builder, [])
2994 if not master_list:
2995 return None, ('No matching master for builder %s.' % builder)
2996 elif len(master_list) > 1:
2997 return None, ('The builder name %s exists in multiple masters %s.' %
2998 (builder, master_list))
2999 else:
3000 cur_master = master_list[0]
3001 if not result_master:
3002 result_master = cur_master
3003 elif result_master != cur_master:
3004 return None, 'The builders do not belong to the same master.'
3005 return result_master, None
3006
3007
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003008def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003009 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003010 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003011 status = GetTreeStatus()
3012 if 'unset' == status:
3013 print 'You must configure your tree status URL by running "git cl config".'
3014 return 2
3015
3016 print "The tree is %s" % status
3017 print
3018 print GetTreeStatusReason()
3019 if status != 'open':
3020 return 1
3021 return 0
3022
3023
maruel@chromium.org15192402012-09-06 12:38:29 +00003024def CMDtry(parser, args):
3025 """Triggers a try job through Rietveld."""
3026 group = optparse.OptionGroup(parser, "Try job options")
3027 group.add_option(
3028 "-b", "--bot", action="append",
3029 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3030 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003031 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003032 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003033 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003034 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003035 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003036 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003037 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003038 "-r", "--revision",
3039 help="Revision to use for the try job; default: the "
3040 "revision will be determined by the try server; see "
3041 "its waterfall for more info")
3042 group.add_option(
3043 "-c", "--clobber", action="store_true", default=False,
3044 help="Force a clobber before building; e.g. don't do an "
3045 "incremental build")
3046 group.add_option(
3047 "--project",
3048 help="Override which project to use. Projects are defined "
3049 "server-side to define what default bot set to use")
3050 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003051 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003052 group.add_option(
3053 "--use-buildbucket", action="store_true", default=False,
3054 help="Use buildbucket to trigger try jobs.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003055 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003056 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003057 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003058 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003059
3060 if args:
3061 parser.error('Unknown arguments: %s' % args)
3062
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003063 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003064 if not cl.GetIssue():
3065 parser.error('Need to upload first')
3066
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003067 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003068 if props.get('closed'):
3069 parser.error('Cannot send tryjobs for a closed CL')
3070
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003071 if props.get('private'):
3072 parser.error('Cannot use trybots with private issue')
3073
maruel@chromium.org15192402012-09-06 12:38:29 +00003074 if not options.name:
3075 options.name = cl.GetBranch()
3076
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003077 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003078 options.master, err_msg = GetBuilderMaster(options.bot)
3079 if err_msg:
3080 parser.error('Tryserver master cannot be found because: %s\n'
3081 'Please manually specify the tryserver master'
3082 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003083
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003084 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003085 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003086 if not options.bot:
3087 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003088
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003089 # Get try masters from PRESUBMIT.py files.
3090 masters = presubmit_support.DoGetTryMasters(
3091 change,
3092 change.LocalPaths(),
3093 settings.GetRoot(),
3094 None,
3095 None,
3096 options.verbose,
3097 sys.stdout)
3098 if masters:
3099 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003100
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003101 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3102 options.bot = presubmit_support.DoGetTrySlaves(
3103 change,
3104 change.LocalPaths(),
3105 settings.GetRoot(),
3106 None,
3107 None,
3108 options.verbose,
3109 sys.stdout)
3110 if not options.bot:
3111 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003112
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003113 builders_and_tests = {}
3114 # TODO(machenbach): The old style command-line options don't support
3115 # multiple try masters yet.
3116 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3117 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3118
3119 for bot in old_style:
3120 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003121 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003122 elif ',' in bot:
3123 parser.error('Specify one bot per --bot flag')
3124 else:
3125 builders_and_tests.setdefault(bot, []).append('defaulttests')
3126
3127 for bot, tests in new_style:
3128 builders_and_tests.setdefault(bot, []).extend(tests)
3129
3130 # Return a master map with one master to be backwards compatible. The
3131 # master name defaults to an empty string, which will cause the master
3132 # not to be set on rietveld (deprecated).
3133 return {options.master: builders_and_tests}
3134
3135 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003136
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003137 for builders in masters.itervalues():
3138 if any('triggered' in b for b in builders):
3139 print >> sys.stderr, (
3140 'ERROR You are trying to send a job to a triggered bot. This type of'
3141 ' bot requires an\ninitial job from a parent (usually a builder). '
3142 'Instead send your job to the parent.\n'
3143 'Bot list: %s' % builders)
3144 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003145
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003146 patchset = cl.GetMostRecentPatchset()
3147 if patchset and patchset != cl.GetPatchset():
3148 print(
3149 '\nWARNING Mismatch between local config and server. Did a previous '
3150 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3151 'Continuing using\npatchset %s.\n' % patchset)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003152 if options.use_buildbucket:
3153 try:
3154 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3155 except BuildbucketResponseException as ex:
3156 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003157 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003158 except Exception as e:
3159 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3160 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3161 e, stacktrace)
3162 return 1
3163 else:
3164 try:
3165 cl.RpcServer().trigger_distributed_try_jobs(
3166 cl.GetIssue(), patchset, options.name, options.clobber,
3167 options.revision, masters)
3168 except urllib2.HTTPError as e:
3169 if e.code == 404:
3170 print('404 from rietveld; '
3171 'did you mean to use "git try" instead of "git cl try"?')
3172 return 1
3173 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003174
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003175 for (master, builders) in sorted(masters.iteritems()):
3176 if master:
3177 print 'Master: %s' % master
3178 length = max(len(builder) for builder in builders)
3179 for builder in sorted(builders):
3180 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003181 return 0
3182
3183
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003184@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003185def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003186 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003187 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003188 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003189 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003190
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003191 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003192 if args:
3193 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003194 branch = cl.GetBranch()
3195 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003196 cl = Changelist()
3197 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003198
3199 # Clear configured merge-base, if there is one.
3200 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003201 else:
3202 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003203 return 0
3204
3205
thestig@chromium.org00858c82013-12-02 23:08:03 +00003206def CMDweb(parser, args):
3207 """Opens the current CL in the web browser."""
3208 _, args = parser.parse_args(args)
3209 if args:
3210 parser.error('Unrecognized args: %s' % ' '.join(args))
3211
3212 issue_url = Changelist().GetIssueURL()
3213 if not issue_url:
3214 print >> sys.stderr, 'ERROR No issue to open'
3215 return 1
3216
3217 webbrowser.open(issue_url)
3218 return 0
3219
3220
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003221def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003222 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003223 auth.add_auth_options(parser)
3224 options, args = parser.parse_args(args)
3225 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003226 if args:
3227 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003228 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003229 props = cl.GetIssueProperties()
3230 if props.get('private'):
3231 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003232 cl.SetFlag('commit', '1')
3233 return 0
3234
3235
groby@chromium.org411034a2013-02-26 15:12:01 +00003236def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003237 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003238 auth.add_auth_options(parser)
3239 options, args = parser.parse_args(args)
3240 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003241 if args:
3242 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003243 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003244 # Ensure there actually is an issue to close.
3245 cl.GetDescription()
3246 cl.CloseIssue()
3247 return 0
3248
3249
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003250def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003251 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003252 auth.add_auth_options(parser)
3253 options, args = parser.parse_args(args)
3254 auth_config = auth.extract_auth_config_from_options(options)
3255 if args:
3256 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003257
3258 # Uncommitted (staged and unstaged) changes will be destroyed by
3259 # "git reset --hard" if there are merging conflicts in PatchIssue().
3260 # Staged changes would be committed along with the patch from last
3261 # upload, hence counted toward the "last upload" side in the final
3262 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003263 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003264 return 1
3265
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003266 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003267 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003268 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003269 if not issue:
3270 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003271 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003272 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003273
3274 # Create a new branch based on the merge-base
3275 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3276 try:
3277 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003278 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003279 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003280 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003281 return rtn
3282
wychen@chromium.org06928532015-02-03 02:11:29 +00003283 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003284 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003285 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003286 finally:
3287 RunGit(['checkout', '-q', branch])
3288 RunGit(['branch', '-D', TMP_BRANCH])
3289
3290 return 0
3291
3292
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003293def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003294 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003295 parser.add_option(
3296 '--no-color',
3297 action='store_true',
3298 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003299 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003300 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003301 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003302
3303 author = RunGit(['config', 'user.email']).strip() or None
3304
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003305 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003306
3307 if args:
3308 if len(args) > 1:
3309 parser.error('Unknown args')
3310 base_branch = args[0]
3311 else:
3312 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003313 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003314
3315 change = cl.GetChange(base_branch, None)
3316 return owners_finder.OwnersFinder(
3317 [f.LocalPath() for f in
3318 cl.GetChange(base_branch, None).AffectedFiles()],
3319 change.RepositoryRoot(), author,
3320 fopen=file, os_path=os.path, glob=glob.glob,
3321 disable_color=options.no_color).run()
3322
3323
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003324def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
3325 """Generates a diff command."""
3326 # Generate diff for the current branch's changes.
3327 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3328 upstream_commit, '--' ]
3329
3330 if args:
3331 for arg in args:
3332 if os.path.isdir(arg):
3333 diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
3334 elif os.path.isfile(arg):
3335 diff_cmd.append(arg)
3336 else:
3337 DieWithError('Argument "%s" is not a file or a directory' % arg)
3338 else:
3339 diff_cmd.extend('*' + ext for ext in extensions)
3340
3341 return diff_cmd
3342
3343
enne@chromium.org555cfe42014-01-29 18:21:39 +00003344@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003345def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003346 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003347 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003348 parser.add_option('--full', action='store_true',
3349 help='Reformat the full content of all touched files')
3350 parser.add_option('--dry-run', action='store_true',
3351 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003352 parser.add_option('--python', action='store_true',
3353 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003354 parser.add_option('--diff', action='store_true',
3355 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003356 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003357
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003358 # git diff generates paths against the root of the repository. Change
3359 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003360 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003361 if rel_base_path:
3362 os.chdir(rel_base_path)
3363
digit@chromium.org29e47272013-05-17 17:01:46 +00003364 # Grab the merge-base commit, i.e. the upstream commit of the current
3365 # branch when it was created or the last time it was rebased. This is
3366 # to cover the case where the user may have called "git fetch origin",
3367 # moving the origin branch to a newer commit, but hasn't rebased yet.
3368 upstream_commit = None
3369 cl = Changelist()
3370 upstream_branch = cl.GetUpstreamBranch()
3371 if upstream_branch:
3372 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3373 upstream_commit = upstream_commit.strip()
3374
3375 if not upstream_commit:
3376 DieWithError('Could not find base commit for this branch. '
3377 'Are you in detached state?')
3378
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003379 if opts.full:
3380 # Only list the names of modified files.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003381 diff_type = '--name-only'
enne@chromium.org555cfe42014-01-29 18:21:39 +00003382 else:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003383 # Only generate context-less patches.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003384 diff_type = '-U0'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003385
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003386 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, CLANG_EXTS)
digit@chromium.org29e47272013-05-17 17:01:46 +00003387 diff_output = RunGit(diff_cmd)
3388
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003389 top_dir = os.path.normpath(
3390 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3391
3392 # Locate the clang-format binary in the checkout
3393 try:
3394 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3395 except clang_format.NotFoundError, e:
3396 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003397
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003398 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3399 # formatted. This is used to block during the presubmit.
3400 return_value = 0
3401
digit@chromium.org29e47272013-05-17 17:01:46 +00003402 if opts.full:
3403 # diff_output is a list of files to send to clang-format.
3404 files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003405 if files:
3406 cmd = [clang_format_tool]
3407 if not opts.dry_run and not opts.diff:
3408 cmd.append('-i')
3409 stdout = RunCommand(cmd + files, cwd=top_dir)
3410 if opts.diff:
3411 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003412 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003413 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003414 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00003415 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003416 try:
3417 script = clang_format.FindClangFormatScriptInChromiumTree(
3418 'clang-format-diff.py')
3419 except clang_format.NotFoundError, e:
3420 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003421
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003422 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003423 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003424 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003425
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003426 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003427 if opts.diff:
3428 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003429 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003430 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003431
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003432 # Similar code to above, but using yapf on .py files rather than clang-format
3433 # on C/C++ files
3434 if opts.python:
3435 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, ['.py'])
3436 diff_output = RunGit(diff_cmd)
3437 yapf_tool = gclient_utils.FindExecutable('yapf')
3438 if yapf_tool is None:
3439 DieWithError('yapf not found in PATH')
3440
3441 if opts.full:
3442 files = diff_output.splitlines()
3443 if files:
3444 cmd = [yapf_tool]
3445 if not opts.dry_run and not opts.diff:
3446 cmd.append('-i')
3447 stdout = RunCommand(cmd + files, cwd=top_dir)
3448 if opts.diff:
3449 sys.stdout.write(stdout)
3450 else:
3451 # TODO(sbc): yapf --lines mode still has some issues.
3452 # https://github.com/google/yapf/issues/154
3453 DieWithError('--python currently only works with --full')
3454
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003455 # Build a diff command that only operates on dart files. dart's formatter
3456 # does not have the nice property of only operating on modified chunks, so
3457 # hard code full.
3458 dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3459 args, ['.dart'])
3460 dart_diff_output = RunGit(dart_diff_cmd)
3461 if dart_diff_output:
3462 try:
3463 command = [dart_format.FindDartFmtToolInChromiumTree()]
3464 if not opts.dry_run and not opts.diff:
3465 command.append('-w')
3466 command.extend(dart_diff_output.splitlines())
3467
3468 stdout = RunCommand(command, cwd=top_dir, env=env)
3469 if opts.dry_run and stdout:
3470 return_value = 2
3471 except dart_format.NotFoundError as e:
3472 print ('Unable to check dart code formatting. Dart SDK is not in ' +
3473 'this checkout.')
3474
3475 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003476
3477
maruel@chromium.org29404b52014-09-08 22:58:00 +00003478def CMDlol(parser, args):
3479 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003480 print zlib.decompress(base64.b64decode(
3481 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3482 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3483 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3484 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003485 return 0
3486
3487
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003488class OptionParser(optparse.OptionParser):
3489 """Creates the option parse and add --verbose support."""
3490 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003491 optparse.OptionParser.__init__(
3492 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003493 self.add_option(
3494 '-v', '--verbose', action='count', default=0,
3495 help='Use 2 times for more debugging info')
3496
3497 def parse_args(self, args=None, values=None):
3498 options, args = optparse.OptionParser.parse_args(self, args, values)
3499 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3500 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3501 return options, args
3502
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003503
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003504def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003505 if sys.hexversion < 0x02060000:
3506 print >> sys.stderr, (
3507 '\nYour python version %s is unsupported, please upgrade.\n' %
3508 sys.version.split(' ', 1)[0])
3509 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003510
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003511 # Reload settings.
3512 global settings
3513 settings = Settings()
3514
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003515 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003516 dispatcher = subcommand.CommandDispatcher(__name__)
3517 try:
3518 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003519 except auth.AuthenticationError as e:
3520 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003521 except urllib2.HTTPError, e:
3522 if e.code != 500:
3523 raise
3524 DieWithError(
3525 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3526 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003527 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003528
3529
3530if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003531 # These affect sys.stdout so do it outside of main() to simplify mocks in
3532 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003533 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003534 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003535 try:
3536 sys.exit(main(sys.argv[1:]))
3537 except KeyboardInterrupt:
3538 sys.stderr.write('interrupted\n')
3539 sys.exit(1)