blob: 3023213ddd903a9d696f48eea7c79f04964f079e [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +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
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000042from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000043import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000044import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000045import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000046import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000047import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000048import git_common
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +000049from git_footers import get_footer_svn_id
piman@chromium.org336f9122014-09-04 02:16:55 +000050import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000051import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000052import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000053import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000054import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000055import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000056import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import watchlists
58
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000061DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000062POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000063DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000064GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000065CHANGE_ID = 'Change-Id:'
rmistry@google.comc68112d2015-03-03 12:48:06 +000066REFS_THAT_ALIAS_TO_OTHER_REFS = {
67 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
68 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
69}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000070
thestig@chromium.org44202a22014-03-11 19:22:18 +000071# Valid extensions for files we want to lint.
72DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
73DEFAULT_LINT_IGNORE_REGEX = r"$^"
74
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000075# Shortcut since it quickly becomes redundant.
76Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000077
maruel@chromium.orgddd59412011-11-30 14:20:38 +000078# Initialized in main()
79settings = None
80
81
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000082def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000083 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000084 sys.exit(1)
85
86
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000087def GetNoGitPagerEnv():
88 env = os.environ.copy()
89 # 'cat' is a magical git string that disables pagers on all platforms.
90 env['GIT_PAGER'] = 'cat'
91 return env
92
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000093
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000094def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000095 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000096 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000097 except subprocess2.CalledProcessError as e:
98 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000099 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000100 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000101 'Command "%s" failed.\n%s' % (
102 ' '.join(args), error_message or e.stdout or ''))
103 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104
105
106def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000107 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000108 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000109
110
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000111def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000112 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000113 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000114 if suppress_stderr:
115 stderr = subprocess2.VOID
116 else:
117 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000118 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000119 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000120 stdout=subprocess2.PIPE,
121 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000122 return code, out[0]
123 except ValueError:
124 # When the subprocess fails, it returns None. That triggers a ValueError
125 # when trying to unpack the return value into (out, code).
126 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000127
128
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000129def RunGitSilent(args):
130 """Returns stdout, suppresses stderr and ingores the return code."""
131 return RunGitWithCode(args, suppress_stderr=True)[1]
132
133
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000134def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000135 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000136 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000137 return (version.startswith(prefix) and
138 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139
140
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000141def BranchExists(branch):
142 """Return True if specified branch exists."""
143 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
144 suppress_stderr=True)
145 return not code
146
147
maruel@chromium.org90541732011-04-01 17:54:18 +0000148def ask_for_data(prompt):
149 try:
150 return raw_input(prompt)
151 except KeyboardInterrupt:
152 # Hide the exception.
153 sys.exit(1)
154
155
iannucci@chromium.org79540052012-10-19 23:15:26 +0000156def git_set_branch_value(key, value):
157 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000158 if not branch:
159 return
160
161 cmd = ['config']
162 if isinstance(value, int):
163 cmd.append('--int')
164 git_key = 'branch.%s.%s' % (branch, key)
165 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000166
167
168def git_get_branch_default(key, default):
169 branch = Changelist().GetBranch()
170 if branch:
171 git_key = 'branch.%s.%s' % (branch, key)
172 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
173 try:
174 return int(stdout.strip())
175 except ValueError:
176 pass
177 return default
178
179
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000180def add_git_similarity(parser):
181 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000182 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000183 help='Sets the percentage that a pair of files need to match in order to'
184 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000185 parser.add_option(
186 '--find-copies', action='store_true',
187 help='Allows git to look for copies.')
188 parser.add_option(
189 '--no-find-copies', action='store_false', dest='find_copies',
190 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000191
192 old_parser_args = parser.parse_args
193 def Parse(args):
194 options, args = old_parser_args(args)
195
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000196 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000197 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000198 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000199 print('Note: Saving similarity of %d%% in git config.'
200 % options.similarity)
201 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000202
iannucci@chromium.org79540052012-10-19 23:15:26 +0000203 options.similarity = max(0, min(options.similarity, 100))
204
205 if options.find_copies is None:
206 options.find_copies = bool(
207 git_get_branch_default('git-find-copies', True))
208 else:
209 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000210
211 print('Using %d%% similarity for rename/copy detection. '
212 'Override with --similarity.' % options.similarity)
213
214 return options, args
215 parser.parse_args = Parse
216
217
machenbach@chromium.org45453142015-09-15 08:45:22 +0000218def _get_properties_from_options(options):
219 properties = dict(x.split('=', 1) for x in options.properties)
220 for key, val in properties.iteritems():
221 try:
222 properties[key] = json.loads(val)
223 except ValueError:
224 pass # If a value couldn't be evaluated, treat it as a string.
225 return properties
226
227
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000228def _prefix_master(master):
229 """Convert user-specified master name to full master name.
230
231 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
232 name, while the developers always use shortened master name
233 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
234 function does the conversion for buildbucket migration.
235 """
236 prefix = 'master.'
237 if master.startswith(prefix):
238 return master
239 return '%s%s' % (prefix, master)
240
241
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000242def trigger_luci_job(changelist, masters, options):
243 """Send a job to run on LUCI."""
244 issue_props = changelist.GetIssueProperties()
245 issue = changelist.GetIssue()
246 patchset = changelist.GetMostRecentPatchset()
247 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000248 # TODO(hinoka et al): add support for other properties.
249 # Currently, this completely ignores testfilter and other properties.
250 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000251 luci_trigger.trigger(
252 builder, 'HEAD', issue, patchset, issue_props['project'])
253
254
machenbach@chromium.org45453142015-09-15 08:45:22 +0000255def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000256 rietveld_url = settings.GetDefaultServerUrl()
257 rietveld_host = urlparse.urlparse(rietveld_url).hostname
258 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
259 http = authenticator.authorize(httplib2.Http())
260 http.force_exception_to_status_code = True
261 issue_props = changelist.GetIssueProperties()
262 issue = changelist.GetIssue()
263 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000264 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000265
266 buildbucket_put_url = (
267 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000268 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000269 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
270 hostname=rietveld_host,
271 issue=issue,
272 patch=patchset)
273
274 batch_req_body = {'builds': []}
275 print_text = []
276 print_text.append('Tried jobs on:')
277 for master, builders_and_tests in sorted(masters.iteritems()):
278 print_text.append('Master: %s' % master)
279 bucket = _prefix_master(master)
280 for builder, tests in sorted(builders_and_tests.iteritems()):
281 print_text.append(' %s: %s' % (builder, tests))
282 parameters = {
283 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000284 'changes': [{
285 'author': {'email': issue_props['owner_email']},
286 'revision': options.revision,
287 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000288 'properties': {
289 'category': category,
290 'issue': issue,
291 'master': master,
292 'patch_project': issue_props['project'],
293 'patch_storage': 'rietveld',
294 'patchset': patchset,
295 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000296 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000297 },
298 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000299 if tests:
300 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000301 if properties:
302 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000303 if options.clobber:
304 parameters['properties']['clobber'] = True
305 batch_req_body['builds'].append(
306 {
307 'bucket': bucket,
308 'parameters_json': json.dumps(parameters),
309 'tags': ['builder:%s' % builder,
310 'buildset:%s' % buildset,
311 'master:%s' % master,
312 'user_agent:git_cl_try']
313 }
314 )
315
316 for try_count in xrange(3):
317 response, content = http.request(
318 buildbucket_put_url,
319 'PUT',
320 body=json.dumps(batch_req_body),
321 headers={'Content-Type': 'application/json'},
322 )
323 content_json = None
324 try:
325 content_json = json.loads(content)
326 except ValueError:
327 pass
328
329 # Buildbucket could return an error even if status==200.
330 if content_json and content_json.get('error'):
331 msg = 'Error in response. Code: %d. Reason: %s. Message: %s.' % (
332 content_json['error'].get('code', ''),
333 content_json['error'].get('reason', ''),
334 content_json['error'].get('message', ''))
335 raise BuildbucketResponseException(msg)
336
337 if response.status == 200:
338 if not content_json:
339 raise BuildbucketResponseException(
340 'Buildbucket returns invalid json content: %s.\n'
341 'Please file bugs at crbug.com, label "Infra-BuildBucket".' %
342 content)
343 break
344 if response.status < 500 or try_count >= 2:
345 raise httplib2.HttpLib2Error(content)
346
347 # status >= 500 means transient failures.
348 logging.debug('Transient errors when triggering tryjobs. Will retry.')
349 time.sleep(0.5 + 1.5*try_count)
350
351 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000352
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000353
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000354def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
355 """Return the corresponding git ref if |base_url| together with |glob_spec|
356 matches the full |url|.
357
358 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
359 """
360 fetch_suburl, as_ref = glob_spec.split(':')
361 if allow_wildcards:
362 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
363 if glob_match:
364 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
365 # "branches/{472,597,648}/src:refs/remotes/svn/*".
366 branch_re = re.escape(base_url)
367 if glob_match.group(1):
368 branch_re += '/' + re.escape(glob_match.group(1))
369 wildcard = glob_match.group(2)
370 if wildcard == '*':
371 branch_re += '([^/]*)'
372 else:
373 # Escape and replace surrounding braces with parentheses and commas
374 # with pipe symbols.
375 wildcard = re.escape(wildcard)
376 wildcard = re.sub('^\\\\{', '(', wildcard)
377 wildcard = re.sub('\\\\,', '|', wildcard)
378 wildcard = re.sub('\\\\}$', ')', wildcard)
379 branch_re += wildcard
380 if glob_match.group(3):
381 branch_re += re.escape(glob_match.group(3))
382 match = re.match(branch_re, url)
383 if match:
384 return re.sub('\*$', match.group(1), as_ref)
385
386 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
387 if fetch_suburl:
388 full_url = base_url + '/' + fetch_suburl
389 else:
390 full_url = base_url
391 if full_url == url:
392 return as_ref
393 return None
394
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000395
iannucci@chromium.org79540052012-10-19 23:15:26 +0000396def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000397 """Prints statistics about the change to the user."""
398 # --no-ext-diff is broken in some versions of Git, so try to work around
399 # this by overriding the environment (but there is still a problem if the
400 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000401 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000402 if 'GIT_EXTERNAL_DIFF' in env:
403 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000404
405 if find_copies:
406 similarity_options = ['--find-copies-harder', '-l100000',
407 '-C%s' % similarity]
408 else:
409 similarity_options = ['-M%s' % similarity]
410
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000411 try:
412 stdout = sys.stdout.fileno()
413 except AttributeError:
414 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000415 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000416 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000417 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000418 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000419
420
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000421class BuildbucketResponseException(Exception):
422 pass
423
424
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000425class Settings(object):
426 def __init__(self):
427 self.default_server = None
428 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000429 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000430 self.is_git_svn = None
431 self.svn_branch = None
432 self.tree_status_url = None
433 self.viewvc_url = None
434 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000435 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000436 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000437 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000438 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000439 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000440 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000441
442 def LazyUpdateIfNeeded(self):
443 """Updates the settings from a codereview.settings file, if available."""
444 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000445 # The only value that actually changes the behavior is
446 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000447 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000448 error_ok=True
449 ).strip().lower()
450
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000451 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000452 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000453 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000454 # set updated to True to avoid infinite calling loop
455 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000456 self.updated = True
457 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000458 self.updated = True
459
460 def GetDefaultServerUrl(self, error_ok=False):
461 if not self.default_server:
462 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000463 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000464 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000465 if error_ok:
466 return self.default_server
467 if not self.default_server:
468 error_message = ('Could not find settings file. You must configure '
469 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000470 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000471 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000472 return self.default_server
473
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000474 @staticmethod
475 def GetRelativeRoot():
476 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000477
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000478 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000479 if self.root is None:
480 self.root = os.path.abspath(self.GetRelativeRoot())
481 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000482
483 def GetIsGitSvn(self):
484 """Return true if this repo looks like it's using git-svn."""
485 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000486 if self.GetPendingRefPrefix():
487 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
488 self.is_git_svn = False
489 else:
490 # If you have any "svn-remote.*" config keys, we think you're using svn.
491 self.is_git_svn = RunGitWithCode(
492 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000493 return self.is_git_svn
494
495 def GetSVNBranch(self):
496 if self.svn_branch is None:
497 if not self.GetIsGitSvn():
498 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
499
500 # Try to figure out which remote branch we're based on.
501 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000502 # 1) iterate through our branch history and find the svn URL.
503 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000504
505 # regexp matching the git-svn line that contains the URL.
506 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
507
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000508 # We don't want to go through all of history, so read a line from the
509 # pipe at a time.
510 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000511 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000512 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
513 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000514 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000515 for line in proc.stdout:
516 match = git_svn_re.match(line)
517 if match:
518 url = match.group(1)
519 proc.stdout.close() # Cut pipe.
520 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000521
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000522 if url:
523 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
524 remotes = RunGit(['config', '--get-regexp',
525 r'^svn-remote\..*\.url']).splitlines()
526 for remote in remotes:
527 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000528 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000529 remote = match.group(1)
530 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000531 rewrite_root = RunGit(
532 ['config', 'svn-remote.%s.rewriteRoot' % remote],
533 error_ok=True).strip()
534 if rewrite_root:
535 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000536 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000537 ['config', 'svn-remote.%s.fetch' % remote],
538 error_ok=True).strip()
539 if fetch_spec:
540 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
541 if self.svn_branch:
542 break
543 branch_spec = RunGit(
544 ['config', 'svn-remote.%s.branches' % remote],
545 error_ok=True).strip()
546 if branch_spec:
547 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
548 if self.svn_branch:
549 break
550 tag_spec = RunGit(
551 ['config', 'svn-remote.%s.tags' % remote],
552 error_ok=True).strip()
553 if tag_spec:
554 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
555 if self.svn_branch:
556 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000557
558 if not self.svn_branch:
559 DieWithError('Can\'t guess svn branch -- try specifying it on the '
560 'command line')
561
562 return self.svn_branch
563
564 def GetTreeStatusUrl(self, error_ok=False):
565 if not self.tree_status_url:
566 error_message = ('You must configure your tree status URL by running '
567 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000568 self.tree_status_url = self._GetRietveldConfig(
569 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000570 return self.tree_status_url
571
572 def GetViewVCUrl(self):
573 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000574 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000575 return self.viewvc_url
576
rmistry@google.com90752582014-01-14 21:04:50 +0000577 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000578 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000579
rmistry@google.com78948ed2015-07-08 23:09:57 +0000580 def GetIsSkipDependencyUpload(self, branch_name):
581 """Returns true if specified branch should skip dep uploads."""
582 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
583 error_ok=True)
584
rmistry@google.com5626a922015-02-26 14:03:30 +0000585 def GetRunPostUploadHook(self):
586 run_post_upload_hook = self._GetRietveldConfig(
587 'run-post-upload-hook', error_ok=True)
588 return run_post_upload_hook == "True"
589
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000590 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000591 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000592
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000593 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000594 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000595
ukai@chromium.orge8077812012-02-03 03:41:46 +0000596 def GetIsGerrit(self):
597 """Return true if this repo is assosiated with gerrit code review system."""
598 if self.is_gerrit is None:
599 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
600 return self.is_gerrit
601
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000602 def GetSquashGerritUploads(self):
603 """Return true if uploads to Gerrit should be squashed by default."""
604 if self.squash_gerrit_uploads is None:
605 self.squash_gerrit_uploads = (
606 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
607 error_ok=True).strip() == 'true')
608 return self.squash_gerrit_uploads
609
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000610 def GetGitEditor(self):
611 """Return the editor specified in the git config, or None if none is."""
612 if self.git_editor is None:
613 self.git_editor = self._GetConfig('core.editor', error_ok=True)
614 return self.git_editor or None
615
thestig@chromium.org44202a22014-03-11 19:22:18 +0000616 def GetLintRegex(self):
617 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
618 DEFAULT_LINT_REGEX)
619
620 def GetLintIgnoreRegex(self):
621 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
622 DEFAULT_LINT_IGNORE_REGEX)
623
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000624 def GetProject(self):
625 if not self.project:
626 self.project = self._GetRietveldConfig('project', error_ok=True)
627 return self.project
628
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000629 def GetForceHttpsCommitUrl(self):
630 if not self.force_https_commit_url:
631 self.force_https_commit_url = self._GetRietveldConfig(
632 'force-https-commit-url', error_ok=True)
633 return self.force_https_commit_url
634
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000635 def GetPendingRefPrefix(self):
636 if not self.pending_ref_prefix:
637 self.pending_ref_prefix = self._GetRietveldConfig(
638 'pending-ref-prefix', error_ok=True)
639 return self.pending_ref_prefix
640
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000641 def _GetRietveldConfig(self, param, **kwargs):
642 return self._GetConfig('rietveld.' + param, **kwargs)
643
rmistry@google.com78948ed2015-07-08 23:09:57 +0000644 def _GetBranchConfig(self, branch_name, param, **kwargs):
645 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
646
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000647 def _GetConfig(self, param, **kwargs):
648 self.LazyUpdateIfNeeded()
649 return RunGit(['config', param], **kwargs).strip()
650
651
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000652def ShortBranchName(branch):
653 """Convert a name like 'refs/heads/foo' to just 'foo'."""
654 return branch.replace('refs/heads/', '')
655
656
657class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000658 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000659 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000660 global settings
661 if not settings:
662 # Happens when git_cl.py is used as a utility library.
663 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000664 settings.GetDefaultServerUrl()
665 self.branchref = branchref
666 if self.branchref:
667 self.branch = ShortBranchName(self.branchref)
668 else:
669 self.branch = None
670 self.rietveld_server = None
671 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000672 self.lookedup_issue = False
673 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000674 self.has_description = False
675 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000676 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000677 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000678 self.cc = None
679 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000680 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000681 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000682 self._remote = None
683 self._rpc_server = None
684
685 @property
686 def auth_config(self):
687 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000688
689 def GetCCList(self):
690 """Return the users cc'd on this CL.
691
692 Return is a string suitable for passing to gcl with the --cc flag.
693 """
694 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000695 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000696 more_cc = ','.join(self.watchers)
697 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
698 return self.cc
699
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000700 def GetCCListWithoutDefault(self):
701 """Return the users cc'd on this CL excluding default ones."""
702 if self.cc is None:
703 self.cc = ','.join(self.watchers)
704 return self.cc
705
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000706 def SetWatchers(self, watchers):
707 """Set the list of email addresses that should be cc'd based on the changed
708 files in this CL.
709 """
710 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000711
712 def GetBranch(self):
713 """Returns the short branch name, e.g. 'master'."""
714 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000715 branchref = RunGit(['symbolic-ref', 'HEAD'],
716 stderr=subprocess2.VOID, error_ok=True).strip()
717 if not branchref:
718 return None
719 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000720 self.branch = ShortBranchName(self.branchref)
721 return self.branch
722
723 def GetBranchRef(self):
724 """Returns the full branch name, e.g. 'refs/heads/master'."""
725 self.GetBranch() # Poke the lazy loader.
726 return self.branchref
727
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000728 @staticmethod
729 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000730 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000731 e.g. 'origin', 'refs/heads/master'
732 """
733 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
735 error_ok=True).strip()
736 if upstream_branch:
737 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
738 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000739 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
740 error_ok=True).strip()
741 if upstream_branch:
742 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000744 # Fall back on trying a git-svn upstream branch.
745 if settings.GetIsGitSvn():
746 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000747 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000748 # Else, try to guess the origin remote.
749 remote_branches = RunGit(['branch', '-r']).split()
750 if 'origin/master' in remote_branches:
751 # Fall back on origin/master if it exits.
752 remote = 'origin'
753 upstream_branch = 'refs/heads/master'
754 elif 'origin/trunk' in remote_branches:
755 # Fall back on origin/trunk if it exists. Generally a shared
756 # git-svn clone
757 remote = 'origin'
758 upstream_branch = 'refs/heads/trunk'
759 else:
760 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000761Either pass complete "git diff"-style arguments, like
762 git cl upload origin/master
763or verify this branch is set up to track another (via the --track argument to
764"git checkout -b ...").""")
765
766 return remote, upstream_branch
767
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000768 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000769 upstream_branch = self.GetUpstreamBranch()
770 if not BranchExists(upstream_branch):
771 DieWithError('The upstream for the current branch (%s) does not exist '
772 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000773 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000774 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000775
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000776 def GetUpstreamBranch(self):
777 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000778 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000779 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000780 upstream_branch = upstream_branch.replace('refs/heads/',
781 'refs/remotes/%s/' % remote)
782 upstream_branch = upstream_branch.replace('refs/branch-heads/',
783 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000784 self.upstream_branch = upstream_branch
785 return self.upstream_branch
786
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000787 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000788 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000789 remote, branch = None, self.GetBranch()
790 seen_branches = set()
791 while branch not in seen_branches:
792 seen_branches.add(branch)
793 remote, branch = self.FetchUpstreamTuple(branch)
794 branch = ShortBranchName(branch)
795 if remote != '.' or branch.startswith('refs/remotes'):
796 break
797 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000798 remotes = RunGit(['remote'], error_ok=True).split()
799 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000800 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000801 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000802 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000803 logging.warning('Could not determine which remote this change is '
804 'associated with, so defaulting to "%s". This may '
805 'not be what you want. You may prevent this message '
806 'by running "git svn info" as documented here: %s',
807 self._remote,
808 GIT_INSTRUCTIONS_URL)
809 else:
810 logging.warn('Could not determine which remote this change is '
811 'associated with. You may prevent this message by '
812 'running "git svn info" as documented here: %s',
813 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000814 branch = 'HEAD'
815 if branch.startswith('refs/remotes'):
816 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000817 elif branch.startswith('refs/branch-heads/'):
818 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000819 else:
820 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000821 return self._remote
822
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000823 def GitSanityChecks(self, upstream_git_obj):
824 """Checks git repo status and ensures diff is from local commits."""
825
sbc@chromium.org79706062015-01-14 21:18:12 +0000826 if upstream_git_obj is None:
827 if self.GetBranch() is None:
828 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +0000829 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +0000830 else:
831 print >> sys.stderr, (
832 'ERROR: no upstream branch')
833 return False
834
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000835 # Verify the commit we're diffing against is in our current branch.
836 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
837 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
838 if upstream_sha != common_ancestor:
839 print >> sys.stderr, (
840 'ERROR: %s is not in the current branch. You may need to rebase '
841 'your tracking branch' % upstream_sha)
842 return False
843
844 # List the commits inside the diff, and verify they are all local.
845 commits_in_diff = RunGit(
846 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
847 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
848 remote_branch = remote_branch.strip()
849 if code != 0:
850 _, remote_branch = self.GetRemoteBranch()
851
852 commits_in_remote = RunGit(
853 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
854
855 common_commits = set(commits_in_diff) & set(commits_in_remote)
856 if common_commits:
857 print >> sys.stderr, (
858 'ERROR: Your diff contains %d commits already in %s.\n'
859 'Run "git log --oneline %s..HEAD" to get a list of commits in '
860 'the diff. If you are using a custom git flow, you can override'
861 ' the reference used for this check with "git config '
862 'gitcl.remotebranch <git-ref>".' % (
863 len(common_commits), remote_branch, upstream_git_obj))
864 return False
865 return True
866
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000867 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000868 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000869
870 Returns None if it is not set.
871 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000872 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
873 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000874
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000875 def GetGitSvnRemoteUrl(self):
876 """Return the configured git-svn remote URL parsed from git svn info.
877
878 Returns None if it is not set.
879 """
880 # URL is dependent on the current directory.
881 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
882 if data:
883 keys = dict(line.split(': ', 1) for line in data.splitlines()
884 if ': ' in line)
885 return keys.get('URL', None)
886 return None
887
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000888 def GetRemoteUrl(self):
889 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
890
891 Returns None if there is no remote.
892 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000893 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000894 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
895
896 # If URL is pointing to a local directory, it is probably a git cache.
897 if os.path.isdir(url):
898 url = RunGit(['config', 'remote.%s.url' % remote],
899 error_ok=True,
900 cwd=url).strip()
901 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000902
903 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000904 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000905 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000906 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000907 self.issue = int(issue) or None if issue else None
908 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000909 return self.issue
910
911 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000912 if not self.rietveld_server:
913 # If we're on a branch then get the server potentially associated
914 # with that branch.
915 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000916 rietveld_server_config = self._RietveldServer()
917 if rietveld_server_config:
918 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
919 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000920 if not self.rietveld_server:
921 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000922 return self.rietveld_server
923
924 def GetIssueURL(self):
925 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000926 if not self.GetIssue():
927 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000928 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
929
930 def GetDescription(self, pretty=False):
931 if not self.has_description:
932 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000933 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000934 try:
935 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000936 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000937 if e.code == 404:
938 DieWithError(
939 ('\nWhile fetching the description for issue %d, received a '
940 '404 (not found)\n'
941 'error. It is likely that you deleted this '
942 'issue on the server. If this is the\n'
943 'case, please run\n\n'
944 ' git cl issue 0\n\n'
945 'to clear the association with the deleted issue. Then run '
946 'this command again.') % issue)
947 else:
948 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000949 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000950 except urllib2.URLError as e:
951 print >> sys.stderr, (
952 'Warning: Failed to retrieve CL description due to network '
953 'failure.')
954 self.description = ''
955
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000956 self.has_description = True
957 if pretty:
958 wrapper = textwrap.TextWrapper()
959 wrapper.initial_indent = wrapper.subsequent_indent = ' '
960 return wrapper.fill(self.description)
961 return self.description
962
963 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000964 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000965 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000966 patchset = RunGit(['config', self._PatchsetSetting()],
967 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000968 self.patchset = int(patchset) or None if patchset else None
969 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000970 return self.patchset
971
972 def SetPatchset(self, patchset):
973 """Set this branch's patchset. If patchset=0, clears the patchset."""
974 if patchset:
975 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000976 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000977 else:
978 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000979 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000980 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000981
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000982 def GetMostRecentPatchset(self):
983 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000984
985 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000986 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000987 '/download/issue%s_%s.diff' % (issue, patchset))
988
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000989 def GetIssueProperties(self):
990 if self._props is None:
991 issue = self.GetIssue()
992 if not issue:
993 self._props = {}
994 else:
995 self._props = self.RpcServer().get_issue_properties(issue, True)
996 return self._props
997
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000998 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000999 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001000
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001001 def AddComment(self, message):
1002 return self.RpcServer().add_comment(self.GetIssue(), message)
1003
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001004 def SetIssue(self, issue):
1005 """Set this branch's issue. If issue=0, clears the issue."""
1006 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001007 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008 RunGit(['config', self._IssueSetting(), str(issue)])
1009 if self.rietveld_server:
1010 RunGit(['config', self._RietveldServer(), self.rietveld_server])
1011 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001012 current_issue = self.GetIssue()
1013 if current_issue:
1014 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001015 self.issue = None
1016 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001017
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001018 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001019 if not self.GitSanityChecks(upstream_branch):
1020 DieWithError('\nGit sanity check failure')
1021
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001022 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001023 if not root:
1024 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001025 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001026
1027 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001028 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001029 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001030 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001031 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001032 except subprocess2.CalledProcessError:
1033 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001034 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001035 'This branch probably doesn\'t exist anymore. To reset the\n'
1036 'tracking branch, please run\n'
1037 ' git branch --set-upstream %s trunk\n'
1038 'replacing trunk with origin/master or the relevant branch') %
1039 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001040
maruel@chromium.org52424302012-08-29 15:14:30 +00001041 issue = self.GetIssue()
1042 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001043 if issue:
1044 description = self.GetDescription()
1045 else:
1046 # If the change was never uploaded, use the log messages of all commits
1047 # up to the branch point, as git cl upload will prefill the description
1048 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001049 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1050 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001051
1052 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001053 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001054 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001055 name,
1056 description,
1057 absroot,
1058 files,
1059 issue,
1060 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001061 author,
1062 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001063
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001064 def GetStatus(self):
1065 """Apply a rough heuristic to give a simple summary of an issue's review
1066 or CQ status, assuming adherence to a common workflow.
1067
1068 Returns None if no issue for this branch, or one of the following keywords:
1069 * 'error' - error from review tool (including deleted issues)
1070 * 'unsent' - not sent for review
1071 * 'waiting' - waiting for review
1072 * 'reply' - waiting for owner to reply to review
1073 * 'lgtm' - LGTM from at least one approved reviewer
1074 * 'commit' - in the commit queue
1075 * 'closed' - closed
1076 """
1077 if not self.GetIssue():
1078 return None
1079
1080 try:
1081 props = self.GetIssueProperties()
1082 except urllib2.HTTPError:
1083 return 'error'
1084
1085 if props.get('closed'):
1086 # Issue is closed.
1087 return 'closed'
1088 if props.get('commit'):
1089 # Issue is in the commit queue.
1090 return 'commit'
1091
1092 try:
1093 reviewers = self.GetApprovingReviewers()
1094 except urllib2.HTTPError:
1095 return 'error'
1096
1097 if reviewers:
1098 # Was LGTM'ed.
1099 return 'lgtm'
1100
1101 messages = props.get('messages') or []
1102
1103 if not messages:
1104 # No message was sent.
1105 return 'unsent'
1106 if messages[-1]['sender'] != props.get('owner_email'):
1107 # Non-LGTM reply from non-owner
1108 return 'reply'
1109 return 'waiting'
1110
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001111 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001112 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001113
1114 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001115 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001116 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001117 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001118 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001119 except presubmit_support.PresubmitFailure, e:
1120 DieWithError(
1121 ('%s\nMaybe your depot_tools is out of date?\n'
1122 'If all fails, contact maruel@') % e)
1123
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001124 def UpdateDescription(self, description):
1125 self.description = description
1126 return self.RpcServer().update_description(
1127 self.GetIssue(), self.description)
1128
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001130 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001131 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001133 def SetFlag(self, flag, value):
1134 """Patchset must match."""
1135 if not self.GetPatchset():
1136 DieWithError('The patchset needs to match. Send another patchset.')
1137 try:
1138 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001139 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001140 except urllib2.HTTPError, e:
1141 if e.code == 404:
1142 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1143 if e.code == 403:
1144 DieWithError(
1145 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1146 'match?') % (self.GetIssue(), self.GetPatchset()))
1147 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001149 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001150 """Returns an upload.RpcServer() to access this review's rietveld instance.
1151 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001152 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001153 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001154 self.GetRietveldServer(),
1155 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001156 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001157
1158 def _IssueSetting(self):
1159 """Return the git setting that stores this change's issue."""
1160 return 'branch.%s.rietveldissue' % self.GetBranch()
1161
1162 def _PatchsetSetting(self):
1163 """Return the git setting that stores this change's most recent patchset."""
1164 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1165
1166 def _RietveldServer(self):
1167 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001168 branch = self.GetBranch()
1169 if branch:
1170 return 'branch.%s.rietveldserver' % branch
1171 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001172
1173
1174def GetCodereviewSettingsInteractively():
1175 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001176 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001177 server = settings.GetDefaultServerUrl(error_ok=True)
1178 prompt = 'Rietveld server (host[:port])'
1179 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001180 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001181 if not server and not newserver:
1182 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001183 if newserver:
1184 newserver = gclient_utils.UpgradeToHttps(newserver)
1185 if newserver != server:
1186 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001187
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001188 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001189 prompt = caption
1190 if initial:
1191 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001192 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001193 if new_val == 'x':
1194 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001195 elif new_val:
1196 if is_url:
1197 new_val = gclient_utils.UpgradeToHttps(new_val)
1198 if new_val != initial:
1199 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001200
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001201 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001202 SetProperty(settings.GetDefaultPrivateFlag(),
1203 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001204 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001205 'tree-status-url', False)
1206 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001207 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001208 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1209 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210
1211 # TODO: configure a default branch to diff against, rather than this
1212 # svn-based hackery.
1213
1214
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001215class ChangeDescription(object):
1216 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001217 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001218 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001219
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001220 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001221 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001222
agable@chromium.org42c20792013-09-12 17:34:49 +00001223 @property # www.logilab.org/ticket/89786
1224 def description(self): # pylint: disable=E0202
1225 return '\n'.join(self._description_lines)
1226
1227 def set_description(self, desc):
1228 if isinstance(desc, basestring):
1229 lines = desc.splitlines()
1230 else:
1231 lines = [line.rstrip() for line in desc]
1232 while lines and not lines[0]:
1233 lines.pop(0)
1234 while lines and not lines[-1]:
1235 lines.pop(-1)
1236 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001237
piman@chromium.org336f9122014-09-04 02:16:55 +00001238 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001239 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001240 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001241 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001242 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001243 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001244
agable@chromium.org42c20792013-09-12 17:34:49 +00001245 # Get the set of R= and TBR= lines and remove them from the desciption.
1246 regexp = re.compile(self.R_LINE)
1247 matches = [regexp.match(line) for line in self._description_lines]
1248 new_desc = [l for i, l in enumerate(self._description_lines)
1249 if not matches[i]]
1250 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001251
agable@chromium.org42c20792013-09-12 17:34:49 +00001252 # Construct new unified R= and TBR= lines.
1253 r_names = []
1254 tbr_names = []
1255 for match in matches:
1256 if not match:
1257 continue
1258 people = cleanup_list([match.group(2).strip()])
1259 if match.group(1) == 'TBR':
1260 tbr_names.extend(people)
1261 else:
1262 r_names.extend(people)
1263 for name in r_names:
1264 if name not in reviewers:
1265 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001266 if add_owners_tbr:
1267 owners_db = owners.Database(change.RepositoryRoot(),
1268 fopen=file, os_path=os.path, glob=glob.glob)
1269 all_reviewers = set(tbr_names + reviewers)
1270 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1271 all_reviewers)
1272 tbr_names.extend(owners_db.reviewers_for(missing_files,
1273 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001274 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1275 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1276
1277 # Put the new lines in the description where the old first R= line was.
1278 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1279 if 0 <= line_loc < len(self._description_lines):
1280 if new_tbr_line:
1281 self._description_lines.insert(line_loc, new_tbr_line)
1282 if new_r_line:
1283 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001284 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001285 if new_r_line:
1286 self.append_footer(new_r_line)
1287 if new_tbr_line:
1288 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001289
1290 def prompt(self):
1291 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001292 self.set_description([
1293 '# Enter a description of the change.',
1294 '# This will be displayed on the codereview site.',
1295 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001296 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001297 '--------------------',
1298 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001299
agable@chromium.org42c20792013-09-12 17:34:49 +00001300 regexp = re.compile(self.BUG_LINE)
1301 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001302 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001303 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001304 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001305 if not content:
1306 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001307 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001308
1309 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001310 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1311 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001312 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001313 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001314
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001315 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001316 if self._description_lines:
1317 # Add an empty line if either the last line or the new line isn't a tag.
1318 last_line = self._description_lines[-1]
1319 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1320 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1321 self._description_lines.append('')
1322 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001323
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001324 def get_reviewers(self):
1325 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001326 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1327 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001328 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001329
1330
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001331def get_approving_reviewers(props):
1332 """Retrieves the reviewers that approved a CL from the issue properties with
1333 messages.
1334
1335 Note that the list may contain reviewers that are not committer, thus are not
1336 considered by the CQ.
1337 """
1338 return sorted(
1339 set(
1340 message['sender']
1341 for message in props['messages']
1342 if message['approval'] and message['sender'] in props['reviewers']
1343 )
1344 )
1345
1346
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001347def FindCodereviewSettingsFile(filename='codereview.settings'):
1348 """Finds the given file starting in the cwd and going up.
1349
1350 Only looks up to the top of the repository unless an
1351 'inherit-review-settings-ok' file exists in the root of the repository.
1352 """
1353 inherit_ok_file = 'inherit-review-settings-ok'
1354 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001355 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001356 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1357 root = '/'
1358 while True:
1359 if filename in os.listdir(cwd):
1360 if os.path.isfile(os.path.join(cwd, filename)):
1361 return open(os.path.join(cwd, filename))
1362 if cwd == root:
1363 break
1364 cwd = os.path.dirname(cwd)
1365
1366
1367def LoadCodereviewSettingsFromFile(fileobj):
1368 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001369 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001370
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 def SetProperty(name, setting, unset_error_ok=False):
1372 fullname = 'rietveld.' + name
1373 if setting in keyvals:
1374 RunGit(['config', fullname, keyvals[setting]])
1375 else:
1376 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1377
1378 SetProperty('server', 'CODE_REVIEW_SERVER')
1379 # Only server setting is required. Other settings can be absent.
1380 # In that case, we ignore errors raised during option deletion attempt.
1381 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001382 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001383 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1384 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001385 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001386 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001387 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1388 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001389 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001390 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001391 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001392 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1393 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001394
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001395 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001396 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001397
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00001398 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
1399 RunGit(['config', 'gerrit.squash-uploads',
1400 keyvals['GERRIT_SQUASH_UPLOADS']])
1401
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1403 #should be of the form
1404 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1405 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1406 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1407 keyvals['ORIGIN_URL_CONFIG']])
1408
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001410def urlretrieve(source, destination):
1411 """urllib is broken for SSL connections via a proxy therefore we
1412 can't use urllib.urlretrieve()."""
1413 with open(destination, 'w') as f:
1414 f.write(urllib2.urlopen(source).read())
1415
1416
ukai@chromium.org712d6102013-11-27 00:52:58 +00001417def hasSheBang(fname):
1418 """Checks fname is a #! script."""
1419 with open(fname) as f:
1420 return f.read(2).startswith('#!')
1421
1422
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001423def DownloadHooks(force):
1424 """downloads hooks
1425
1426 Args:
1427 force: True to update hooks. False to install hooks if not present.
1428 """
1429 if not settings.GetIsGerrit():
1430 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001431 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001432 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1433 if not os.access(dst, os.X_OK):
1434 if os.path.exists(dst):
1435 if not force:
1436 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001437 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001438 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001439 if not hasSheBang(dst):
1440 DieWithError('Not a script: %s\n'
1441 'You need to download from\n%s\n'
1442 'into .git/hooks/commit-msg and '
1443 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001444 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1445 except Exception:
1446 if os.path.exists(dst):
1447 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001448 DieWithError('\nFailed to download hooks.\n'
1449 'You need to download from\n%s\n'
1450 'into .git/hooks/commit-msg and '
1451 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001452
1453
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001454@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001455def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001456 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001457
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001458 parser.add_option('--activate-update', action='store_true',
1459 help='activate auto-updating [rietveld] section in '
1460 '.git/config')
1461 parser.add_option('--deactivate-update', action='store_true',
1462 help='deactivate auto-updating [rietveld] section in '
1463 '.git/config')
1464 options, args = parser.parse_args(args)
1465
1466 if options.deactivate_update:
1467 RunGit(['config', 'rietveld.autoupdate', 'false'])
1468 return
1469
1470 if options.activate_update:
1471 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1472 return
1473
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001474 if len(args) == 0:
1475 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001476 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001477 return 0
1478
1479 url = args[0]
1480 if not url.endswith('codereview.settings'):
1481 url = os.path.join(url, 'codereview.settings')
1482
1483 # Load code review settings and download hooks (if available).
1484 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001485 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001486 return 0
1487
1488
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001489def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001490 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001491 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1492 branch = ShortBranchName(branchref)
1493 _, args = parser.parse_args(args)
1494 if not args:
1495 print("Current base-url:")
1496 return RunGit(['config', 'branch.%s.base-url' % branch],
1497 error_ok=False).strip()
1498 else:
1499 print("Setting base-url to %s" % args[0])
1500 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1501 error_ok=False).strip()
1502
1503
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001504def color_for_status(status):
1505 """Maps a Changelist status to color, for CMDstatus and other tools."""
1506 return {
1507 'unsent': Fore.RED,
1508 'waiting': Fore.BLUE,
1509 'reply': Fore.YELLOW,
1510 'lgtm': Fore.GREEN,
1511 'commit': Fore.MAGENTA,
1512 'closed': Fore.CYAN,
1513 'error': Fore.WHITE,
1514 }.get(status, Fore.WHITE)
1515
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001516def fetch_cl_status(branch, auth_config=None):
1517 """Fetches information for an issue and returns (branch, issue, status)."""
1518 cl = Changelist(branchref=branch, auth_config=auth_config)
1519 url = cl.GetIssueURL()
1520 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001521
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001522 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001523 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001524 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001525
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001526 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001527
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001528def get_cl_statuses(
1529 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001530 """Returns a blocking iterable of (branch, issue, color) for given branches.
1531
1532 If fine_grained is true, this will fetch CL statuses from the server.
1533 Otherwise, simply indicate if there's a matching url for the given branches.
1534
1535 If max_processes is specified, it is used as the maximum number of processes
1536 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1537 spawned.
1538 """
1539 # Silence upload.py otherwise it becomes unwieldly.
1540 upload.verbosity = 0
1541
1542 if fine_grained:
1543 # Process one branch synchronously to work through authentication, then
1544 # spawn processes to process all the other branches in parallel.
1545 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001546 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1547 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001548
1549 branches_to_fetch = branches[1:]
1550 pool = ThreadPool(
1551 min(max_processes, len(branches_to_fetch))
1552 if max_processes is not None
1553 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001554 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001555 yield x
1556 else:
1557 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1558 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001559 cl = Changelist(branchref=b, auth_config=auth_config)
1560 url = cl.GetIssueURL()
1561 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001562
rmistry@google.com2dd99862015-06-22 12:22:18 +00001563
1564def upload_branch_deps(cl, args):
1565 """Uploads CLs of local branches that are dependents of the current branch.
1566
1567 If the local branch dependency tree looks like:
1568 test1 -> test2.1 -> test3.1
1569 -> test3.2
1570 -> test2.2 -> test3.3
1571
1572 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1573 run on the dependent branches in this order:
1574 test2.1, test3.1, test3.2, test2.2, test3.3
1575
1576 Note: This function does not rebase your local dependent branches. Use it when
1577 you make a change to the parent branch that will not conflict with its
1578 dependent branches, and you would like their dependencies updated in
1579 Rietveld.
1580 """
1581 if git_common.is_dirty_git_tree('upload-branch-deps'):
1582 return 1
1583
1584 root_branch = cl.GetBranch()
1585 if root_branch is None:
1586 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1587 'Get on a branch!')
1588 if not cl.GetIssue() or not cl.GetPatchset():
1589 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1590 'patchset dependencies without an uploaded CL.')
1591
1592 branches = RunGit(['for-each-ref',
1593 '--format=%(refname:short) %(upstream:short)',
1594 'refs/heads'])
1595 if not branches:
1596 print('No local branches found.')
1597 return 0
1598
1599 # Create a dictionary of all local branches to the branches that are dependent
1600 # on it.
1601 tracked_to_dependents = collections.defaultdict(list)
1602 for b in branches.splitlines():
1603 tokens = b.split()
1604 if len(tokens) == 2:
1605 branch_name, tracked = tokens
1606 tracked_to_dependents[tracked].append(branch_name)
1607
1608 print
1609 print 'The dependent local branches of %s are:' % root_branch
1610 dependents = []
1611 def traverse_dependents_preorder(branch, padding=''):
1612 dependents_to_process = tracked_to_dependents.get(branch, [])
1613 padding += ' '
1614 for dependent in dependents_to_process:
1615 print '%s%s' % (padding, dependent)
1616 dependents.append(dependent)
1617 traverse_dependents_preorder(dependent, padding)
1618 traverse_dependents_preorder(root_branch)
1619 print
1620
1621 if not dependents:
1622 print 'There are no dependent local branches for %s' % root_branch
1623 return 0
1624
1625 print ('This command will checkout all dependent branches and run '
1626 '"git cl upload".')
1627 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1628
andybons@chromium.org962f9462016-02-03 20:00:42 +00001629 # Add a default patchset title to all upload calls in Rietveld.
1630 if not settings.GetIsGerrit():
1631 args.extend(['-t', 'Updated patchset dependency'])
1632
rmistry@google.com2dd99862015-06-22 12:22:18 +00001633 # Record all dependents that failed to upload.
1634 failures = {}
1635 # Go through all dependents, checkout the branch and upload.
1636 try:
1637 for dependent_branch in dependents:
1638 print
1639 print '--------------------------------------'
1640 print 'Running "git cl upload" from %s:' % dependent_branch
1641 RunGit(['checkout', '-q', dependent_branch])
1642 print
1643 try:
1644 if CMDupload(OptionParser(), args) != 0:
1645 print 'Upload failed for %s!' % dependent_branch
1646 failures[dependent_branch] = 1
1647 except: # pylint: disable=W0702
1648 failures[dependent_branch] = 1
1649 print
1650 finally:
1651 # Swap back to the original root branch.
1652 RunGit(['checkout', '-q', root_branch])
1653
1654 print
1655 print 'Upload complete for dependent branches!'
1656 for dependent_branch in dependents:
1657 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1658 print ' %s : %s' % (dependent_branch, upload_status)
1659 print
1660
1661 return 0
1662
1663
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001664def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001665 """Show status of changelists.
1666
1667 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001668 - Red not sent for review or broken
1669 - Blue waiting for review
1670 - Yellow waiting for you to reply to review
1671 - Green LGTM'ed
1672 - Magenta in the commit queue
1673 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001674
1675 Also see 'git cl comments'.
1676 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001677 parser.add_option('--field',
1678 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001679 parser.add_option('-f', '--fast', action='store_true',
1680 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001681 parser.add_option(
1682 '-j', '--maxjobs', action='store', type=int,
1683 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001684
1685 auth.add_auth_options(parser)
1686 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001687 if args:
1688 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001689 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001690
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001691 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001692 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001693 if options.field.startswith('desc'):
1694 print cl.GetDescription()
1695 elif options.field == 'id':
1696 issueid = cl.GetIssue()
1697 if issueid:
1698 print issueid
1699 elif options.field == 'patch':
1700 patchset = cl.GetPatchset()
1701 if patchset:
1702 print patchset
1703 elif options.field == 'url':
1704 url = cl.GetIssueURL()
1705 if url:
1706 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001707 return 0
1708
1709 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1710 if not branches:
1711 print('No local branch found.')
1712 return 0
1713
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001714 changes = (
1715 Changelist(branchref=b, auth_config=auth_config)
1716 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001717 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001718 alignment = max(5, max(len(b) for b in branches))
1719 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001720 output = get_cl_statuses(branches,
1721 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001722 max_processes=options.maxjobs,
1723 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001724
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001725 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001726 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001727 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001728 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001729 b, i, status = output.next()
1730 branch_statuses[b] = (i, status)
1731 issue_url, status = branch_statuses.pop(branch)
1732 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001733 reset = Fore.RESET
1734 if not sys.stdout.isatty():
1735 color = ''
1736 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001737 status_str = '(%s)' % status if status else ''
1738 print ' %*s : %s%s %s%s' % (
1739 alignment, ShortBranchName(branch), color, issue_url, status_str,
1740 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001741
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001742 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001743 print
1744 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001745 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001746 if not cl.GetIssue():
1747 print 'No issue assigned.'
1748 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001749 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001750 if not options.fast:
1751 print 'Issue description:'
1752 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001753 return 0
1754
1755
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001756def colorize_CMDstatus_doc():
1757 """To be called once in main() to add colors to git cl status help."""
1758 colors = [i for i in dir(Fore) if i[0].isupper()]
1759
1760 def colorize_line(line):
1761 for color in colors:
1762 if color in line.upper():
1763 # Extract whitespaces first and the leading '-'.
1764 indent = len(line) - len(line.lstrip(' ')) + 1
1765 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1766 return line
1767
1768 lines = CMDstatus.__doc__.splitlines()
1769 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1770
1771
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001772@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001773def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001774 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001775
1776 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001777 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001778 parser.add_option('-r', '--reverse', action='store_true',
1779 help='Lookup the branch(es) for the specified issues. If '
1780 'no issues are specified, all branches with mapped '
1781 'issues will be listed.')
1782 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001783
dnj@chromium.org406c4402015-03-03 17:22:28 +00001784 if options.reverse:
1785 branches = RunGit(['for-each-ref', 'refs/heads',
1786 '--format=%(refname:short)']).splitlines()
1787
1788 # Reverse issue lookup.
1789 issue_branch_map = {}
1790 for branch in branches:
1791 cl = Changelist(branchref=branch)
1792 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1793 if not args:
1794 args = sorted(issue_branch_map.iterkeys())
1795 for issue in args:
1796 if not issue:
1797 continue
1798 print 'Branch for issue number %s: %s' % (
1799 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1800 else:
1801 cl = Changelist()
1802 if len(args) > 0:
1803 try:
1804 issue = int(args[0])
1805 except ValueError:
1806 DieWithError('Pass a number to set the issue or none to list it.\n'
1807 'Maybe you want to run git cl status?')
1808 cl.SetIssue(issue)
1809 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001810 return 0
1811
1812
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001813def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001814 """Shows or posts review comments for any changelist."""
1815 parser.add_option('-a', '--add-comment', dest='comment',
1816 help='comment to add to an issue')
1817 parser.add_option('-i', dest='issue',
1818 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00001819 parser.add_option('-j', '--json-file',
1820 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001821 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001822 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001823 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001824
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001825 issue = None
1826 if options.issue:
1827 try:
1828 issue = int(options.issue)
1829 except ValueError:
1830 DieWithError('A review issue id is expected to be a number')
1831
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001832 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001833
1834 if options.comment:
1835 cl.AddComment(options.comment)
1836 return 0
1837
1838 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00001839 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001840 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00001841 summary.append({
1842 'date': message['date'],
1843 'lgtm': False,
1844 'message': message['text'],
1845 'not_lgtm': False,
1846 'sender': message['sender'],
1847 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001848 if message['disapproval']:
1849 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00001850 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001851 elif message['approval']:
1852 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00001853 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001854 elif message['sender'] == data['owner_email']:
1855 color = Fore.MAGENTA
1856 else:
1857 color = Fore.BLUE
1858 print '\n%s%s %s%s' % (
1859 color, message['date'].split('.', 1)[0], message['sender'],
1860 Fore.RESET)
1861 if message['text'].strip():
1862 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00001863 if options.json_file:
1864 with open(options.json_file, 'wb') as f:
1865 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001866 return 0
1867
1868
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001869def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001870 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00001871 parser.add_option('-d', '--display', action='store_true',
1872 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001873 auth.add_auth_options(parser)
1874 options, _ = parser.parse_args(args)
1875 auth_config = auth.extract_auth_config_from_options(options)
1876 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001877 if not cl.GetIssue():
1878 DieWithError('This branch has no associated changelist.')
1879 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00001880 if options.display:
1881 print description.description
1882 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001883 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00001884 if cl.GetDescription() != description.description:
1885 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001886 return 0
1887
1888
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001889def CreateDescriptionFromLog(args):
1890 """Pulls out the commit log to use as a base for the CL description."""
1891 log_args = []
1892 if len(args) == 1 and not args[0].endswith('.'):
1893 log_args = [args[0] + '..']
1894 elif len(args) == 1 and args[0].endswith('...'):
1895 log_args = [args[0][:-1]]
1896 elif len(args) == 2:
1897 log_args = [args[0] + '..' + args[1]]
1898 else:
1899 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001900 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001901
1902
thestig@chromium.org44202a22014-03-11 19:22:18 +00001903def CMDlint(parser, args):
1904 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001905 parser.add_option('--filter', action='append', metavar='-x,+y',
1906 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001907 auth.add_auth_options(parser)
1908 options, args = parser.parse_args(args)
1909 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001910
1911 # Access to a protected member _XX of a client class
1912 # pylint: disable=W0212
1913 try:
1914 import cpplint
1915 import cpplint_chromium
1916 except ImportError:
1917 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1918 return 1
1919
1920 # Change the current working directory before calling lint so that it
1921 # shows the correct base.
1922 previous_cwd = os.getcwd()
1923 os.chdir(settings.GetRoot())
1924 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001925 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001926 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1927 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001928 if not files:
1929 print "Cannot lint an empty CL"
1930 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001931
1932 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001933 command = args + files
1934 if options.filter:
1935 command = ['--filter=' + ','.join(options.filter)] + command
1936 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001937
1938 white_regex = re.compile(settings.GetLintRegex())
1939 black_regex = re.compile(settings.GetLintIgnoreRegex())
1940 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1941 for filename in filenames:
1942 if white_regex.match(filename):
1943 if black_regex.match(filename):
1944 print "Ignoring file %s" % filename
1945 else:
1946 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1947 extra_check_functions)
1948 else:
1949 print "Skipping file %s" % filename
1950 finally:
1951 os.chdir(previous_cwd)
1952 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1953 if cpplint._cpplint_state.error_count != 0:
1954 return 1
1955 return 0
1956
1957
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001958def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001959 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001960 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001961 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001962 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001963 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001964 auth.add_auth_options(parser)
1965 options, args = parser.parse_args(args)
1966 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001967
sbc@chromium.org71437c02015-04-09 19:29:40 +00001968 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00001969 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001970 return 1
1971
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001972 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001973 if args:
1974 base_branch = args[0]
1975 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001976 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001977 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001978
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001979 cl.RunHook(
1980 committing=not options.upload,
1981 may_prompt=False,
1982 verbose=options.verbose,
1983 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001984 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001985
1986
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001987def AddChangeIdToCommitMessage(options, args):
1988 """Re-commits using the current message, assumes the commit hook is in
1989 place.
1990 """
1991 log_desc = options.message or CreateDescriptionFromLog(args)
1992 git_command = ['commit', '--amend', '-m', log_desc]
1993 RunGit(git_command)
1994 new_log_desc = CreateDescriptionFromLog(args)
1995 if CHANGE_ID in new_log_desc:
1996 print 'git-cl: Added Change-Id to commit message.'
1997 else:
1998 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1999
2000
piman@chromium.org336f9122014-09-04 02:16:55 +00002001def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002002 """upload the current branch to gerrit."""
2003 # We assume the remote called "origin" is the one we want.
2004 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002005 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00002006
2007 remote, remote_branch = cl.GetRemoteBranch()
2008 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2009 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002010
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002011 change_desc = ChangeDescription(
2012 options.message or CreateDescriptionFromLog(args))
2013 if not change_desc.description:
andybons@chromium.org962f9462016-02-03 20:00:42 +00002014 print "\nDescription is empty. Aborting..."
2015 return 1
2016
2017 if options.title:
2018 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002019 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002020
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002021 if options.squash:
2022 # Try to get the message from a previous upload.
2023 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
bauerb@chromium.org13502e02016-02-18 10:18:29 +00002024 message = RunGitSilent(['show', '--format=%B', '-s', shadow_branch])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002025 if not message:
2026 if not options.force:
2027 change_desc.prompt()
2028
2029 if CHANGE_ID not in change_desc.description:
2030 # Run the commit-msg hook without modifying the head commit by writing
2031 # the commit message to a temporary file and running the hook over it,
2032 # then reading the file back in.
2033 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
2034 'commit-msg')
2035 file_handle, msg_file = tempfile.mkstemp(text=True,
2036 prefix='commit_msg')
tandrii@chromium.orga83663a2016-01-14 16:01:00 +00002037 logging.debug("%s %s", file_handle, msg_file)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002038 try:
2039 try:
tandrii@chromium.orga83663a2016-01-14 16:01:00 +00002040 try:
2041 fileobj = os.fdopen(file_handle, 'w')
2042 except OSError:
2043 # if fdopen fails, file_handle remains open.
2044 # See https://docs.python.org/2/library/os.html#os.fdopen.
2045 os.close(file_handle)
2046 raise
2047 with fileobj:
2048 # This will close the file_handle.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002049 fileobj.write(change_desc.description)
tandrii@chromium.orga83663a2016-01-14 16:01:00 +00002050 logging.debug("%s %s finish editing", file_handle, msg_file)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002051 finally:
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002052 RunCommand([commit_msg_hook, msg_file])
2053 change_desc.set_description(gclient_utils.FileRead(msg_file))
2054 finally:
2055 os.remove(msg_file)
2056
2057 if not change_desc.description:
2058 print "Description is empty; aborting."
2059 return 1
2060
2061 message = change_desc.description
2062
2063 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2064 if remote is '.':
2065 # If our upstream branch is local, we base our squashed commit on its
2066 # squashed version.
2067 parent = ('refs/heads/git_cl_uploads/' +
2068 scm.GIT.ShortBranchName(upstream_branch))
2069
2070 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2071 # will create additional CLs when uploading.
2072 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2073 RunGitSilent(['rev-parse', parent + ':'])):
2074 print 'Upload upstream branch ' + upstream_branch + ' first.'
2075 return 1
2076 else:
2077 parent = cl.GetCommonAncestorWithUpstream()
2078
2079 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2080 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2081 '-m', message]).strip()
2082 else:
2083 if CHANGE_ID not in change_desc.description:
2084 AddChangeIdToCommitMessage(options, args)
2085 ref_to_push = 'HEAD'
2086 parent = '%s/%s' % (gerrit_remote, branch)
2087
2088 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2089 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002090 if len(commits) > 1:
2091 print('WARNING: This will upload %d commits. Run the following command '
2092 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002093 print('git log %s..%s' % (parent, ref_to_push))
2094 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002095 'commit.')
2096 ask_for_data('About to upload; enter to confirm.')
2097
piman@chromium.org336f9122014-09-04 02:16:55 +00002098 if options.reviewers or options.tbr_owners:
2099 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002100
ukai@chromium.orge8077812012-02-03 03:41:46 +00002101 receive_options = []
2102 cc = cl.GetCCList().split(',')
2103 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002104 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002105 cc = filter(None, cc)
2106 if cc:
2107 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002108 if change_desc.get_reviewers():
2109 receive_options.extend(
2110 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002111
ukai@chromium.orge8077812012-02-03 03:41:46 +00002112 git_command = ['push']
2113 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002114 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002115 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002116 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002117 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002118
2119 if options.squash:
2120 head = RunGit(['rev-parse', 'HEAD']).strip()
2121 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2122
ukai@chromium.orge8077812012-02-03 03:41:46 +00002123 # TODO(ukai): parse Change-Id: and set issue number?
2124 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002125
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002126
wittman@chromium.org455dc922015-01-26 20:15:50 +00002127def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2128 """Computes the remote branch ref to use for the CL.
2129
2130 Args:
2131 remote (str): The git remote for the CL.
2132 remote_branch (str): The git remote branch for the CL.
2133 target_branch (str): The target branch specified by the user.
2134 pending_prefix (str): The pending prefix from the settings.
2135 """
2136 if not (remote and remote_branch):
2137 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002138
wittman@chromium.org455dc922015-01-26 20:15:50 +00002139 if target_branch:
2140 # Cannonicalize branch references to the equivalent local full symbolic
2141 # refs, which are then translated into the remote full symbolic refs
2142 # below.
2143 if '/' not in target_branch:
2144 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2145 else:
2146 prefix_replacements = (
2147 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2148 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2149 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2150 )
2151 match = None
2152 for regex, replacement in prefix_replacements:
2153 match = re.search(regex, target_branch)
2154 if match:
2155 remote_branch = target_branch.replace(match.group(0), replacement)
2156 break
2157 if not match:
2158 # This is a branch path but not one we recognize; use as-is.
2159 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002160 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2161 # Handle the refs that need to land in different refs.
2162 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002163
wittman@chromium.org455dc922015-01-26 20:15:50 +00002164 # Create the true path to the remote branch.
2165 # Does the following translation:
2166 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2167 # * refs/remotes/origin/master -> refs/heads/master
2168 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2169 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2170 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2171 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2172 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2173 'refs/heads/')
2174 elif remote_branch.startswith('refs/remotes/branch-heads'):
2175 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2176 # If a pending prefix exists then replace refs/ with it.
2177 if pending_prefix:
2178 remote_branch = remote_branch.replace('refs/', pending_prefix)
2179 return remote_branch
2180
2181
piman@chromium.org336f9122014-09-04 02:16:55 +00002182def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002183 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002184 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2185 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002186 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002187 if options.emulate_svn_auto_props:
2188 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002189
2190 change_desc = None
2191
pgervais@chromium.org91141372014-01-09 23:27:20 +00002192 if options.email is not None:
2193 upload_args.extend(['--email', options.email])
2194
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002195 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002196 if options.title:
2197 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002198 if options.message:
2199 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002200 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002201 print ("This branch is associated with issue %s. "
2202 "Adding patch to that issue." % cl.GetIssue())
2203 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002204 if options.title:
2205 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002206 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002207 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002208 if options.reviewers or options.tbr_owners:
2209 change_desc.update_reviewers(options.reviewers,
2210 options.tbr_owners,
2211 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002212 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002213 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002214
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002215 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002216 print "Description is empty; aborting."
2217 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002218
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002219 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002220 if change_desc.get_reviewers():
2221 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002222 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002223 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002224 DieWithError("Must specify reviewers to send email.")
2225 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002226
2227 # We check this before applying rietveld.private assuming that in
2228 # rietveld.cc only addresses which we can send private CLs to are listed
2229 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2230 # --private is specified explicitly on the command line.
2231 if options.private:
2232 logging.warn('rietveld.cc is ignored since private flag is specified. '
2233 'You need to review and add them manually if necessary.')
2234 cc = cl.GetCCListWithoutDefault()
2235 else:
2236 cc = cl.GetCCList()
2237 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002238 if cc:
2239 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002240
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002241 if options.private or settings.GetDefaultPrivateFlag() == "True":
2242 upload_args.append('--private')
2243
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002244 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002245 if not options.find_copies:
2246 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002247
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002248 # Include the upstream repo's URL in the change -- this is useful for
2249 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002250 remote_url = cl.GetGitBaseUrlFromConfig()
2251 if not remote_url:
2252 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002253 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002254 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002255 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2256 remote_url = (cl.GetRemoteUrl() + '@'
2257 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002258 if remote_url:
2259 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002260 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002261 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2262 settings.GetPendingRefPrefix())
2263 if target_ref:
2264 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002265
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002266 # Look for dependent patchsets. See crbug.com/480453 for more details.
2267 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2268 upstream_branch = ShortBranchName(upstream_branch)
2269 if remote is '.':
2270 # A local branch is being tracked.
2271 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002272 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002273 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002274 print ('Skipping dependency patchset upload because git config '
2275 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002276 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002277 else:
2278 auth_config = auth.extract_auth_config_from_options(options)
2279 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2280 branch_cl_issue_url = branch_cl.GetIssueURL()
2281 branch_cl_issue = branch_cl.GetIssue()
2282 branch_cl_patchset = branch_cl.GetPatchset()
2283 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2284 upload_args.extend(
2285 ['--depends_on_patchset', '%s:%s' % (
2286 branch_cl_issue, branch_cl_patchset)])
2287 print
2288 print ('The current branch (%s) is tracking a local branch (%s) with '
2289 'an associated CL.') % (cl.GetBranch(), local_branch)
2290 print 'Adding %s/#ps%s as a dependency patchset.' % (
2291 branch_cl_issue_url, branch_cl_patchset)
2292 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002293
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002294 project = settings.GetProject()
2295 if project:
2296 upload_args.extend(['--project', project])
2297
rmistry@google.comef966222015-04-07 11:15:01 +00002298 if options.cq_dry_run:
2299 upload_args.extend(['--cq_dry_run'])
2300
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002301 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002302 upload_args = ['upload'] + upload_args + args
2303 logging.info('upload.RealMain(%s)', upload_args)
2304 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002305 issue = int(issue)
2306 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002307 except KeyboardInterrupt:
2308 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002309 except:
2310 # If we got an exception after the user typed a description for their
2311 # change, back up the description before re-raising.
2312 if change_desc:
2313 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2314 print '\nGot exception while uploading -- saving description to %s\n' \
2315 % backup_path
2316 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002317 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002318 backup_file.close()
2319 raise
2320
2321 if not cl.GetIssue():
2322 cl.SetIssue(issue)
2323 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002324
2325 if options.use_commit_queue:
2326 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002327 return 0
2328
2329
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002330def cleanup_list(l):
2331 """Fixes a list so that comma separated items are put as individual items.
2332
2333 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2334 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2335 """
2336 items = sum((i.split(',') for i in l), [])
2337 stripped_items = (i.strip() for i in items)
2338 return sorted(filter(None, stripped_items))
2339
2340
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002341@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002342def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002343 """Uploads the current changelist to codereview.
2344
2345 Can skip dependency patchset uploads for a branch by running:
2346 git config branch.branch_name.skip-deps-uploads True
2347 To unset run:
2348 git config --unset branch.branch_name.skip-deps-uploads
2349 Can also set the above globally by using the --global flag.
2350 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002351 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2352 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002353 parser.add_option('--bypass-watchlists', action='store_true',
2354 dest='bypass_watchlists',
2355 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002356 parser.add_option('-f', action='store_true', dest='force',
2357 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002358 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00002359 parser.add_option('-t', dest='title',
2360 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002361 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002362 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002363 help='reviewer email addresses')
2364 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002365 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002366 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002367 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002368 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002369 parser.add_option('--emulate_svn_auto_props',
2370 '--emulate-svn-auto-props',
2371 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002372 dest="emulate_svn_auto_props",
2373 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002374 parser.add_option('-c', '--use-commit-queue', action='store_true',
2375 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002376 parser.add_option('--private', action='store_true',
2377 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002378 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002379 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002380 metavar='TARGET',
2381 help='Apply CL to remote ref TARGET. ' +
2382 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002383 parser.add_option('--squash', action='store_true',
2384 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002385 parser.add_option('--no-squash', action='store_true',
2386 help='Don\'t squash multiple commits into one ' +
2387 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002388 parser.add_option('--email', default=None,
2389 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002390 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2391 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002392 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2393 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002394 help='Send the patchset to do a CQ dry run right after '
2395 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002396 parser.add_option('--dependencies', action='store_true',
2397 help='Uploads CLs of all the local branches that depend on '
2398 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002399
rmistry@google.com2dd99862015-06-22 12:22:18 +00002400 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002401 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002402 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002403 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002404 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002405
sbc@chromium.org71437c02015-04-09 19:29:40 +00002406 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002407 return 1
2408
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002409 options.reviewers = cleanup_list(options.reviewers)
2410 options.cc = cleanup_list(options.cc)
2411
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002412 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002413 if args:
2414 # TODO(ukai): is it ok for gerrit case?
2415 base_branch = args[0]
2416 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002417 if cl.GetBranch() is None:
2418 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2419
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002420 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002421 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002422 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002423
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002424 # Make sure authenticated to Rietveld before running expensive hooks. It is
2425 # a fast, best efforts check. Rietveld still can reject the authentication
2426 # during the actual upload.
2427 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2428 authenticator = auth.get_authenticator_for_host(
2429 cl.GetRietveldServer(), auth_config)
2430 if not authenticator.has_cached_credentials():
2431 raise auth.LoginRequiredError(cl.GetRietveldServer())
2432
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002433 # Apply watchlists on upload.
2434 change = cl.GetChange(base_branch, None)
2435 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2436 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002437 if not options.bypass_watchlists:
2438 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002439
ukai@chromium.orge8077812012-02-03 03:41:46 +00002440 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002441 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002442 # Set the reviewer list now so that presubmit checks can access it.
2443 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002444 change_description.update_reviewers(options.reviewers,
2445 options.tbr_owners,
2446 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002447 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002448 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002449 may_prompt=not options.force,
2450 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002451 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002452 if not hook_results.should_continue():
2453 return 1
2454 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002455 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002456
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002457 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002458 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002459 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002460 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002461 print ('The last upload made from this repository was patchset #%d but '
2462 'the most recent patchset on the server is #%d.'
2463 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002464 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2465 'from another machine or branch the patch you\'re uploading now '
2466 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002467 ask_for_data('About to upload; enter to confirm.')
2468
iannucci@chromium.org79540052012-10-19 23:15:26 +00002469 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002470 if settings.GetIsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002471 if options.squash and options.no_squash:
2472 DieWithError('Can only use one of --squash or --no-squash')
2473
2474 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2475 not options.no_squash)
2476
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00002477 ret = GerritUpload(options, args, cl, change)
2478 else:
2479 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002480 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002481 git_set_branch_value('last-upload-hash',
2482 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002483 # Run post upload hooks, if specified.
2484 if settings.GetRunPostUploadHook():
2485 presubmit_support.DoPostUploadExecuter(
2486 change,
2487 cl,
2488 settings.GetRoot(),
2489 options.verbose,
2490 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002491
rmistry@google.com2dd99862015-06-22 12:22:18 +00002492 # Upload all dependencies if specified.
2493 if options.dependencies:
2494 print
2495 print '--dependencies has been specified.'
2496 print 'All dependent local branches will be re-uploaded.'
2497 print
2498 # Remove the dependencies flag from args so that we do not end up in a
2499 # loop.
2500 orig_args.remove('--dependencies')
2501 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002502 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002503
2504
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002505def IsSubmoduleMergeCommit(ref):
2506 # When submodules are added to the repo, we expect there to be a single
2507 # non-git-svn merge commit at remote HEAD with a signature comment.
2508 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002509 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002510 return RunGit(cmd) != ''
2511
2512
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002513def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002514 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002515
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002516 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002517 Updates changelog with metadata (e.g. pointer to review).
2518 Pushes/dcommits the code upstream.
2519 Updates review and closes.
2520 """
2521 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2522 help='bypass upload presubmit hook')
2523 parser.add_option('-m', dest='message',
2524 help="override review description")
2525 parser.add_option('-f', action='store_true', dest='force',
2526 help="force yes to questions (don't prompt)")
2527 parser.add_option('-c', dest='contributor',
2528 help="external contributor for patch (appended to " +
2529 "description and used as author for git). Should be " +
2530 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002531 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002532 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002533 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002534 auth_config = auth.extract_auth_config_from_options(options)
2535
2536 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002537
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002538 current = cl.GetBranch()
2539 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2540 if not settings.GetIsGitSvn() and remote == '.':
2541 print
2542 print 'Attempting to push branch %r into another local branch!' % current
2543 print
2544 print 'Either reparent this branch on top of origin/master:'
2545 print ' git reparent-branch --root'
2546 print
2547 print 'OR run `git rebase-update` if you think the parent branch is already'
2548 print 'committed.'
2549 print
2550 print ' Current parent: %r' % upstream_branch
2551 return 1
2552
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002553 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002554 # Default to merging against our best guess of the upstream branch.
2555 args = [cl.GetUpstreamBranch()]
2556
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002557 if options.contributor:
2558 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2559 print "Please provide contibutor as 'First Last <email@example.com>'"
2560 return 1
2561
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002562 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002563 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002564
sbc@chromium.org71437c02015-04-09 19:29:40 +00002565 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002566 return 1
2567
2568 # This rev-list syntax means "show all commits not in my branch that
2569 # are in base_branch".
2570 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2571 base_branch]).splitlines()
2572 if upstream_commits:
2573 print ('Base branch "%s" has %d commits '
2574 'not in this branch.' % (base_branch, len(upstream_commits)))
2575 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2576 return 1
2577
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002578 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002579 svn_head = None
2580 if cmd == 'dcommit' or base_has_submodules:
2581 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2582 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002583
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002584 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002585 # If the base_head is a submodule merge commit, the first parent of the
2586 # base_head should be a git-svn commit, which is what we're interested in.
2587 base_svn_head = base_branch
2588 if base_has_submodules:
2589 base_svn_head += '^1'
2590
2591 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002592 if extra_commits:
2593 print ('This branch has %d additional commits not upstreamed yet.'
2594 % len(extra_commits.splitlines()))
2595 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2596 'before attempting to %s.' % (base_branch, cmd))
2597 return 1
2598
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002599 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002600 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002601 author = None
2602 if options.contributor:
2603 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002604 hook_results = cl.RunHook(
2605 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002606 may_prompt=not options.force,
2607 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002608 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002609 if not hook_results.should_continue():
2610 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002611
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002612 # Check the tree status if the tree status URL is set.
2613 status = GetTreeStatus()
2614 if 'closed' == status:
2615 print('The tree is closed. Please wait for it to reopen. Use '
2616 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2617 return 1
2618 elif 'unknown' == status:
2619 print('Unable to determine tree status. Please verify manually and '
2620 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2621 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002622
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002623 change_desc = ChangeDescription(options.message)
2624 if not change_desc.description and cl.GetIssue():
2625 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002626
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002627 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002628 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002629 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002630 else:
2631 print 'No description set.'
2632 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2633 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002634
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002635 # Keep a separate copy for the commit message, because the commit message
2636 # contains the link to the Rietveld issue, while the Rietveld message contains
2637 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002638 # Keep a separate copy for the commit message.
2639 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002640 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002641
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002642 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002643 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002644 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002645 # after it. Add a period on a new line to circumvent this. Also add a space
2646 # before the period to make sure that Gitiles continues to correctly resolve
2647 # the URL.
2648 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002649 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002650 commit_desc.append_footer('Patch from %s.' % options.contributor)
2651
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002652 print('Description:')
2653 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002654
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002655 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002656 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002657 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002658
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002659 # We want to squash all this branch's commits into one commit with the proper
2660 # description. We do this by doing a "reset --soft" to the base branch (which
2661 # keeps the working copy the same), then dcommitting that. If origin/master
2662 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2663 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002664 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002665 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2666 # Delete the branches if they exist.
2667 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2668 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2669 result = RunGitWithCode(showref_cmd)
2670 if result[0] == 0:
2671 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002672
2673 # We might be in a directory that's present in this branch but not in the
2674 # trunk. Move up to the top of the tree so that git commands that expect a
2675 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002676 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002677 if rel_base_path:
2678 os.chdir(rel_base_path)
2679
2680 # Stuff our change into the merge branch.
2681 # We wrap in a try...finally block so if anything goes wrong,
2682 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002683 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002684 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002685 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002686 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002687 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002688 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002689 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002690 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002691 RunGit(
2692 [
2693 'commit', '--author', options.contributor,
2694 '-m', commit_desc.description,
2695 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002696 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002697 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002698 if base_has_submodules:
2699 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2700 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2701 RunGit(['checkout', CHERRY_PICK_BRANCH])
2702 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002703 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002704 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002705 pending_prefix = settings.GetPendingRefPrefix()
2706 if not pending_prefix or branch.startswith(pending_prefix):
2707 # If not using refs/pending/heads/* at all, or target ref is already set
2708 # to pending, then push to the target ref directly.
2709 retcode, output = RunGitWithCode(
2710 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002711 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002712 else:
2713 # Cherry-pick the change on top of pending ref and then push it.
2714 assert branch.startswith('refs/'), branch
2715 assert pending_prefix[-1] == '/', pending_prefix
2716 pending_ref = pending_prefix + branch[len('refs/'):]
2717 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002718 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002719 if retcode == 0:
2720 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002721 else:
2722 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002723 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002724 'svn', 'dcommit',
2725 '-C%s' % options.similarity,
2726 '--no-rebase', '--rmdir',
2727 ]
2728 if settings.GetForceHttpsCommitUrl():
2729 # Allow forcing https commit URLs for some projects that don't allow
2730 # committing to http URLs (like Google Code).
2731 remote_url = cl.GetGitSvnRemoteUrl()
2732 if urlparse.urlparse(remote_url).scheme == 'http':
2733 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002734 cmd_args.append('--commit-url=%s' % remote_url)
2735 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002736 if 'Committed r' in output:
2737 revision = re.match(
2738 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2739 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002740 finally:
2741 # And then swap back to the original branch and clean up.
2742 RunGit(['checkout', '-q', cl.GetBranch()])
2743 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002744 if base_has_submodules:
2745 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002746
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002747 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002748 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002749 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002750
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002751 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002752 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002753 try:
2754 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2755 # We set pushed_to_pending to False, since it made it all the way to the
2756 # real ref.
2757 pushed_to_pending = False
2758 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002759 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002760
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002761 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002762 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002763 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002764 if not to_pending:
2765 if viewvc_url and revision:
2766 change_desc.append_footer(
2767 'Committed: %s%s' % (viewvc_url, revision))
2768 elif revision:
2769 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002770 print ('Closing issue '
2771 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002772 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002773 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002774 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002775 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002776 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002777 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002778 if options.bypass_hooks:
2779 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2780 else:
2781 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002782 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002783 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002784
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002785 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002786 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2787 print 'The commit is in the pending queue (%s).' % pending_ref
2788 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002789 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002790 'footer.' % branch)
2791
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002792 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2793 if os.path.isfile(hook):
2794 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002795
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002796 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002797
2798
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002799def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2800 print
2801 print 'Waiting for commit to be landed on %s...' % real_ref
2802 print '(If you are impatient, you may Ctrl-C once without harm)'
2803 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2804 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2805
2806 loop = 0
2807 while True:
2808 sys.stdout.write('fetching (%d)... \r' % loop)
2809 sys.stdout.flush()
2810 loop += 1
2811
2812 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2813 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2814 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2815 for commit in commits.splitlines():
2816 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2817 print 'Found commit on %s' % real_ref
2818 return commit
2819
2820 current_rev = to_rev
2821
2822
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002823def PushToGitPending(remote, pending_ref, upstream_ref):
2824 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2825
2826 Returns:
2827 (retcode of last operation, output log of last operation).
2828 """
2829 assert pending_ref.startswith('refs/'), pending_ref
2830 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2831 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2832 code = 0
2833 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002834 max_attempts = 3
2835 attempts_left = max_attempts
2836 while attempts_left:
2837 if attempts_left != max_attempts:
2838 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2839 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002840
2841 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002842 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002843 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002844 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002845 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002846 print 'Fetch failed with exit code %d.' % code
2847 if out.strip():
2848 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002849 continue
2850
2851 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002852 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002853 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002854 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002855 if code:
2856 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002857 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2858 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002859 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2860 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002861 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002862 return code, out
2863
2864 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002865 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002866 code, out = RunGitWithCode(
2867 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2868 if code == 0:
2869 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002870 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002871 return code, out
2872
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002873 print 'Push failed with exit code %d.' % code
2874 if out.strip():
2875 print out.strip()
2876 if IsFatalPushFailure(out):
2877 print (
2878 'Fatal push error. Make sure your .netrc credentials and git '
2879 'user.email are correct and you have push access to the repo.')
2880 return code, out
2881
2882 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002883 return code, out
2884
2885
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002886def IsFatalPushFailure(push_stdout):
2887 """True if retrying push won't help."""
2888 return '(prohibited by Gerrit)' in push_stdout
2889
2890
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002891@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002892def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002893 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002894 if not settings.GetIsGitSvn():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002895 if get_footer_svn_id():
2896 # If it looks like previous commits were mirrored with git-svn.
2897 message = """This repository appears to be a git-svn mirror, but no
2898upstream SVN master is set. You probably need to run 'git auto-svn' once."""
2899 else:
2900 message = """This doesn't appear to be an SVN repository.
2901If your project has a true, writeable git repository, you probably want to run
2902'git cl land' instead.
2903If your project has a git mirror of an upstream SVN master, you probably need
2904to run 'git svn init'.
2905
2906Using the wrong command might cause your commit to appear to succeed, and the
2907review to be closed, without actually landing upstream. If you choose to
2908proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002909 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002910 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002911 return SendUpstream(parser, args, 'dcommit')
2912
2913
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002914@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002915def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002916 """Commits the current changelist via git."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002917 if settings.GetIsGitSvn() or get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002918 print('This appears to be an SVN repository.')
2919 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002920 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00002921 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002922 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002923
2924
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00002925def ParseIssueNum(arg):
2926 """Parses the issue number from args if present otherwise returns None."""
2927 if re.match(r'\d+', arg):
2928 return arg
2929 if arg.startswith('http'):
2930 return re.sub(r'.*/(\d+)/?', r'\1', arg)
2931 return None
2932
2933
2934@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002935def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002936 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002937 parser.add_option('-b', dest='newbranch',
2938 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002939 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002940 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002941 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2942 help='Change to the directory DIR immediately, '
2943 'before doing anything else.')
2944 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002945 help='failed patches spew .rej files rather than '
2946 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002947 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2948 help="don't commit after patch applies")
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00002949
2950 group = optparse.OptionGroup(parser,
2951 """Options for continuing work on the current issue uploaded
2952from a different clone (e.g. different machine). Must be used independently from
2953the other options. No issue number should be specified, and the branch must have
2954an issue number associated with it""")
2955 group.add_option('--reapply', action='store_true',
2956 dest='reapply',
2957 help="""Reset the branch and reapply the issue.
2958CAUTION: This will undo any local changes in this branch""")
2959
2960 group.add_option('--pull', action='store_true', dest='pull',
2961 help="Performs a pull before reapplying.")
2962 parser.add_option_group(group)
2963
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002964 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002965 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002966 auth_config = auth.extract_auth_config_from_options(options)
2967
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00002968 issue_arg = None
2969 if options.reapply :
2970 if len(args) > 0:
2971 parser.error("--reapply implies no additional arguments.")
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00002972
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00002973 cl = Changelist()
2974 issue_arg = cl.GetIssue()
2975 upstream = cl.GetUpstreamBranch()
2976 if upstream == None:
2977 parser.error("No upstream branch specified. Cannot reset branch")
2978
2979 RunGit(['reset', '--hard', upstream])
2980 if options.pull:
2981 RunGit(['pull'])
2982 else:
2983 if len(args) != 1:
2984 parser.error("Must specify issue number")
2985
2986 issue_arg = ParseIssueNum(args[0])
2987
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00002988 # The patch URL works because ParseIssueNum won't do any substitution
2989 # as the re.sub pattern fails to match and just returns it.
2990 if issue_arg == None:
2991 parser.print_help()
2992 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002993
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002994 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002995 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002996 return 1
2997
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002998 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002999 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003000
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003001 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003002 if options.reapply:
3003 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003004 if options.force:
3005 RunGit(['branch', '-D', options.newbranch],
3006 stderr=subprocess2.PIPE, error_ok=True)
3007 RunGit(['checkout', '-b', options.newbranch,
3008 Changelist().GetUpstreamBranch()])
3009
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003010 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003011 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003012
3013
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003014def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00003015 # PatchIssue should never be called with a dirty tree. It is up to the
3016 # caller to check this, but just in case we assert here since the
3017 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003018 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003019
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003020 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003021 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00003022 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003023 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003024 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00003025 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003026 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00003027 # Assume it's a URL to the patch. Default to https.
3028 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003029 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003030 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003031 DieWithError('Must pass an issue ID or full URL for '
3032 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00003033 issue = int(match.group(2))
3034 cl = Changelist(issue=issue, auth_config=auth_config)
3035 cl.rietveld_server = match.group(1)
3036 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003037 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003038
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003039 # Switch up to the top-level directory, if necessary, in preparation for
3040 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003041 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003042 if top:
3043 os.chdir(top)
3044
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003045 # Git patches have a/ at the beginning of source paths. We strip that out
3046 # with a sed script rather than the -p flag to patch so we can feed either
3047 # Git or svn-style patches into the same apply command.
3048 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003049 try:
3050 patch_data = subprocess2.check_output(
3051 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3052 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003053 DieWithError('Git patch mungling failed.')
3054 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003055
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003056 # We use "git apply" to apply the patch instead of "patch" so that we can
3057 # pick up file adds.
3058 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003059 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003060 if directory:
3061 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003062 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003063 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003064 elif IsGitVersionAtLeast('1.7.12'):
3065 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003066 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003067 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003068 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003069 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003070 print 'Failed to apply the patch'
3071 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003072
3073 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003074 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003075 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3076 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003077 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3078 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003079 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003080 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003081 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003082 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003083 else:
3084 print "Patch applied to index."
3085 return 0
3086
3087
3088def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003089 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003090 # Provide a wrapper for git svn rebase to help avoid accidental
3091 # git svn dcommit.
3092 # It's the only command that doesn't use parser at all since we just defer
3093 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003094
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003095 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003096
3097
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003098def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003099 """Fetches the tree status and returns either 'open', 'closed',
3100 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003101 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003102 if url:
3103 status = urllib2.urlopen(url).read().lower()
3104 if status.find('closed') != -1 or status == '0':
3105 return 'closed'
3106 elif status.find('open') != -1 or status == '1':
3107 return 'open'
3108 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003109 return 'unset'
3110
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003111
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003112def GetTreeStatusReason():
3113 """Fetches the tree status from a json url and returns the message
3114 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003115 url = settings.GetTreeStatusUrl()
3116 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003117 connection = urllib2.urlopen(json_url)
3118 status = json.loads(connection.read())
3119 connection.close()
3120 return status['message']
3121
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003122
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003123def GetBuilderMaster(bot_list):
3124 """For a given builder, fetch the master from AE if available."""
3125 map_url = 'https://builders-map.appspot.com/'
3126 try:
3127 master_map = json.load(urllib2.urlopen(map_url))
3128 except urllib2.URLError as e:
3129 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3130 (map_url, e))
3131 except ValueError as e:
3132 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3133 if not master_map:
3134 return None, 'Failed to build master map.'
3135
3136 result_master = ''
3137 for bot in bot_list:
3138 builder = bot.split(':', 1)[0]
3139 master_list = master_map.get(builder, [])
3140 if not master_list:
3141 return None, ('No matching master for builder %s.' % builder)
3142 elif len(master_list) > 1:
3143 return None, ('The builder name %s exists in multiple masters %s.' %
3144 (builder, master_list))
3145 else:
3146 cur_master = master_list[0]
3147 if not result_master:
3148 result_master = cur_master
3149 elif result_master != cur_master:
3150 return None, 'The builders do not belong to the same master.'
3151 return result_master, None
3152
3153
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003154def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003155 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003156 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003157 status = GetTreeStatus()
3158 if 'unset' == status:
3159 print 'You must configure your tree status URL by running "git cl config".'
3160 return 2
3161
3162 print "The tree is %s" % status
3163 print
3164 print GetTreeStatusReason()
3165 if status != 'open':
3166 return 1
3167 return 0
3168
3169
maruel@chromium.org15192402012-09-06 12:38:29 +00003170def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003171 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003172 group = optparse.OptionGroup(parser, "Try job options")
3173 group.add_option(
3174 "-b", "--bot", action="append",
3175 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3176 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003177 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003178 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003179 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003180 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003181 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003182 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003183 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003184 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003185 "-r", "--revision",
3186 help="Revision to use for the try job; default: the "
3187 "revision will be determined by the try server; see "
3188 "its waterfall for more info")
3189 group.add_option(
3190 "-c", "--clobber", action="store_true", default=False,
3191 help="Force a clobber before building; e.g. don't do an "
3192 "incremental build")
3193 group.add_option(
3194 "--project",
3195 help="Override which project to use. Projects are defined "
3196 "server-side to define what default bot set to use")
3197 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003198 "-p", "--property", dest="properties", action="append", default=[],
3199 help="Specify generic properties in the form -p key1=value1 -p "
3200 "key2=value2 etc (buildbucket only). The value will be treated as "
3201 "json if decodable, or as string otherwise.")
3202 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003203 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003204 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003205 "--use-rietveld", action="store_true", default=False,
3206 help="Use Rietveld to trigger try jobs.")
3207 group.add_option(
3208 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3209 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003210 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003211 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003212 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003213 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003214
machenbach@chromium.org45453142015-09-15 08:45:22 +00003215 if options.use_rietveld and options.properties:
3216 parser.error('Properties can only be specified with buildbucket')
3217
3218 # Make sure that all properties are prop=value pairs.
3219 bad_params = [x for x in options.properties if '=' not in x]
3220 if bad_params:
3221 parser.error('Got properties with missing "=": %s' % bad_params)
3222
maruel@chromium.org15192402012-09-06 12:38:29 +00003223 if args:
3224 parser.error('Unknown arguments: %s' % args)
3225
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003226 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003227 if not cl.GetIssue():
3228 parser.error('Need to upload first')
3229
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003230 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003231 if props.get('closed'):
3232 parser.error('Cannot send tryjobs for a closed CL')
3233
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003234 if props.get('private'):
3235 parser.error('Cannot use trybots with private issue')
3236
maruel@chromium.org15192402012-09-06 12:38:29 +00003237 if not options.name:
3238 options.name = cl.GetBranch()
3239
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003240 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003241 options.master, err_msg = GetBuilderMaster(options.bot)
3242 if err_msg:
3243 parser.error('Tryserver master cannot be found because: %s\n'
3244 'Please manually specify the tryserver master'
3245 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003246
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003247 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003248 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003249 if not options.bot:
3250 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003251
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003252 # Get try masters from PRESUBMIT.py files.
3253 masters = presubmit_support.DoGetTryMasters(
3254 change,
3255 change.LocalPaths(),
3256 settings.GetRoot(),
3257 None,
3258 None,
3259 options.verbose,
3260 sys.stdout)
3261 if masters:
3262 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003263
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003264 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3265 options.bot = presubmit_support.DoGetTrySlaves(
3266 change,
3267 change.LocalPaths(),
3268 settings.GetRoot(),
3269 None,
3270 None,
3271 options.verbose,
3272 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003273
3274 if not options.bot:
3275 # Get try masters from cq.cfg if any.
3276 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3277 # location.
3278 cq_cfg = os.path.join(change.RepositoryRoot(),
3279 'infra', 'config', 'cq.cfg')
3280 if os.path.exists(cq_cfg):
3281 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00003282 cq_masters = commit_queue.get_master_builder_map(
3283 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003284 for master, builders in cq_masters.iteritems():
3285 for builder in builders:
3286 # Skip presubmit builders, because these will fail without LGTM.
3287 if 'presubmit' not in builder.lower():
3288 masters.setdefault(master, {})[builder] = ['defaulttests']
3289 if masters:
3290 return masters
3291
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003292 if not options.bot:
3293 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003294
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003295 builders_and_tests = {}
3296 # TODO(machenbach): The old style command-line options don't support
3297 # multiple try masters yet.
3298 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3299 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3300
3301 for bot in old_style:
3302 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003303 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003304 elif ',' in bot:
3305 parser.error('Specify one bot per --bot flag')
3306 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003307 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003308
3309 for bot, tests in new_style:
3310 builders_and_tests.setdefault(bot, []).extend(tests)
3311
3312 # Return a master map with one master to be backwards compatible. The
3313 # master name defaults to an empty string, which will cause the master
3314 # not to be set on rietveld (deprecated).
3315 return {options.master: builders_and_tests}
3316
3317 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003318
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003319 for builders in masters.itervalues():
3320 if any('triggered' in b for b in builders):
3321 print >> sys.stderr, (
3322 'ERROR You are trying to send a job to a triggered bot. This type of'
3323 ' bot requires an\ninitial job from a parent (usually a builder). '
3324 'Instead send your job to the parent.\n'
3325 'Bot list: %s' % builders)
3326 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003327
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003328 patchset = cl.GetMostRecentPatchset()
3329 if patchset and patchset != cl.GetPatchset():
3330 print(
3331 '\nWARNING Mismatch between local config and server. Did a previous '
3332 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3333 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003334 if options.luci:
3335 trigger_luci_job(cl, masters, options)
3336 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003337 try:
3338 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3339 except BuildbucketResponseException as ex:
3340 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003341 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003342 except Exception as e:
3343 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3344 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3345 e, stacktrace)
3346 return 1
3347 else:
3348 try:
3349 cl.RpcServer().trigger_distributed_try_jobs(
3350 cl.GetIssue(), patchset, options.name, options.clobber,
3351 options.revision, masters)
3352 except urllib2.HTTPError as e:
3353 if e.code == 404:
3354 print('404 from rietveld; '
3355 'did you mean to use "git try" instead of "git cl try"?')
3356 return 1
3357 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003358
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003359 for (master, builders) in sorted(masters.iteritems()):
3360 if master:
3361 print 'Master: %s' % master
3362 length = max(len(builder) for builder in builders)
3363 for builder in sorted(builders):
3364 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003365 return 0
3366
3367
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003368@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003369def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003370 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003371 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003372 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003373 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003374
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003375 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003376 if args:
3377 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003378 branch = cl.GetBranch()
3379 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003380 cl = Changelist()
3381 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003382
3383 # Clear configured merge-base, if there is one.
3384 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003385 else:
3386 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003387 return 0
3388
3389
thestig@chromium.org00858c82013-12-02 23:08:03 +00003390def CMDweb(parser, args):
3391 """Opens the current CL in the web browser."""
3392 _, args = parser.parse_args(args)
3393 if args:
3394 parser.error('Unrecognized args: %s' % ' '.join(args))
3395
3396 issue_url = Changelist().GetIssueURL()
3397 if not issue_url:
3398 print >> sys.stderr, 'ERROR No issue to open'
3399 return 1
3400
3401 webbrowser.open(issue_url)
3402 return 0
3403
3404
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003405def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003406 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003407 auth.add_auth_options(parser)
3408 options, args = parser.parse_args(args)
3409 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003410 if args:
3411 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003412 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003413 props = cl.GetIssueProperties()
3414 if props.get('private'):
3415 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003416 cl.SetFlag('commit', '1')
3417 return 0
3418
3419
groby@chromium.org411034a2013-02-26 15:12:01 +00003420def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003421 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003422 auth.add_auth_options(parser)
3423 options, args = parser.parse_args(args)
3424 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003425 if args:
3426 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003427 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003428 # Ensure there actually is an issue to close.
3429 cl.GetDescription()
3430 cl.CloseIssue()
3431 return 0
3432
3433
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003434def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003435 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003436 auth.add_auth_options(parser)
3437 options, args = parser.parse_args(args)
3438 auth_config = auth.extract_auth_config_from_options(options)
3439 if args:
3440 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003441
3442 # Uncommitted (staged and unstaged) changes will be destroyed by
3443 # "git reset --hard" if there are merging conflicts in PatchIssue().
3444 # Staged changes would be committed along with the patch from last
3445 # upload, hence counted toward the "last upload" side in the final
3446 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003447 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003448 return 1
3449
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003450 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003451 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003452 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003453 if not issue:
3454 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003455 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003456 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003457
3458 # Create a new branch based on the merge-base
3459 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3460 try:
3461 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003462 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003463 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003464 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003465 return rtn
3466
wychen@chromium.org06928532015-02-03 02:11:29 +00003467 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003468 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003469 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003470 finally:
3471 RunGit(['checkout', '-q', branch])
3472 RunGit(['branch', '-D', TMP_BRANCH])
3473
3474 return 0
3475
3476
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003477def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003478 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003479 parser.add_option(
3480 '--no-color',
3481 action='store_true',
3482 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003483 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003484 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003485 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003486
3487 author = RunGit(['config', 'user.email']).strip() or None
3488
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003489 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003490
3491 if args:
3492 if len(args) > 1:
3493 parser.error('Unknown args')
3494 base_branch = args[0]
3495 else:
3496 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003497 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003498
3499 change = cl.GetChange(base_branch, None)
3500 return owners_finder.OwnersFinder(
3501 [f.LocalPath() for f in
3502 cl.GetChange(base_branch, None).AffectedFiles()],
3503 change.RepositoryRoot(), author,
3504 fopen=file, os_path=os.path, glob=glob.glob,
3505 disable_color=options.no_color).run()
3506
3507
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003508def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003509 """Generates a diff command."""
3510 # Generate diff for the current branch's changes.
3511 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3512 upstream_commit, '--' ]
3513
3514 if args:
3515 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003516 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003517 diff_cmd.append(arg)
3518 else:
3519 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003520
3521 return diff_cmd
3522
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003523def MatchingFileType(file_name, extensions):
3524 """Returns true if the file name ends with one of the given extensions."""
3525 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003526
enne@chromium.org555cfe42014-01-29 18:21:39 +00003527@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003528def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003529 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003530 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003531 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003532 parser.add_option('--full', action='store_true',
3533 help='Reformat the full content of all touched files')
3534 parser.add_option('--dry-run', action='store_true',
3535 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003536 parser.add_option('--python', action='store_true',
3537 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003538 parser.add_option('--diff', action='store_true',
3539 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003540 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003541
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003542 # git diff generates paths against the root of the repository. Change
3543 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003544 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003545 if rel_base_path:
3546 os.chdir(rel_base_path)
3547
digit@chromium.org29e47272013-05-17 17:01:46 +00003548 # Grab the merge-base commit, i.e. the upstream commit of the current
3549 # branch when it was created or the last time it was rebased. This is
3550 # to cover the case where the user may have called "git fetch origin",
3551 # moving the origin branch to a newer commit, but hasn't rebased yet.
3552 upstream_commit = None
3553 cl = Changelist()
3554 upstream_branch = cl.GetUpstreamBranch()
3555 if upstream_branch:
3556 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3557 upstream_commit = upstream_commit.strip()
3558
3559 if not upstream_commit:
3560 DieWithError('Could not find base commit for this branch. '
3561 'Are you in detached state?')
3562
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003563 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
3564 diff_output = RunGit(changed_files_cmd)
3565 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00003566 # Filter out files deleted by this CL
3567 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003568
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003569 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
3570 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
3571 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003572 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00003573
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003574 top_dir = os.path.normpath(
3575 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3576
3577 # Locate the clang-format binary in the checkout
3578 try:
3579 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3580 except clang_format.NotFoundError, e:
3581 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003582
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003583 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3584 # formatted. This is used to block during the presubmit.
3585 return_value = 0
3586
digit@chromium.org29e47272013-05-17 17:01:46 +00003587 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003588 if clang_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003589 cmd = [clang_format_tool]
3590 if not opts.dry_run and not opts.diff:
3591 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003592 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003593 if opts.diff:
3594 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003595 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003596 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003597 env['PATH'] = str(os.path.dirname(clang_format_tool))
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003598 try:
3599 script = clang_format.FindClangFormatScriptInChromiumTree(
3600 'clang-format-diff.py')
3601 except clang_format.NotFoundError, e:
3602 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003603
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003604 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003605 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003606 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003607
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003608 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
3609 diff_output = RunGit(diff_cmd)
3610
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003611 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003612 if opts.diff:
3613 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003614 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003615 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003616
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003617 # Similar code to above, but using yapf on .py files rather than clang-format
3618 # on C/C++ files
3619 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003620 yapf_tool = gclient_utils.FindExecutable('yapf')
3621 if yapf_tool is None:
3622 DieWithError('yapf not found in PATH')
3623
3624 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003625 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003626 cmd = [yapf_tool]
3627 if not opts.dry_run and not opts.diff:
3628 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003629 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003630 if opts.diff:
3631 sys.stdout.write(stdout)
3632 else:
3633 # TODO(sbc): yapf --lines mode still has some issues.
3634 # https://github.com/google/yapf/issues/154
3635 DieWithError('--python currently only works with --full')
3636
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003637 # Dart's formatter does not have the nice property of only operating on
3638 # modified chunks, so hard code full.
3639 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003640 try:
3641 command = [dart_format.FindDartFmtToolInChromiumTree()]
3642 if not opts.dry_run and not opts.diff:
3643 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003644 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003645
3646 stdout = RunCommand(command, cwd=top_dir, env=env)
3647 if opts.dry_run and stdout:
3648 return_value = 2
3649 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00003650 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
3651 'found in this checkout. Files in other languages are still ' +
3652 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003653
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003654 # Format GN build files. Always run on full build files for canonical form.
3655 if gn_diff_files:
3656 cmd = ['gn', 'format']
3657 if not opts.dry_run and not opts.diff:
3658 cmd.append('--in-place')
3659 for gn_diff_file in gn_diff_files:
3660 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
3661 if opts.diff:
3662 sys.stdout.write(stdout)
3663
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003664 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003665
3666
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003667@subcommand.usage('<codereview url or issue id>')
3668def CMDcheckout(parser, args):
3669 """Checks out a branch associated with a given Rietveld issue."""
3670 _, args = parser.parse_args(args)
3671
3672 if len(args) != 1:
3673 parser.print_help()
3674 return 1
3675
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003676 target_issue = ParseIssueNum(args[0])
3677 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003678 parser.print_help()
3679 return 1
3680
3681 key_and_issues = [x.split() for x in RunGit(
3682 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
3683 .splitlines()]
3684 branches = []
3685 for key, issue in key_and_issues:
3686 if issue == target_issue:
3687 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
3688
3689 if len(branches) == 0:
3690 print 'No branch found for issue %s.' % target_issue
3691 return 1
3692 if len(branches) == 1:
3693 RunGit(['checkout', branches[0]])
3694 else:
3695 print 'Multiple branches match issue %s:' % target_issue
3696 for i in range(len(branches)):
3697 print '%d: %s' % (i, branches[i])
3698 which = raw_input('Choose by index: ')
3699 try:
3700 RunGit(['checkout', branches[int(which)]])
3701 except (IndexError, ValueError):
3702 print 'Invalid selection, not checking out any branch.'
3703 return 1
3704
3705 return 0
3706
3707
maruel@chromium.org29404b52014-09-08 22:58:00 +00003708def CMDlol(parser, args):
3709 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003710 print zlib.decompress(base64.b64decode(
3711 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3712 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3713 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3714 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003715 return 0
3716
3717
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003718class OptionParser(optparse.OptionParser):
3719 """Creates the option parse and add --verbose support."""
3720 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003721 optparse.OptionParser.__init__(
3722 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003723 self.add_option(
3724 '-v', '--verbose', action='count', default=0,
3725 help='Use 2 times for more debugging info')
3726
3727 def parse_args(self, args=None, values=None):
3728 options, args = optparse.OptionParser.parse_args(self, args, values)
3729 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3730 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3731 return options, args
3732
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003733
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003734def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003735 if sys.hexversion < 0x02060000:
3736 print >> sys.stderr, (
3737 '\nYour python version %s is unsupported, please upgrade.\n' %
3738 sys.version.split(' ', 1)[0])
3739 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003740
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003741 # Reload settings.
3742 global settings
3743 settings = Settings()
3744
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003745 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003746 dispatcher = subcommand.CommandDispatcher(__name__)
3747 try:
3748 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003749 except auth.AuthenticationError as e:
3750 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003751 except urllib2.HTTPError, e:
3752 if e.code != 500:
3753 raise
3754 DieWithError(
3755 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3756 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003757 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003758
3759
3760if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003761 # These affect sys.stdout so do it outside of main() to simplify mocks in
3762 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003763 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003764 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003765 try:
3766 sys.exit(main(sys.argv[1:]))
3767 except KeyboardInterrupt:
3768 sys.stderr.write('interrupted\n')
3769 sys.exit(1)