blob: 49bed386baa821cfedf4370030bc9cb1b6afb452 [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
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000044import clang_format
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
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000436 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000437 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000438 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000439 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000440
441 def LazyUpdateIfNeeded(self):
442 """Updates the settings from a codereview.settings file, if available."""
443 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000444 # The only value that actually changes the behavior is
445 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000446 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000447 error_ok=True
448 ).strip().lower()
449
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000450 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000451 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000452 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000453 # set updated to True to avoid infinite calling loop
454 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000455 self.updated = True
456 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000457 self.updated = True
458
459 def GetDefaultServerUrl(self, error_ok=False):
460 if not self.default_server:
461 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000462 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000463 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000464 if error_ok:
465 return self.default_server
466 if not self.default_server:
467 error_message = ('Could not find settings file. You must configure '
468 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000469 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000470 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000471 return self.default_server
472
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000473 @staticmethod
474 def GetRelativeRoot():
475 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000476
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000477 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000478 if self.root is None:
479 self.root = os.path.abspath(self.GetRelativeRoot())
480 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000481
482 def GetIsGitSvn(self):
483 """Return true if this repo looks like it's using git-svn."""
484 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000485 if self.GetPendingRefPrefix():
486 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
487 self.is_git_svn = False
488 else:
489 # If you have any "svn-remote.*" config keys, we think you're using svn.
490 self.is_git_svn = RunGitWithCode(
491 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000492 return self.is_git_svn
493
494 def GetSVNBranch(self):
495 if self.svn_branch is None:
496 if not self.GetIsGitSvn():
497 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
498
499 # Try to figure out which remote branch we're based on.
500 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000501 # 1) iterate through our branch history and find the svn URL.
502 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000503
504 # regexp matching the git-svn line that contains the URL.
505 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
506
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000507 # We don't want to go through all of history, so read a line from the
508 # pipe at a time.
509 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000510 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000511 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
512 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000513 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000514 for line in proc.stdout:
515 match = git_svn_re.match(line)
516 if match:
517 url = match.group(1)
518 proc.stdout.close() # Cut pipe.
519 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000520
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000521 if url:
522 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
523 remotes = RunGit(['config', '--get-regexp',
524 r'^svn-remote\..*\.url']).splitlines()
525 for remote in remotes:
526 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000527 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000528 remote = match.group(1)
529 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000530 rewrite_root = RunGit(
531 ['config', 'svn-remote.%s.rewriteRoot' % remote],
532 error_ok=True).strip()
533 if rewrite_root:
534 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000535 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000536 ['config', 'svn-remote.%s.fetch' % remote],
537 error_ok=True).strip()
538 if fetch_spec:
539 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
540 if self.svn_branch:
541 break
542 branch_spec = RunGit(
543 ['config', 'svn-remote.%s.branches' % remote],
544 error_ok=True).strip()
545 if branch_spec:
546 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
547 if self.svn_branch:
548 break
549 tag_spec = RunGit(
550 ['config', 'svn-remote.%s.tags' % remote],
551 error_ok=True).strip()
552 if tag_spec:
553 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
554 if self.svn_branch:
555 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000556
557 if not self.svn_branch:
558 DieWithError('Can\'t guess svn branch -- try specifying it on the '
559 'command line')
560
561 return self.svn_branch
562
563 def GetTreeStatusUrl(self, error_ok=False):
564 if not self.tree_status_url:
565 error_message = ('You must configure your tree status URL by running '
566 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000567 self.tree_status_url = self._GetRietveldConfig(
568 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000569 return self.tree_status_url
570
571 def GetViewVCUrl(self):
572 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000573 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000574 return self.viewvc_url
575
rmistry@google.com90752582014-01-14 21:04:50 +0000576 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000577 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000578
rmistry@google.com78948ed2015-07-08 23:09:57 +0000579 def GetIsSkipDependencyUpload(self, branch_name):
580 """Returns true if specified branch should skip dep uploads."""
581 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
582 error_ok=True)
583
rmistry@google.com5626a922015-02-26 14:03:30 +0000584 def GetRunPostUploadHook(self):
585 run_post_upload_hook = self._GetRietveldConfig(
586 'run-post-upload-hook', error_ok=True)
587 return run_post_upload_hook == "True"
588
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000589 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000590 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000591
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000592 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000593 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000594
ukai@chromium.orge8077812012-02-03 03:41:46 +0000595 def GetIsGerrit(self):
596 """Return true if this repo is assosiated with gerrit code review system."""
597 if self.is_gerrit is None:
598 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
599 return self.is_gerrit
600
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000601 def GetGitEditor(self):
602 """Return the editor specified in the git config, or None if none is."""
603 if self.git_editor is None:
604 self.git_editor = self._GetConfig('core.editor', error_ok=True)
605 return self.git_editor or None
606
thestig@chromium.org44202a22014-03-11 19:22:18 +0000607 def GetLintRegex(self):
608 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
609 DEFAULT_LINT_REGEX)
610
611 def GetLintIgnoreRegex(self):
612 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
613 DEFAULT_LINT_IGNORE_REGEX)
614
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000615 def GetProject(self):
616 if not self.project:
617 self.project = self._GetRietveldConfig('project', error_ok=True)
618 return self.project
619
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000620 def GetForceHttpsCommitUrl(self):
621 if not self.force_https_commit_url:
622 self.force_https_commit_url = self._GetRietveldConfig(
623 'force-https-commit-url', error_ok=True)
624 return self.force_https_commit_url
625
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000626 def GetPendingRefPrefix(self):
627 if not self.pending_ref_prefix:
628 self.pending_ref_prefix = self._GetRietveldConfig(
629 'pending-ref-prefix', error_ok=True)
630 return self.pending_ref_prefix
631
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000632 def _GetRietveldConfig(self, param, **kwargs):
633 return self._GetConfig('rietveld.' + param, **kwargs)
634
rmistry@google.com78948ed2015-07-08 23:09:57 +0000635 def _GetBranchConfig(self, branch_name, param, **kwargs):
636 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
637
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000638 def _GetConfig(self, param, **kwargs):
639 self.LazyUpdateIfNeeded()
640 return RunGit(['config', param], **kwargs).strip()
641
642
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000643def ShortBranchName(branch):
644 """Convert a name like 'refs/heads/foo' to just 'foo'."""
645 return branch.replace('refs/heads/', '')
646
647
648class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000649 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000650 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000651 global settings
652 if not settings:
653 # Happens when git_cl.py is used as a utility library.
654 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000655 settings.GetDefaultServerUrl()
656 self.branchref = branchref
657 if self.branchref:
658 self.branch = ShortBranchName(self.branchref)
659 else:
660 self.branch = None
661 self.rietveld_server = None
662 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000663 self.lookedup_issue = False
664 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000665 self.has_description = False
666 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000667 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000668 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000669 self.cc = None
670 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000671 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000672 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000673 self._remote = None
674 self._rpc_server = None
675
676 @property
677 def auth_config(self):
678 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000679
680 def GetCCList(self):
681 """Return the users cc'd on this CL.
682
683 Return is a string suitable for passing to gcl with the --cc flag.
684 """
685 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000686 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000687 more_cc = ','.join(self.watchers)
688 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
689 return self.cc
690
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000691 def GetCCListWithoutDefault(self):
692 """Return the users cc'd on this CL excluding default ones."""
693 if self.cc is None:
694 self.cc = ','.join(self.watchers)
695 return self.cc
696
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000697 def SetWatchers(self, watchers):
698 """Set the list of email addresses that should be cc'd based on the changed
699 files in this CL.
700 """
701 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000702
703 def GetBranch(self):
704 """Returns the short branch name, e.g. 'master'."""
705 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000706 branchref = RunGit(['symbolic-ref', 'HEAD'],
707 stderr=subprocess2.VOID, error_ok=True).strip()
708 if not branchref:
709 return None
710 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000711 self.branch = ShortBranchName(self.branchref)
712 return self.branch
713
714 def GetBranchRef(self):
715 """Returns the full branch name, e.g. 'refs/heads/master'."""
716 self.GetBranch() # Poke the lazy loader.
717 return self.branchref
718
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000719 @staticmethod
720 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000721 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000722 e.g. 'origin', 'refs/heads/master'
723 """
724 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
726 error_ok=True).strip()
727 if upstream_branch:
728 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
729 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000730 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
731 error_ok=True).strip()
732 if upstream_branch:
733 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000735 # Fall back on trying a git-svn upstream branch.
736 if settings.GetIsGitSvn():
737 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000738 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000739 # Else, try to guess the origin remote.
740 remote_branches = RunGit(['branch', '-r']).split()
741 if 'origin/master' in remote_branches:
742 # Fall back on origin/master if it exits.
743 remote = 'origin'
744 upstream_branch = 'refs/heads/master'
745 elif 'origin/trunk' in remote_branches:
746 # Fall back on origin/trunk if it exists. Generally a shared
747 # git-svn clone
748 remote = 'origin'
749 upstream_branch = 'refs/heads/trunk'
750 else:
751 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000752Either pass complete "git diff"-style arguments, like
753 git cl upload origin/master
754or verify this branch is set up to track another (via the --track argument to
755"git checkout -b ...").""")
756
757 return remote, upstream_branch
758
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000759 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000760 upstream_branch = self.GetUpstreamBranch()
761 if not BranchExists(upstream_branch):
762 DieWithError('The upstream for the current branch (%s) does not exist '
763 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000764 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000765 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000766
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000767 def GetUpstreamBranch(self):
768 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000769 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000770 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000771 upstream_branch = upstream_branch.replace('refs/heads/',
772 'refs/remotes/%s/' % remote)
773 upstream_branch = upstream_branch.replace('refs/branch-heads/',
774 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000775 self.upstream_branch = upstream_branch
776 return self.upstream_branch
777
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000778 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000779 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000780 remote, branch = None, self.GetBranch()
781 seen_branches = set()
782 while branch not in seen_branches:
783 seen_branches.add(branch)
784 remote, branch = self.FetchUpstreamTuple(branch)
785 branch = ShortBranchName(branch)
786 if remote != '.' or branch.startswith('refs/remotes'):
787 break
788 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000789 remotes = RunGit(['remote'], error_ok=True).split()
790 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000791 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000792 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000793 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000794 logging.warning('Could not determine which remote this change is '
795 'associated with, so defaulting to "%s". This may '
796 'not be what you want. You may prevent this message '
797 'by running "git svn info" as documented here: %s',
798 self._remote,
799 GIT_INSTRUCTIONS_URL)
800 else:
801 logging.warn('Could not determine which remote this change is '
802 'associated with. You may prevent this message by '
803 'running "git svn info" as documented here: %s',
804 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000805 branch = 'HEAD'
806 if branch.startswith('refs/remotes'):
807 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000808 elif branch.startswith('refs/branch-heads/'):
809 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000810 else:
811 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000812 return self._remote
813
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000814 def GitSanityChecks(self, upstream_git_obj):
815 """Checks git repo status and ensures diff is from local commits."""
816
sbc@chromium.org79706062015-01-14 21:18:12 +0000817 if upstream_git_obj is None:
818 if self.GetBranch() is None:
819 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +0000820 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +0000821 else:
822 print >> sys.stderr, (
823 'ERROR: no upstream branch')
824 return False
825
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000826 # Verify the commit we're diffing against is in our current branch.
827 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
828 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
829 if upstream_sha != common_ancestor:
830 print >> sys.stderr, (
831 'ERROR: %s is not in the current branch. You may need to rebase '
832 'your tracking branch' % upstream_sha)
833 return False
834
835 # List the commits inside the diff, and verify they are all local.
836 commits_in_diff = RunGit(
837 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
838 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
839 remote_branch = remote_branch.strip()
840 if code != 0:
841 _, remote_branch = self.GetRemoteBranch()
842
843 commits_in_remote = RunGit(
844 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
845
846 common_commits = set(commits_in_diff) & set(commits_in_remote)
847 if common_commits:
848 print >> sys.stderr, (
849 'ERROR: Your diff contains %d commits already in %s.\n'
850 'Run "git log --oneline %s..HEAD" to get a list of commits in '
851 'the diff. If you are using a custom git flow, you can override'
852 ' the reference used for this check with "git config '
853 'gitcl.remotebranch <git-ref>".' % (
854 len(common_commits), remote_branch, upstream_git_obj))
855 return False
856 return True
857
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000858 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000859 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000860
861 Returns None if it is not set.
862 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000863 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
864 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000865
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000866 def GetGitSvnRemoteUrl(self):
867 """Return the configured git-svn remote URL parsed from git svn info.
868
869 Returns None if it is not set.
870 """
871 # URL is dependent on the current directory.
872 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
873 if data:
874 keys = dict(line.split(': ', 1) for line in data.splitlines()
875 if ': ' in line)
876 return keys.get('URL', None)
877 return None
878
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000879 def GetRemoteUrl(self):
880 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
881
882 Returns None if there is no remote.
883 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000884 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000885 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
886
887 # If URL is pointing to a local directory, it is probably a git cache.
888 if os.path.isdir(url):
889 url = RunGit(['config', 'remote.%s.url' % remote],
890 error_ok=True,
891 cwd=url).strip()
892 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000893
894 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000895 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000896 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000897 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000898 self.issue = int(issue) or None if issue else None
899 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000900 return self.issue
901
902 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000903 if not self.rietveld_server:
904 # If we're on a branch then get the server potentially associated
905 # with that branch.
906 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000907 rietveld_server_config = self._RietveldServer()
908 if rietveld_server_config:
909 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
910 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000911 if not self.rietveld_server:
912 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000913 return self.rietveld_server
914
915 def GetIssueURL(self):
916 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000917 if not self.GetIssue():
918 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000919 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
920
921 def GetDescription(self, pretty=False):
922 if not self.has_description:
923 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000924 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000925 try:
926 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000927 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000928 if e.code == 404:
929 DieWithError(
930 ('\nWhile fetching the description for issue %d, received a '
931 '404 (not found)\n'
932 'error. It is likely that you deleted this '
933 'issue on the server. If this is the\n'
934 'case, please run\n\n'
935 ' git cl issue 0\n\n'
936 'to clear the association with the deleted issue. Then run '
937 'this command again.') % issue)
938 else:
939 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000940 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000941 except urllib2.URLError as e:
942 print >> sys.stderr, (
943 'Warning: Failed to retrieve CL description due to network '
944 'failure.')
945 self.description = ''
946
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000947 self.has_description = True
948 if pretty:
949 wrapper = textwrap.TextWrapper()
950 wrapper.initial_indent = wrapper.subsequent_indent = ' '
951 return wrapper.fill(self.description)
952 return self.description
953
954 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000955 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000956 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957 patchset = RunGit(['config', self._PatchsetSetting()],
958 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000959 self.patchset = int(patchset) or None if patchset else None
960 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000961 return self.patchset
962
963 def SetPatchset(self, patchset):
964 """Set this branch's patchset. If patchset=0, clears the patchset."""
965 if patchset:
966 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000967 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000968 else:
969 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000970 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000971 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000972
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000973 def GetMostRecentPatchset(self):
974 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000975
976 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000977 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000978 '/download/issue%s_%s.diff' % (issue, patchset))
979
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000980 def GetIssueProperties(self):
981 if self._props is None:
982 issue = self.GetIssue()
983 if not issue:
984 self._props = {}
985 else:
986 self._props = self.RpcServer().get_issue_properties(issue, True)
987 return self._props
988
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000989 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000990 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000991
apavlov@chromium.orge4efd512014-11-05 09:05:29 +0000992 def AddComment(self, message):
993 return self.RpcServer().add_comment(self.GetIssue(), message)
994
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000995 def SetIssue(self, issue):
996 """Set this branch's issue. If issue=0, clears the issue."""
997 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000998 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000999 RunGit(['config', self._IssueSetting(), str(issue)])
1000 if self.rietveld_server:
1001 RunGit(['config', self._RietveldServer(), self.rietveld_server])
1002 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001003 current_issue = self.GetIssue()
1004 if current_issue:
1005 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001006 self.issue = None
1007 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001009 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001010 if not self.GitSanityChecks(upstream_branch):
1011 DieWithError('\nGit sanity check failure')
1012
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001013 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001014 if not root:
1015 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001016 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001017
1018 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001019 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001020 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001021 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001022 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001023 except subprocess2.CalledProcessError:
1024 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001025 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001026 'This branch probably doesn\'t exist anymore. To reset the\n'
1027 'tracking branch, please run\n'
1028 ' git branch --set-upstream %s trunk\n'
1029 'replacing trunk with origin/master or the relevant branch') %
1030 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001031
maruel@chromium.org52424302012-08-29 15:14:30 +00001032 issue = self.GetIssue()
1033 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001034 if issue:
1035 description = self.GetDescription()
1036 else:
1037 # If the change was never uploaded, use the log messages of all commits
1038 # up to the branch point, as git cl upload will prefill the description
1039 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001040 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1041 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001042
1043 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001044 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001045 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001046 name,
1047 description,
1048 absroot,
1049 files,
1050 issue,
1051 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001052 author,
1053 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001054
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001055 def GetStatus(self):
1056 """Apply a rough heuristic to give a simple summary of an issue's review
1057 or CQ status, assuming adherence to a common workflow.
1058
1059 Returns None if no issue for this branch, or one of the following keywords:
1060 * 'error' - error from review tool (including deleted issues)
1061 * 'unsent' - not sent for review
1062 * 'waiting' - waiting for review
1063 * 'reply' - waiting for owner to reply to review
1064 * 'lgtm' - LGTM from at least one approved reviewer
1065 * 'commit' - in the commit queue
1066 * 'closed' - closed
1067 """
1068 if not self.GetIssue():
1069 return None
1070
1071 try:
1072 props = self.GetIssueProperties()
1073 except urllib2.HTTPError:
1074 return 'error'
1075
1076 if props.get('closed'):
1077 # Issue is closed.
1078 return 'closed'
1079 if props.get('commit'):
1080 # Issue is in the commit queue.
1081 return 'commit'
1082
1083 try:
1084 reviewers = self.GetApprovingReviewers()
1085 except urllib2.HTTPError:
1086 return 'error'
1087
1088 if reviewers:
1089 # Was LGTM'ed.
1090 return 'lgtm'
1091
1092 messages = props.get('messages') or []
1093
1094 if not messages:
1095 # No message was sent.
1096 return 'unsent'
1097 if messages[-1]['sender'] != props.get('owner_email'):
1098 # Non-LGTM reply from non-owner
1099 return 'reply'
1100 return 'waiting'
1101
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001102 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001103 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001104
1105 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001106 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001107 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001108 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001109 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001110 except presubmit_support.PresubmitFailure, e:
1111 DieWithError(
1112 ('%s\nMaybe your depot_tools is out of date?\n'
1113 'If all fails, contact maruel@') % e)
1114
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001115 def UpdateDescription(self, description):
1116 self.description = description
1117 return self.RpcServer().update_description(
1118 self.GetIssue(), self.description)
1119
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001121 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001122 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001123
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001124 def SetFlag(self, flag, value):
1125 """Patchset must match."""
1126 if not self.GetPatchset():
1127 DieWithError('The patchset needs to match. Send another patchset.')
1128 try:
1129 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001130 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001131 except urllib2.HTTPError, e:
1132 if e.code == 404:
1133 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1134 if e.code == 403:
1135 DieWithError(
1136 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1137 'match?') % (self.GetIssue(), self.GetPatchset()))
1138 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001140 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141 """Returns an upload.RpcServer() to access this review's rietveld instance.
1142 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001143 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001144 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001145 self.GetRietveldServer(),
1146 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001147 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148
1149 def _IssueSetting(self):
1150 """Return the git setting that stores this change's issue."""
1151 return 'branch.%s.rietveldissue' % self.GetBranch()
1152
1153 def _PatchsetSetting(self):
1154 """Return the git setting that stores this change's most recent patchset."""
1155 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1156
1157 def _RietveldServer(self):
1158 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001159 branch = self.GetBranch()
1160 if branch:
1161 return 'branch.%s.rietveldserver' % branch
1162 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001163
1164
1165def GetCodereviewSettingsInteractively():
1166 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001167 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001168 server = settings.GetDefaultServerUrl(error_ok=True)
1169 prompt = 'Rietveld server (host[:port])'
1170 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001171 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001172 if not server and not newserver:
1173 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001174 if newserver:
1175 newserver = gclient_utils.UpgradeToHttps(newserver)
1176 if newserver != server:
1177 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001178
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001179 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001180 prompt = caption
1181 if initial:
1182 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001183 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184 if new_val == 'x':
1185 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001186 elif new_val:
1187 if is_url:
1188 new_val = gclient_utils.UpgradeToHttps(new_val)
1189 if new_val != initial:
1190 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001191
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001192 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001193 SetProperty(settings.GetDefaultPrivateFlag(),
1194 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001195 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001196 'tree-status-url', False)
1197 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001198 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001199 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1200 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201
1202 # TODO: configure a default branch to diff against, rather than this
1203 # svn-based hackery.
1204
1205
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001206class ChangeDescription(object):
1207 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001208 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001209 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001210
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001211 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001212 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001213
agable@chromium.org42c20792013-09-12 17:34:49 +00001214 @property # www.logilab.org/ticket/89786
1215 def description(self): # pylint: disable=E0202
1216 return '\n'.join(self._description_lines)
1217
1218 def set_description(self, desc):
1219 if isinstance(desc, basestring):
1220 lines = desc.splitlines()
1221 else:
1222 lines = [line.rstrip() for line in desc]
1223 while lines and not lines[0]:
1224 lines.pop(0)
1225 while lines and not lines[-1]:
1226 lines.pop(-1)
1227 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001228
piman@chromium.org336f9122014-09-04 02:16:55 +00001229 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001230 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001231 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001232 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001233 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001234 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001235
agable@chromium.org42c20792013-09-12 17:34:49 +00001236 # Get the set of R= and TBR= lines and remove them from the desciption.
1237 regexp = re.compile(self.R_LINE)
1238 matches = [regexp.match(line) for line in self._description_lines]
1239 new_desc = [l for i, l in enumerate(self._description_lines)
1240 if not matches[i]]
1241 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001242
agable@chromium.org42c20792013-09-12 17:34:49 +00001243 # Construct new unified R= and TBR= lines.
1244 r_names = []
1245 tbr_names = []
1246 for match in matches:
1247 if not match:
1248 continue
1249 people = cleanup_list([match.group(2).strip()])
1250 if match.group(1) == 'TBR':
1251 tbr_names.extend(people)
1252 else:
1253 r_names.extend(people)
1254 for name in r_names:
1255 if name not in reviewers:
1256 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001257 if add_owners_tbr:
1258 owners_db = owners.Database(change.RepositoryRoot(),
1259 fopen=file, os_path=os.path, glob=glob.glob)
1260 all_reviewers = set(tbr_names + reviewers)
1261 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1262 all_reviewers)
1263 tbr_names.extend(owners_db.reviewers_for(missing_files,
1264 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001265 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1266 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1267
1268 # Put the new lines in the description where the old first R= line was.
1269 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1270 if 0 <= line_loc < len(self._description_lines):
1271 if new_tbr_line:
1272 self._description_lines.insert(line_loc, new_tbr_line)
1273 if new_r_line:
1274 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001275 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001276 if new_r_line:
1277 self.append_footer(new_r_line)
1278 if new_tbr_line:
1279 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001280
1281 def prompt(self):
1282 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001283 self.set_description([
1284 '# Enter a description of the change.',
1285 '# This will be displayed on the codereview site.',
1286 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001287 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001288 '--------------------',
1289 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001290
agable@chromium.org42c20792013-09-12 17:34:49 +00001291 regexp = re.compile(self.BUG_LINE)
1292 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001293 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001294 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001295 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001296 if not content:
1297 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001298 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001299
1300 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001301 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1302 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001303 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001304 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001305
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001306 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001307 if self._description_lines:
1308 # Add an empty line if either the last line or the new line isn't a tag.
1309 last_line = self._description_lines[-1]
1310 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1311 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1312 self._description_lines.append('')
1313 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001314
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001315 def get_reviewers(self):
1316 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001317 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1318 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001319 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001320
1321
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001322def get_approving_reviewers(props):
1323 """Retrieves the reviewers that approved a CL from the issue properties with
1324 messages.
1325
1326 Note that the list may contain reviewers that are not committer, thus are not
1327 considered by the CQ.
1328 """
1329 return sorted(
1330 set(
1331 message['sender']
1332 for message in props['messages']
1333 if message['approval'] and message['sender'] in props['reviewers']
1334 )
1335 )
1336
1337
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001338def FindCodereviewSettingsFile(filename='codereview.settings'):
1339 """Finds the given file starting in the cwd and going up.
1340
1341 Only looks up to the top of the repository unless an
1342 'inherit-review-settings-ok' file exists in the root of the repository.
1343 """
1344 inherit_ok_file = 'inherit-review-settings-ok'
1345 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001346 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001347 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1348 root = '/'
1349 while True:
1350 if filename in os.listdir(cwd):
1351 if os.path.isfile(os.path.join(cwd, filename)):
1352 return open(os.path.join(cwd, filename))
1353 if cwd == root:
1354 break
1355 cwd = os.path.dirname(cwd)
1356
1357
1358def LoadCodereviewSettingsFromFile(fileobj):
1359 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001360 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001361
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362 def SetProperty(name, setting, unset_error_ok=False):
1363 fullname = 'rietveld.' + name
1364 if setting in keyvals:
1365 RunGit(['config', fullname, keyvals[setting]])
1366 else:
1367 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1368
1369 SetProperty('server', 'CODE_REVIEW_SERVER')
1370 # Only server setting is required. Other settings can be absent.
1371 # In that case, we ignore errors raised during option deletion attempt.
1372 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001373 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001374 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1375 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001376 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001377 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001378 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1379 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001380 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001381 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001382 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001383 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1384 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001385
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001386 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001387 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001388
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001389 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1390 #should be of the form
1391 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1392 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1393 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1394 keyvals['ORIGIN_URL_CONFIG']])
1395
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001397def urlretrieve(source, destination):
1398 """urllib is broken for SSL connections via a proxy therefore we
1399 can't use urllib.urlretrieve()."""
1400 with open(destination, 'w') as f:
1401 f.write(urllib2.urlopen(source).read())
1402
1403
ukai@chromium.org712d6102013-11-27 00:52:58 +00001404def hasSheBang(fname):
1405 """Checks fname is a #! script."""
1406 with open(fname) as f:
1407 return f.read(2).startswith('#!')
1408
1409
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001410def DownloadHooks(force):
1411 """downloads hooks
1412
1413 Args:
1414 force: True to update hooks. False to install hooks if not present.
1415 """
1416 if not settings.GetIsGerrit():
1417 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001418 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001419 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1420 if not os.access(dst, os.X_OK):
1421 if os.path.exists(dst):
1422 if not force:
1423 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001424 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001425 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001426 if not hasSheBang(dst):
1427 DieWithError('Not a script: %s\n'
1428 'You need to download from\n%s\n'
1429 'into .git/hooks/commit-msg and '
1430 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001431 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1432 except Exception:
1433 if os.path.exists(dst):
1434 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001435 DieWithError('\nFailed to download hooks.\n'
1436 'You need to download from\n%s\n'
1437 'into .git/hooks/commit-msg and '
1438 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001439
1440
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001441@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001442def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001443 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001444
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001445 parser.add_option('--activate-update', action='store_true',
1446 help='activate auto-updating [rietveld] section in '
1447 '.git/config')
1448 parser.add_option('--deactivate-update', action='store_true',
1449 help='deactivate auto-updating [rietveld] section in '
1450 '.git/config')
1451 options, args = parser.parse_args(args)
1452
1453 if options.deactivate_update:
1454 RunGit(['config', 'rietveld.autoupdate', 'false'])
1455 return
1456
1457 if options.activate_update:
1458 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1459 return
1460
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001461 if len(args) == 0:
1462 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001463 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001464 return 0
1465
1466 url = args[0]
1467 if not url.endswith('codereview.settings'):
1468 url = os.path.join(url, 'codereview.settings')
1469
1470 # Load code review settings and download hooks (if available).
1471 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001472 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001473 return 0
1474
1475
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001476def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001477 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001478 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1479 branch = ShortBranchName(branchref)
1480 _, args = parser.parse_args(args)
1481 if not args:
1482 print("Current base-url:")
1483 return RunGit(['config', 'branch.%s.base-url' % branch],
1484 error_ok=False).strip()
1485 else:
1486 print("Setting base-url to %s" % args[0])
1487 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1488 error_ok=False).strip()
1489
1490
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001491def color_for_status(status):
1492 """Maps a Changelist status to color, for CMDstatus and other tools."""
1493 return {
1494 'unsent': Fore.RED,
1495 'waiting': Fore.BLUE,
1496 'reply': Fore.YELLOW,
1497 'lgtm': Fore.GREEN,
1498 'commit': Fore.MAGENTA,
1499 'closed': Fore.CYAN,
1500 'error': Fore.WHITE,
1501 }.get(status, Fore.WHITE)
1502
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001503def fetch_cl_status(branch, auth_config=None):
1504 """Fetches information for an issue and returns (branch, issue, status)."""
1505 cl = Changelist(branchref=branch, auth_config=auth_config)
1506 url = cl.GetIssueURL()
1507 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001508
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001509 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001510 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001511 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001512
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001513 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001514
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001515def get_cl_statuses(
1516 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001517 """Returns a blocking iterable of (branch, issue, color) for given branches.
1518
1519 If fine_grained is true, this will fetch CL statuses from the server.
1520 Otherwise, simply indicate if there's a matching url for the given branches.
1521
1522 If max_processes is specified, it is used as the maximum number of processes
1523 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1524 spawned.
1525 """
1526 # Silence upload.py otherwise it becomes unwieldly.
1527 upload.verbosity = 0
1528
1529 if fine_grained:
1530 # Process one branch synchronously to work through authentication, then
1531 # spawn processes to process all the other branches in parallel.
1532 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001533 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1534 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001535
1536 branches_to_fetch = branches[1:]
1537 pool = ThreadPool(
1538 min(max_processes, len(branches_to_fetch))
1539 if max_processes is not None
1540 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001541 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001542 yield x
1543 else:
1544 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1545 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001546 cl = Changelist(branchref=b, auth_config=auth_config)
1547 url = cl.GetIssueURL()
1548 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001549
rmistry@google.com2dd99862015-06-22 12:22:18 +00001550
1551def upload_branch_deps(cl, args):
1552 """Uploads CLs of local branches that are dependents of the current branch.
1553
1554 If the local branch dependency tree looks like:
1555 test1 -> test2.1 -> test3.1
1556 -> test3.2
1557 -> test2.2 -> test3.3
1558
1559 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1560 run on the dependent branches in this order:
1561 test2.1, test3.1, test3.2, test2.2, test3.3
1562
1563 Note: This function does not rebase your local dependent branches. Use it when
1564 you make a change to the parent branch that will not conflict with its
1565 dependent branches, and you would like their dependencies updated in
1566 Rietveld.
1567 """
1568 if git_common.is_dirty_git_tree('upload-branch-deps'):
1569 return 1
1570
1571 root_branch = cl.GetBranch()
1572 if root_branch is None:
1573 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1574 'Get on a branch!')
1575 if not cl.GetIssue() or not cl.GetPatchset():
1576 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1577 'patchset dependencies without an uploaded CL.')
1578
1579 branches = RunGit(['for-each-ref',
1580 '--format=%(refname:short) %(upstream:short)',
1581 'refs/heads'])
1582 if not branches:
1583 print('No local branches found.')
1584 return 0
1585
1586 # Create a dictionary of all local branches to the branches that are dependent
1587 # on it.
1588 tracked_to_dependents = collections.defaultdict(list)
1589 for b in branches.splitlines():
1590 tokens = b.split()
1591 if len(tokens) == 2:
1592 branch_name, tracked = tokens
1593 tracked_to_dependents[tracked].append(branch_name)
1594
1595 print
1596 print 'The dependent local branches of %s are:' % root_branch
1597 dependents = []
1598 def traverse_dependents_preorder(branch, padding=''):
1599 dependents_to_process = tracked_to_dependents.get(branch, [])
1600 padding += ' '
1601 for dependent in dependents_to_process:
1602 print '%s%s' % (padding, dependent)
1603 dependents.append(dependent)
1604 traverse_dependents_preorder(dependent, padding)
1605 traverse_dependents_preorder(root_branch)
1606 print
1607
1608 if not dependents:
1609 print 'There are no dependent local branches for %s' % root_branch
1610 return 0
1611
1612 print ('This command will checkout all dependent branches and run '
1613 '"git cl upload".')
1614 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1615
1616 # Add a default patchset title to all upload calls.
1617 args.extend(['-t', 'Updated patchset dependency'])
1618 # Record all dependents that failed to upload.
1619 failures = {}
1620 # Go through all dependents, checkout the branch and upload.
1621 try:
1622 for dependent_branch in dependents:
1623 print
1624 print '--------------------------------------'
1625 print 'Running "git cl upload" from %s:' % dependent_branch
1626 RunGit(['checkout', '-q', dependent_branch])
1627 print
1628 try:
1629 if CMDupload(OptionParser(), args) != 0:
1630 print 'Upload failed for %s!' % dependent_branch
1631 failures[dependent_branch] = 1
1632 except: # pylint: disable=W0702
1633 failures[dependent_branch] = 1
1634 print
1635 finally:
1636 # Swap back to the original root branch.
1637 RunGit(['checkout', '-q', root_branch])
1638
1639 print
1640 print 'Upload complete for dependent branches!'
1641 for dependent_branch in dependents:
1642 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1643 print ' %s : %s' % (dependent_branch, upload_status)
1644 print
1645
1646 return 0
1647
1648
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001649def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001650 """Show status of changelists.
1651
1652 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001653 - Red not sent for review or broken
1654 - Blue waiting for review
1655 - Yellow waiting for you to reply to review
1656 - Green LGTM'ed
1657 - Magenta in the commit queue
1658 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001659
1660 Also see 'git cl comments'.
1661 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001662 parser.add_option('--field',
1663 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001664 parser.add_option('-f', '--fast', action='store_true',
1665 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001666 parser.add_option(
1667 '-j', '--maxjobs', action='store', type=int,
1668 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001669
1670 auth.add_auth_options(parser)
1671 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001672 if args:
1673 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001674 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001675
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001676 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001677 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001678 if options.field.startswith('desc'):
1679 print cl.GetDescription()
1680 elif options.field == 'id':
1681 issueid = cl.GetIssue()
1682 if issueid:
1683 print issueid
1684 elif options.field == 'patch':
1685 patchset = cl.GetPatchset()
1686 if patchset:
1687 print patchset
1688 elif options.field == 'url':
1689 url = cl.GetIssueURL()
1690 if url:
1691 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001692 return 0
1693
1694 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1695 if not branches:
1696 print('No local branch found.')
1697 return 0
1698
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001699 changes = (
1700 Changelist(branchref=b, auth_config=auth_config)
1701 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001702 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001703 alignment = max(5, max(len(b) for b in branches))
1704 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001705 output = get_cl_statuses(branches,
1706 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001707 max_processes=options.maxjobs,
1708 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001709
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001710 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001711 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001712 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001713 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001714 b, i, status = output.next()
1715 branch_statuses[b] = (i, status)
1716 issue_url, status = branch_statuses.pop(branch)
1717 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001718 reset = Fore.RESET
1719 if not sys.stdout.isatty():
1720 color = ''
1721 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001722 status_str = '(%s)' % status if status else ''
1723 print ' %*s : %s%s %s%s' % (
1724 alignment, ShortBranchName(branch), color, issue_url, status_str,
1725 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001726
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001727 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001728 print
1729 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001730 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001731 if not cl.GetIssue():
1732 print 'No issue assigned.'
1733 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001734 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001735 if not options.fast:
1736 print 'Issue description:'
1737 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001738 return 0
1739
1740
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001741def colorize_CMDstatus_doc():
1742 """To be called once in main() to add colors to git cl status help."""
1743 colors = [i for i in dir(Fore) if i[0].isupper()]
1744
1745 def colorize_line(line):
1746 for color in colors:
1747 if color in line.upper():
1748 # Extract whitespaces first and the leading '-'.
1749 indent = len(line) - len(line.lstrip(' ')) + 1
1750 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1751 return line
1752
1753 lines = CMDstatus.__doc__.splitlines()
1754 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1755
1756
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001757@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001758def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001759 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001760
1761 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001762 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001763 parser.add_option('-r', '--reverse', action='store_true',
1764 help='Lookup the branch(es) for the specified issues. If '
1765 'no issues are specified, all branches with mapped '
1766 'issues will be listed.')
1767 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001768
dnj@chromium.org406c4402015-03-03 17:22:28 +00001769 if options.reverse:
1770 branches = RunGit(['for-each-ref', 'refs/heads',
1771 '--format=%(refname:short)']).splitlines()
1772
1773 # Reverse issue lookup.
1774 issue_branch_map = {}
1775 for branch in branches:
1776 cl = Changelist(branchref=branch)
1777 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1778 if not args:
1779 args = sorted(issue_branch_map.iterkeys())
1780 for issue in args:
1781 if not issue:
1782 continue
1783 print 'Branch for issue number %s: %s' % (
1784 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1785 else:
1786 cl = Changelist()
1787 if len(args) > 0:
1788 try:
1789 issue = int(args[0])
1790 except ValueError:
1791 DieWithError('Pass a number to set the issue or none to list it.\n'
1792 'Maybe you want to run git cl status?')
1793 cl.SetIssue(issue)
1794 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001795 return 0
1796
1797
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001798def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001799 """Shows or posts review comments for any changelist."""
1800 parser.add_option('-a', '--add-comment', dest='comment',
1801 help='comment to add to an issue')
1802 parser.add_option('-i', dest='issue',
1803 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00001804 parser.add_option('-j', '--json-file',
1805 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001806 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001807 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001808 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001809
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001810 issue = None
1811 if options.issue:
1812 try:
1813 issue = int(options.issue)
1814 except ValueError:
1815 DieWithError('A review issue id is expected to be a number')
1816
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001817 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001818
1819 if options.comment:
1820 cl.AddComment(options.comment)
1821 return 0
1822
1823 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00001824 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001825 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00001826 summary.append({
1827 'date': message['date'],
1828 'lgtm': False,
1829 'message': message['text'],
1830 'not_lgtm': False,
1831 'sender': message['sender'],
1832 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001833 if message['disapproval']:
1834 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00001835 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001836 elif message['approval']:
1837 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00001838 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001839 elif message['sender'] == data['owner_email']:
1840 color = Fore.MAGENTA
1841 else:
1842 color = Fore.BLUE
1843 print '\n%s%s %s%s' % (
1844 color, message['date'].split('.', 1)[0], message['sender'],
1845 Fore.RESET)
1846 if message['text'].strip():
1847 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00001848 if options.json_file:
1849 with open(options.json_file, 'wb') as f:
1850 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001851 return 0
1852
1853
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001854def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001855 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00001856 parser.add_option('-d', '--display', action='store_true',
1857 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001858 auth.add_auth_options(parser)
1859 options, _ = parser.parse_args(args)
1860 auth_config = auth.extract_auth_config_from_options(options)
1861 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001862 if not cl.GetIssue():
1863 DieWithError('This branch has no associated changelist.')
1864 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00001865 if options.display:
1866 print description.description
1867 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001868 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00001869 if cl.GetDescription() != description.description:
1870 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001871 return 0
1872
1873
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001874def CreateDescriptionFromLog(args):
1875 """Pulls out the commit log to use as a base for the CL description."""
1876 log_args = []
1877 if len(args) == 1 and not args[0].endswith('.'):
1878 log_args = [args[0] + '..']
1879 elif len(args) == 1 and args[0].endswith('...'):
1880 log_args = [args[0][:-1]]
1881 elif len(args) == 2:
1882 log_args = [args[0] + '..' + args[1]]
1883 else:
1884 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001885 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001886
1887
thestig@chromium.org44202a22014-03-11 19:22:18 +00001888def CMDlint(parser, args):
1889 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001890 parser.add_option('--filter', action='append', metavar='-x,+y',
1891 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001892 auth.add_auth_options(parser)
1893 options, args = parser.parse_args(args)
1894 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001895
1896 # Access to a protected member _XX of a client class
1897 # pylint: disable=W0212
1898 try:
1899 import cpplint
1900 import cpplint_chromium
1901 except ImportError:
1902 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1903 return 1
1904
1905 # Change the current working directory before calling lint so that it
1906 # shows the correct base.
1907 previous_cwd = os.getcwd()
1908 os.chdir(settings.GetRoot())
1909 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001910 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001911 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1912 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001913 if not files:
1914 print "Cannot lint an empty CL"
1915 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001916
1917 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001918 command = args + files
1919 if options.filter:
1920 command = ['--filter=' + ','.join(options.filter)] + command
1921 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001922
1923 white_regex = re.compile(settings.GetLintRegex())
1924 black_regex = re.compile(settings.GetLintIgnoreRegex())
1925 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1926 for filename in filenames:
1927 if white_regex.match(filename):
1928 if black_regex.match(filename):
1929 print "Ignoring file %s" % filename
1930 else:
1931 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1932 extra_check_functions)
1933 else:
1934 print "Skipping file %s" % filename
1935 finally:
1936 os.chdir(previous_cwd)
1937 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1938 if cpplint._cpplint_state.error_count != 0:
1939 return 1
1940 return 0
1941
1942
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001943def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001944 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001945 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001946 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001947 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001948 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001949 auth.add_auth_options(parser)
1950 options, args = parser.parse_args(args)
1951 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001952
sbc@chromium.org71437c02015-04-09 19:29:40 +00001953 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00001954 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001955 return 1
1956
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001957 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001958 if args:
1959 base_branch = args[0]
1960 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001961 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001962 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001963
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001964 cl.RunHook(
1965 committing=not options.upload,
1966 may_prompt=False,
1967 verbose=options.verbose,
1968 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001969 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001970
1971
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001972def AddChangeIdToCommitMessage(options, args):
1973 """Re-commits using the current message, assumes the commit hook is in
1974 place.
1975 """
1976 log_desc = options.message or CreateDescriptionFromLog(args)
1977 git_command = ['commit', '--amend', '-m', log_desc]
1978 RunGit(git_command)
1979 new_log_desc = CreateDescriptionFromLog(args)
1980 if CHANGE_ID in new_log_desc:
1981 print 'git-cl: Added Change-Id to commit message.'
1982 else:
1983 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1984
1985
piman@chromium.org336f9122014-09-04 02:16:55 +00001986def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001987 """upload the current branch to gerrit."""
1988 # We assume the remote called "origin" is the one we want.
1989 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001990 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00001991
1992 remote, remote_branch = cl.GetRemoteBranch()
1993 branch = GetTargetRef(remote, remote_branch, options.target_branch,
1994 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001995
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001996 change_desc = ChangeDescription(
1997 options.message or CreateDescriptionFromLog(args))
1998 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001999 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002000 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002001
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002002 if options.squash:
2003 # Try to get the message from a previous upload.
2004 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
2005 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
2006 if not message:
2007 if not options.force:
2008 change_desc.prompt()
2009
2010 if CHANGE_ID not in change_desc.description:
2011 # Run the commit-msg hook without modifying the head commit by writing
2012 # the commit message to a temporary file and running the hook over it,
2013 # then reading the file back in.
2014 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
2015 'commit-msg')
2016 file_handle, msg_file = tempfile.mkstemp(text=True,
2017 prefix='commit_msg')
2018 try:
2019 try:
2020 with os.fdopen(file_handle, 'w') as fileobj:
2021 fileobj.write(change_desc.description)
2022 finally:
2023 os.close(file_handle)
2024 RunCommand([commit_msg_hook, msg_file])
2025 change_desc.set_description(gclient_utils.FileRead(msg_file))
2026 finally:
2027 os.remove(msg_file)
2028
2029 if not change_desc.description:
2030 print "Description is empty; aborting."
2031 return 1
2032
2033 message = change_desc.description
2034
2035 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2036 if remote is '.':
2037 # If our upstream branch is local, we base our squashed commit on its
2038 # squashed version.
2039 parent = ('refs/heads/git_cl_uploads/' +
2040 scm.GIT.ShortBranchName(upstream_branch))
2041
2042 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2043 # will create additional CLs when uploading.
2044 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2045 RunGitSilent(['rev-parse', parent + ':'])):
2046 print 'Upload upstream branch ' + upstream_branch + ' first.'
2047 return 1
2048 else:
2049 parent = cl.GetCommonAncestorWithUpstream()
2050
2051 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2052 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2053 '-m', message]).strip()
2054 else:
2055 if CHANGE_ID not in change_desc.description:
2056 AddChangeIdToCommitMessage(options, args)
2057 ref_to_push = 'HEAD'
2058 parent = '%s/%s' % (gerrit_remote, branch)
2059
2060 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2061 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002062 if len(commits) > 1:
2063 print('WARNING: This will upload %d commits. Run the following command '
2064 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002065 print('git log %s..%s' % (parent, ref_to_push))
2066 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002067 'commit.')
2068 ask_for_data('About to upload; enter to confirm.')
2069
piman@chromium.org336f9122014-09-04 02:16:55 +00002070 if options.reviewers or options.tbr_owners:
2071 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002072
ukai@chromium.orge8077812012-02-03 03:41:46 +00002073 receive_options = []
2074 cc = cl.GetCCList().split(',')
2075 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002076 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002077 cc = filter(None, cc)
2078 if cc:
2079 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002080 if change_desc.get_reviewers():
2081 receive_options.extend(
2082 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002083
ukai@chromium.orge8077812012-02-03 03:41:46 +00002084 git_command = ['push']
2085 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002086 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002087 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002088 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002089 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002090
2091 if options.squash:
2092 head = RunGit(['rev-parse', 'HEAD']).strip()
2093 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2094
ukai@chromium.orge8077812012-02-03 03:41:46 +00002095 # TODO(ukai): parse Change-Id: and set issue number?
2096 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002097
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002098
wittman@chromium.org455dc922015-01-26 20:15:50 +00002099def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2100 """Computes the remote branch ref to use for the CL.
2101
2102 Args:
2103 remote (str): The git remote for the CL.
2104 remote_branch (str): The git remote branch for the CL.
2105 target_branch (str): The target branch specified by the user.
2106 pending_prefix (str): The pending prefix from the settings.
2107 """
2108 if not (remote and remote_branch):
2109 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002110
wittman@chromium.org455dc922015-01-26 20:15:50 +00002111 if target_branch:
2112 # Cannonicalize branch references to the equivalent local full symbolic
2113 # refs, which are then translated into the remote full symbolic refs
2114 # below.
2115 if '/' not in target_branch:
2116 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2117 else:
2118 prefix_replacements = (
2119 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2120 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2121 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2122 )
2123 match = None
2124 for regex, replacement in prefix_replacements:
2125 match = re.search(regex, target_branch)
2126 if match:
2127 remote_branch = target_branch.replace(match.group(0), replacement)
2128 break
2129 if not match:
2130 # This is a branch path but not one we recognize; use as-is.
2131 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002132 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2133 # Handle the refs that need to land in different refs.
2134 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002135
wittman@chromium.org455dc922015-01-26 20:15:50 +00002136 # Create the true path to the remote branch.
2137 # Does the following translation:
2138 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2139 # * refs/remotes/origin/master -> refs/heads/master
2140 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2141 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2142 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2143 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2144 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2145 'refs/heads/')
2146 elif remote_branch.startswith('refs/remotes/branch-heads'):
2147 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2148 # If a pending prefix exists then replace refs/ with it.
2149 if pending_prefix:
2150 remote_branch = remote_branch.replace('refs/', pending_prefix)
2151 return remote_branch
2152
2153
piman@chromium.org336f9122014-09-04 02:16:55 +00002154def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002155 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002156 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2157 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002158 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002159 if options.emulate_svn_auto_props:
2160 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002161
2162 change_desc = None
2163
pgervais@chromium.org91141372014-01-09 23:27:20 +00002164 if options.email is not None:
2165 upload_args.extend(['--email', options.email])
2166
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002167 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002168 if options.title:
2169 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002170 if options.message:
2171 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002172 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002173 print ("This branch is associated with issue %s. "
2174 "Adding patch to that issue." % cl.GetIssue())
2175 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002176 if options.title:
2177 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002178 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002179 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002180 if options.reviewers or options.tbr_owners:
2181 change_desc.update_reviewers(options.reviewers,
2182 options.tbr_owners,
2183 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002184 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002185 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002186
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002187 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002188 print "Description is empty; aborting."
2189 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002190
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002191 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002192 if change_desc.get_reviewers():
2193 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002194 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002195 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002196 DieWithError("Must specify reviewers to send email.")
2197 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002198
2199 # We check this before applying rietveld.private assuming that in
2200 # rietveld.cc only addresses which we can send private CLs to are listed
2201 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2202 # --private is specified explicitly on the command line.
2203 if options.private:
2204 logging.warn('rietveld.cc is ignored since private flag is specified. '
2205 'You need to review and add them manually if necessary.')
2206 cc = cl.GetCCListWithoutDefault()
2207 else:
2208 cc = cl.GetCCList()
2209 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002210 if cc:
2211 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002212
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002213 if options.private or settings.GetDefaultPrivateFlag() == "True":
2214 upload_args.append('--private')
2215
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002216 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002217 if not options.find_copies:
2218 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002219
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002220 # Include the upstream repo's URL in the change -- this is useful for
2221 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002222 remote_url = cl.GetGitBaseUrlFromConfig()
2223 if not remote_url:
2224 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002225 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002226 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002227 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2228 remote_url = (cl.GetRemoteUrl() + '@'
2229 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002230 if remote_url:
2231 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002232 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002233 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2234 settings.GetPendingRefPrefix())
2235 if target_ref:
2236 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002237
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002238 # Look for dependent patchsets. See crbug.com/480453 for more details.
2239 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2240 upstream_branch = ShortBranchName(upstream_branch)
2241 if remote is '.':
2242 # A local branch is being tracked.
2243 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002244 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002245 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002246 print ('Skipping dependency patchset upload because git config '
2247 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002248 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002249 else:
2250 auth_config = auth.extract_auth_config_from_options(options)
2251 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2252 branch_cl_issue_url = branch_cl.GetIssueURL()
2253 branch_cl_issue = branch_cl.GetIssue()
2254 branch_cl_patchset = branch_cl.GetPatchset()
2255 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2256 upload_args.extend(
2257 ['--depends_on_patchset', '%s:%s' % (
2258 branch_cl_issue, branch_cl_patchset)])
2259 print
2260 print ('The current branch (%s) is tracking a local branch (%s) with '
2261 'an associated CL.') % (cl.GetBranch(), local_branch)
2262 print 'Adding %s/#ps%s as a dependency patchset.' % (
2263 branch_cl_issue_url, branch_cl_patchset)
2264 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002265
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002266 project = settings.GetProject()
2267 if project:
2268 upload_args.extend(['--project', project])
2269
rmistry@google.comef966222015-04-07 11:15:01 +00002270 if options.cq_dry_run:
2271 upload_args.extend(['--cq_dry_run'])
2272
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002273 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002274 upload_args = ['upload'] + upload_args + args
2275 logging.info('upload.RealMain(%s)', upload_args)
2276 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002277 issue = int(issue)
2278 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002279 except KeyboardInterrupt:
2280 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002281 except:
2282 # If we got an exception after the user typed a description for their
2283 # change, back up the description before re-raising.
2284 if change_desc:
2285 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2286 print '\nGot exception while uploading -- saving description to %s\n' \
2287 % backup_path
2288 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002289 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002290 backup_file.close()
2291 raise
2292
2293 if not cl.GetIssue():
2294 cl.SetIssue(issue)
2295 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002296
2297 if options.use_commit_queue:
2298 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002299 return 0
2300
2301
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002302def cleanup_list(l):
2303 """Fixes a list so that comma separated items are put as individual items.
2304
2305 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2306 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2307 """
2308 items = sum((i.split(',') for i in l), [])
2309 stripped_items = (i.strip() for i in items)
2310 return sorted(filter(None, stripped_items))
2311
2312
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002313@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002314def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002315 """Uploads the current changelist to codereview.
2316
2317 Can skip dependency patchset uploads for a branch by running:
2318 git config branch.branch_name.skip-deps-uploads True
2319 To unset run:
2320 git config --unset branch.branch_name.skip-deps-uploads
2321 Can also set the above globally by using the --global flag.
2322 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002323 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2324 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002325 parser.add_option('--bypass-watchlists', action='store_true',
2326 dest='bypass_watchlists',
2327 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002328 parser.add_option('-f', action='store_true', dest='force',
2329 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002330 parser.add_option('-m', dest='message', help='message for patchset')
2331 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002332 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002333 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002334 help='reviewer email addresses')
2335 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002336 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002337 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002338 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002339 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002340 parser.add_option('--emulate_svn_auto_props',
2341 '--emulate-svn-auto-props',
2342 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002343 dest="emulate_svn_auto_props",
2344 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002345 parser.add_option('-c', '--use-commit-queue', action='store_true',
2346 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002347 parser.add_option('--private', action='store_true',
2348 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002349 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002350 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002351 metavar='TARGET',
2352 help='Apply CL to remote ref TARGET. ' +
2353 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002354 parser.add_option('--squash', action='store_true',
2355 help='Squash multiple commits into one (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002356 parser.add_option('--email', default=None,
2357 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002358 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2359 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002360 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2361 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002362 help='Send the patchset to do a CQ dry run right after '
2363 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002364 parser.add_option('--dependencies', action='store_true',
2365 help='Uploads CLs of all the local branches that depend on '
2366 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002367
rmistry@google.com2dd99862015-06-22 12:22:18 +00002368 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002369 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002370 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002371 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002372 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002373
sbc@chromium.org71437c02015-04-09 19:29:40 +00002374 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002375 return 1
2376
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002377 options.reviewers = cleanup_list(options.reviewers)
2378 options.cc = cleanup_list(options.cc)
2379
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002380 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002381 if args:
2382 # TODO(ukai): is it ok for gerrit case?
2383 base_branch = args[0]
2384 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002385 if cl.GetBranch() is None:
2386 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2387
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002388 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002389 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002390 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002391
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002392 # Make sure authenticated to Rietveld before running expensive hooks. It is
2393 # a fast, best efforts check. Rietveld still can reject the authentication
2394 # during the actual upload.
2395 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2396 authenticator = auth.get_authenticator_for_host(
2397 cl.GetRietveldServer(), auth_config)
2398 if not authenticator.has_cached_credentials():
2399 raise auth.LoginRequiredError(cl.GetRietveldServer())
2400
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002401 # Apply watchlists on upload.
2402 change = cl.GetChange(base_branch, None)
2403 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2404 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002405 if not options.bypass_watchlists:
2406 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002407
ukai@chromium.orge8077812012-02-03 03:41:46 +00002408 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002409 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002410 # Set the reviewer list now so that presubmit checks can access it.
2411 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002412 change_description.update_reviewers(options.reviewers,
2413 options.tbr_owners,
2414 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002415 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002416 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002417 may_prompt=not options.force,
2418 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002419 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002420 if not hook_results.should_continue():
2421 return 1
2422 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002423 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002424
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002425 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002426 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002427 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002428 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002429 print ('The last upload made from this repository was patchset #%d but '
2430 'the most recent patchset on the server is #%d.'
2431 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002432 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2433 'from another machine or branch the patch you\'re uploading now '
2434 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002435 ask_for_data('About to upload; enter to confirm.')
2436
iannucci@chromium.org79540052012-10-19 23:15:26 +00002437 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002438 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00002439 return GerritUpload(options, args, cl, change)
2440 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002441 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002442 git_set_branch_value('last-upload-hash',
2443 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002444 # Run post upload hooks, if specified.
2445 if settings.GetRunPostUploadHook():
2446 presubmit_support.DoPostUploadExecuter(
2447 change,
2448 cl,
2449 settings.GetRoot(),
2450 options.verbose,
2451 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002452
rmistry@google.com2dd99862015-06-22 12:22:18 +00002453 # Upload all dependencies if specified.
2454 if options.dependencies:
2455 print
2456 print '--dependencies has been specified.'
2457 print 'All dependent local branches will be re-uploaded.'
2458 print
2459 # Remove the dependencies flag from args so that we do not end up in a
2460 # loop.
2461 orig_args.remove('--dependencies')
2462 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002463 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002464
2465
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002466def IsSubmoduleMergeCommit(ref):
2467 # When submodules are added to the repo, we expect there to be a single
2468 # non-git-svn merge commit at remote HEAD with a signature comment.
2469 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002470 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002471 return RunGit(cmd) != ''
2472
2473
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002474def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002475 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002476
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002477 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002478 Updates changelog with metadata (e.g. pointer to review).
2479 Pushes/dcommits the code upstream.
2480 Updates review and closes.
2481 """
2482 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2483 help='bypass upload presubmit hook')
2484 parser.add_option('-m', dest='message',
2485 help="override review description")
2486 parser.add_option('-f', action='store_true', dest='force',
2487 help="force yes to questions (don't prompt)")
2488 parser.add_option('-c', dest='contributor',
2489 help="external contributor for patch (appended to " +
2490 "description and used as author for git). Should be " +
2491 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002492 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002493 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002494 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002495 auth_config = auth.extract_auth_config_from_options(options)
2496
2497 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002498
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002499 current = cl.GetBranch()
2500 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2501 if not settings.GetIsGitSvn() and remote == '.':
2502 print
2503 print 'Attempting to push branch %r into another local branch!' % current
2504 print
2505 print 'Either reparent this branch on top of origin/master:'
2506 print ' git reparent-branch --root'
2507 print
2508 print 'OR run `git rebase-update` if you think the parent branch is already'
2509 print 'committed.'
2510 print
2511 print ' Current parent: %r' % upstream_branch
2512 return 1
2513
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002514 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002515 # Default to merging against our best guess of the upstream branch.
2516 args = [cl.GetUpstreamBranch()]
2517
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002518 if options.contributor:
2519 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2520 print "Please provide contibutor as 'First Last <email@example.com>'"
2521 return 1
2522
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002523 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002524 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002525
sbc@chromium.org71437c02015-04-09 19:29:40 +00002526 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002527 return 1
2528
2529 # This rev-list syntax means "show all commits not in my branch that
2530 # are in base_branch".
2531 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2532 base_branch]).splitlines()
2533 if upstream_commits:
2534 print ('Base branch "%s" has %d commits '
2535 'not in this branch.' % (base_branch, len(upstream_commits)))
2536 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2537 return 1
2538
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002539 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002540 svn_head = None
2541 if cmd == 'dcommit' or base_has_submodules:
2542 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2543 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002544
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002545 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002546 # If the base_head is a submodule merge commit, the first parent of the
2547 # base_head should be a git-svn commit, which is what we're interested in.
2548 base_svn_head = base_branch
2549 if base_has_submodules:
2550 base_svn_head += '^1'
2551
2552 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002553 if extra_commits:
2554 print ('This branch has %d additional commits not upstreamed yet.'
2555 % len(extra_commits.splitlines()))
2556 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2557 'before attempting to %s.' % (base_branch, cmd))
2558 return 1
2559
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002560 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002561 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002562 author = None
2563 if options.contributor:
2564 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002565 hook_results = cl.RunHook(
2566 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002567 may_prompt=not options.force,
2568 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002569 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002570 if not hook_results.should_continue():
2571 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002572
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002573 # Check the tree status if the tree status URL is set.
2574 status = GetTreeStatus()
2575 if 'closed' == status:
2576 print('The tree is closed. Please wait for it to reopen. Use '
2577 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2578 return 1
2579 elif 'unknown' == status:
2580 print('Unable to determine tree status. Please verify manually and '
2581 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2582 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002583 else:
2584 breakpad.SendStack(
2585 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002586 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2587 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002588 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002589
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002590 change_desc = ChangeDescription(options.message)
2591 if not change_desc.description and cl.GetIssue():
2592 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002593
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002594 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002595 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002596 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002597 else:
2598 print 'No description set.'
2599 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2600 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002601
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002602 # Keep a separate copy for the commit message, because the commit message
2603 # contains the link to the Rietveld issue, while the Rietveld message contains
2604 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002605 # Keep a separate copy for the commit message.
2606 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002607 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002608
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002609 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002610 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002611 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002612 # after it. Add a period on a new line to circumvent this. Also add a space
2613 # before the period to make sure that Gitiles continues to correctly resolve
2614 # the URL.
2615 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002616 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002617 commit_desc.append_footer('Patch from %s.' % options.contributor)
2618
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002619 print('Description:')
2620 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002621
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002622 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002623 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002624 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002625
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002626 # We want to squash all this branch's commits into one commit with the proper
2627 # description. We do this by doing a "reset --soft" to the base branch (which
2628 # keeps the working copy the same), then dcommitting that. If origin/master
2629 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2630 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002631 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002632 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2633 # Delete the branches if they exist.
2634 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2635 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2636 result = RunGitWithCode(showref_cmd)
2637 if result[0] == 0:
2638 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002639
2640 # We might be in a directory that's present in this branch but not in the
2641 # trunk. Move up to the top of the tree so that git commands that expect a
2642 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002643 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002644 if rel_base_path:
2645 os.chdir(rel_base_path)
2646
2647 # Stuff our change into the merge branch.
2648 # We wrap in a try...finally block so if anything goes wrong,
2649 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002650 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002651 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002652 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002653 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002654 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002655 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002656 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002657 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002658 RunGit(
2659 [
2660 'commit', '--author', options.contributor,
2661 '-m', commit_desc.description,
2662 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002663 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002664 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002665 if base_has_submodules:
2666 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2667 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2668 RunGit(['checkout', CHERRY_PICK_BRANCH])
2669 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002670 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002671 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002672 pending_prefix = settings.GetPendingRefPrefix()
2673 if not pending_prefix or branch.startswith(pending_prefix):
2674 # If not using refs/pending/heads/* at all, or target ref is already set
2675 # to pending, then push to the target ref directly.
2676 retcode, output = RunGitWithCode(
2677 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002678 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002679 else:
2680 # Cherry-pick the change on top of pending ref and then push it.
2681 assert branch.startswith('refs/'), branch
2682 assert pending_prefix[-1] == '/', pending_prefix
2683 pending_ref = pending_prefix + branch[len('refs/'):]
2684 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002685 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002686 if retcode == 0:
2687 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002688 else:
2689 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002690 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002691 'svn', 'dcommit',
2692 '-C%s' % options.similarity,
2693 '--no-rebase', '--rmdir',
2694 ]
2695 if settings.GetForceHttpsCommitUrl():
2696 # Allow forcing https commit URLs for some projects that don't allow
2697 # committing to http URLs (like Google Code).
2698 remote_url = cl.GetGitSvnRemoteUrl()
2699 if urlparse.urlparse(remote_url).scheme == 'http':
2700 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002701 cmd_args.append('--commit-url=%s' % remote_url)
2702 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002703 if 'Committed r' in output:
2704 revision = re.match(
2705 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2706 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002707 finally:
2708 # And then swap back to the original branch and clean up.
2709 RunGit(['checkout', '-q', cl.GetBranch()])
2710 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002711 if base_has_submodules:
2712 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002713
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002714 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002715 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002716 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002717
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002718 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002719 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002720 try:
2721 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2722 # We set pushed_to_pending to False, since it made it all the way to the
2723 # real ref.
2724 pushed_to_pending = False
2725 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002726 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002727
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002728 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002729 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002730 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002731 if not to_pending:
2732 if viewvc_url and revision:
2733 change_desc.append_footer(
2734 'Committed: %s%s' % (viewvc_url, revision))
2735 elif revision:
2736 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002737 print ('Closing issue '
2738 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002739 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002740 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002741 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002742 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002743 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002744 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002745 if options.bypass_hooks:
2746 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2747 else:
2748 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002749 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002750 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002751
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002752 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002753 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2754 print 'The commit is in the pending queue (%s).' % pending_ref
2755 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002756 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002757 'footer.' % branch)
2758
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002759 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2760 if os.path.isfile(hook):
2761 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002762
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002763 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002764
2765
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002766def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2767 print
2768 print 'Waiting for commit to be landed on %s...' % real_ref
2769 print '(If you are impatient, you may Ctrl-C once without harm)'
2770 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2771 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2772
2773 loop = 0
2774 while True:
2775 sys.stdout.write('fetching (%d)... \r' % loop)
2776 sys.stdout.flush()
2777 loop += 1
2778
2779 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2780 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2781 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2782 for commit in commits.splitlines():
2783 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2784 print 'Found commit on %s' % real_ref
2785 return commit
2786
2787 current_rev = to_rev
2788
2789
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002790def PushToGitPending(remote, pending_ref, upstream_ref):
2791 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2792
2793 Returns:
2794 (retcode of last operation, output log of last operation).
2795 """
2796 assert pending_ref.startswith('refs/'), pending_ref
2797 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2798 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2799 code = 0
2800 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002801 max_attempts = 3
2802 attempts_left = max_attempts
2803 while attempts_left:
2804 if attempts_left != max_attempts:
2805 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2806 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002807
2808 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002809 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002810 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002811 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002812 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002813 print 'Fetch failed with exit code %d.' % code
2814 if out.strip():
2815 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002816 continue
2817
2818 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002819 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002820 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002821 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002822 if code:
2823 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002824 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2825 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002826 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2827 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002828 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002829 return code, out
2830
2831 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002832 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002833 code, out = RunGitWithCode(
2834 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2835 if code == 0:
2836 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002837 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002838 return code, out
2839
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002840 print 'Push failed with exit code %d.' % code
2841 if out.strip():
2842 print out.strip()
2843 if IsFatalPushFailure(out):
2844 print (
2845 'Fatal push error. Make sure your .netrc credentials and git '
2846 'user.email are correct and you have push access to the repo.')
2847 return code, out
2848
2849 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002850 return code, out
2851
2852
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002853def IsFatalPushFailure(push_stdout):
2854 """True if retrying push won't help."""
2855 return '(prohibited by Gerrit)' in push_stdout
2856
2857
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002858@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002859def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002860 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002861 if not settings.GetIsGitSvn():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002862 if get_footer_svn_id():
2863 # If it looks like previous commits were mirrored with git-svn.
2864 message = """This repository appears to be a git-svn mirror, but no
2865upstream SVN master is set. You probably need to run 'git auto-svn' once."""
2866 else:
2867 message = """This doesn't appear to be an SVN repository.
2868If your project has a true, writeable git repository, you probably want to run
2869'git cl land' instead.
2870If your project has a git mirror of an upstream SVN master, you probably need
2871to run 'git svn init'.
2872
2873Using the wrong command might cause your commit to appear to succeed, and the
2874review to be closed, without actually landing upstream. If you choose to
2875proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002876 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002877 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002878 return SendUpstream(parser, args, 'dcommit')
2879
2880
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002881@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002882def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002883 """Commits the current changelist via git."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002884 if settings.GetIsGitSvn() or get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002885 print('This appears to be an SVN repository.')
2886 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002887 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00002888 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002889 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002890
2891
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00002892def ParseIssueNum(arg):
2893 """Parses the issue number from args if present otherwise returns None."""
2894 if re.match(r'\d+', arg):
2895 return arg
2896 if arg.startswith('http'):
2897 return re.sub(r'.*/(\d+)/?', r'\1', arg)
2898 return None
2899
2900
2901@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002902def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002903 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002904 parser.add_option('-b', dest='newbranch',
2905 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002906 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002907 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002908 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2909 help='Change to the directory DIR immediately, '
2910 'before doing anything else.')
2911 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002912 help='failed patches spew .rej files rather than '
2913 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002914 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2915 help="don't commit after patch applies")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002916 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002917 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002918 auth_config = auth.extract_auth_config_from_options(options)
2919
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002920 if len(args) != 1:
2921 parser.print_help()
2922 return 1
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00002923
2924 issue_arg = ParseIssueNum(args[0])
2925 # The patch URL works because ParseIssueNum won't do any substitution
2926 # as the re.sub pattern fails to match and just returns it.
2927 if issue_arg == None:
2928 parser.print_help()
2929 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002930
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002931 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002932 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002933 return 1
2934
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002935 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002936 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002937
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002938 if options.newbranch:
2939 if options.force:
2940 RunGit(['branch', '-D', options.newbranch],
2941 stderr=subprocess2.PIPE, error_ok=True)
2942 RunGit(['checkout', '-b', options.newbranch,
2943 Changelist().GetUpstreamBranch()])
2944
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002945 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002946 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002947
2948
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002949def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00002950 # PatchIssue should never be called with a dirty tree. It is up to the
2951 # caller to check this, but just in case we assert here since the
2952 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002953 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002954
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002955 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002956 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002957 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002958 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002959 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002960 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002961 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002962 # Assume it's a URL to the patch. Default to https.
2963 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00002964 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002965 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002966 DieWithError('Must pass an issue ID or full URL for '
2967 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00002968 issue = int(match.group(2))
2969 cl = Changelist(issue=issue, auth_config=auth_config)
2970 cl.rietveld_server = match.group(1)
2971 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002972 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002973
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002974 # Switch up to the top-level directory, if necessary, in preparation for
2975 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002976 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002977 if top:
2978 os.chdir(top)
2979
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002980 # Git patches have a/ at the beginning of source paths. We strip that out
2981 # with a sed script rather than the -p flag to patch so we can feed either
2982 # Git or svn-style patches into the same apply command.
2983 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002984 try:
2985 patch_data = subprocess2.check_output(
2986 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2987 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002988 DieWithError('Git patch mungling failed.')
2989 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002990
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002991 # We use "git apply" to apply the patch instead of "patch" so that we can
2992 # pick up file adds.
2993 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002994 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002995 if directory:
2996 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002997 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002998 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002999 elif IsGitVersionAtLeast('1.7.12'):
3000 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003001 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003002 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003003 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003004 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003005 print 'Failed to apply the patch'
3006 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003007
3008 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003009 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003010 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3011 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003012 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3013 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003014 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003015 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003016 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003017 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003018 else:
3019 print "Patch applied to index."
3020 return 0
3021
3022
3023def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003024 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003025 # Provide a wrapper for git svn rebase to help avoid accidental
3026 # git svn dcommit.
3027 # It's the only command that doesn't use parser at all since we just defer
3028 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003029
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003030 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003031
3032
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003033def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003034 """Fetches the tree status and returns either 'open', 'closed',
3035 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003036 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003037 if url:
3038 status = urllib2.urlopen(url).read().lower()
3039 if status.find('closed') != -1 or status == '0':
3040 return 'closed'
3041 elif status.find('open') != -1 or status == '1':
3042 return 'open'
3043 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003044 return 'unset'
3045
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003046
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003047def GetTreeStatusReason():
3048 """Fetches the tree status from a json url and returns the message
3049 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003050 url = settings.GetTreeStatusUrl()
3051 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003052 connection = urllib2.urlopen(json_url)
3053 status = json.loads(connection.read())
3054 connection.close()
3055 return status['message']
3056
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003057
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003058def GetBuilderMaster(bot_list):
3059 """For a given builder, fetch the master from AE if available."""
3060 map_url = 'https://builders-map.appspot.com/'
3061 try:
3062 master_map = json.load(urllib2.urlopen(map_url))
3063 except urllib2.URLError as e:
3064 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3065 (map_url, e))
3066 except ValueError as e:
3067 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3068 if not master_map:
3069 return None, 'Failed to build master map.'
3070
3071 result_master = ''
3072 for bot in bot_list:
3073 builder = bot.split(':', 1)[0]
3074 master_list = master_map.get(builder, [])
3075 if not master_list:
3076 return None, ('No matching master for builder %s.' % builder)
3077 elif len(master_list) > 1:
3078 return None, ('The builder name %s exists in multiple masters %s.' %
3079 (builder, master_list))
3080 else:
3081 cur_master = master_list[0]
3082 if not result_master:
3083 result_master = cur_master
3084 elif result_master != cur_master:
3085 return None, 'The builders do not belong to the same master.'
3086 return result_master, None
3087
3088
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003089def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003090 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003091 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003092 status = GetTreeStatus()
3093 if 'unset' == status:
3094 print 'You must configure your tree status URL by running "git cl config".'
3095 return 2
3096
3097 print "The tree is %s" % status
3098 print
3099 print GetTreeStatusReason()
3100 if status != 'open':
3101 return 1
3102 return 0
3103
3104
maruel@chromium.org15192402012-09-06 12:38:29 +00003105def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003106 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003107 group = optparse.OptionGroup(parser, "Try job options")
3108 group.add_option(
3109 "-b", "--bot", action="append",
3110 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3111 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003112 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003113 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003114 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003115 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003116 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003117 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003118 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003119 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003120 "-r", "--revision",
3121 help="Revision to use for the try job; default: the "
3122 "revision will be determined by the try server; see "
3123 "its waterfall for more info")
3124 group.add_option(
3125 "-c", "--clobber", action="store_true", default=False,
3126 help="Force a clobber before building; e.g. don't do an "
3127 "incremental build")
3128 group.add_option(
3129 "--project",
3130 help="Override which project to use. Projects are defined "
3131 "server-side to define what default bot set to use")
3132 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003133 "-p", "--property", dest="properties", action="append", default=[],
3134 help="Specify generic properties in the form -p key1=value1 -p "
3135 "key2=value2 etc (buildbucket only). The value will be treated as "
3136 "json if decodable, or as string otherwise.")
3137 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003138 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003139 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003140 "--use-rietveld", action="store_true", default=False,
3141 help="Use Rietveld to trigger try jobs.")
3142 group.add_option(
3143 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3144 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003145 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003146 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003147 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003148 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003149
machenbach@chromium.org45453142015-09-15 08:45:22 +00003150 if options.use_rietveld and options.properties:
3151 parser.error('Properties can only be specified with buildbucket')
3152
3153 # Make sure that all properties are prop=value pairs.
3154 bad_params = [x for x in options.properties if '=' not in x]
3155 if bad_params:
3156 parser.error('Got properties with missing "=": %s' % bad_params)
3157
maruel@chromium.org15192402012-09-06 12:38:29 +00003158 if args:
3159 parser.error('Unknown arguments: %s' % args)
3160
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003161 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003162 if not cl.GetIssue():
3163 parser.error('Need to upload first')
3164
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003165 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003166 if props.get('closed'):
3167 parser.error('Cannot send tryjobs for a closed CL')
3168
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003169 if props.get('private'):
3170 parser.error('Cannot use trybots with private issue')
3171
maruel@chromium.org15192402012-09-06 12:38:29 +00003172 if not options.name:
3173 options.name = cl.GetBranch()
3174
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003175 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003176 options.master, err_msg = GetBuilderMaster(options.bot)
3177 if err_msg:
3178 parser.error('Tryserver master cannot be found because: %s\n'
3179 'Please manually specify the tryserver master'
3180 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003181
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003182 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003183 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003184 if not options.bot:
3185 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003186
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003187 # Get try masters from PRESUBMIT.py files.
3188 masters = presubmit_support.DoGetTryMasters(
3189 change,
3190 change.LocalPaths(),
3191 settings.GetRoot(),
3192 None,
3193 None,
3194 options.verbose,
3195 sys.stdout)
3196 if masters:
3197 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003198
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003199 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3200 options.bot = presubmit_support.DoGetTrySlaves(
3201 change,
3202 change.LocalPaths(),
3203 settings.GetRoot(),
3204 None,
3205 None,
3206 options.verbose,
3207 sys.stdout)
3208 if not options.bot:
3209 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003210
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003211 builders_and_tests = {}
3212 # TODO(machenbach): The old style command-line options don't support
3213 # multiple try masters yet.
3214 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3215 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3216
3217 for bot in old_style:
3218 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003219 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003220 elif ',' in bot:
3221 parser.error('Specify one bot per --bot flag')
3222 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003223 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003224
3225 for bot, tests in new_style:
3226 builders_and_tests.setdefault(bot, []).extend(tests)
3227
3228 # Return a master map with one master to be backwards compatible. The
3229 # master name defaults to an empty string, which will cause the master
3230 # not to be set on rietveld (deprecated).
3231 return {options.master: builders_and_tests}
3232
3233 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003234
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003235 for builders in masters.itervalues():
3236 if any('triggered' in b for b in builders):
3237 print >> sys.stderr, (
3238 'ERROR You are trying to send a job to a triggered bot. This type of'
3239 ' bot requires an\ninitial job from a parent (usually a builder). '
3240 'Instead send your job to the parent.\n'
3241 'Bot list: %s' % builders)
3242 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003243
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003244 patchset = cl.GetMostRecentPatchset()
3245 if patchset and patchset != cl.GetPatchset():
3246 print(
3247 '\nWARNING Mismatch between local config and server. Did a previous '
3248 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3249 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003250 if options.luci:
3251 trigger_luci_job(cl, masters, options)
3252 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003253 try:
3254 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3255 except BuildbucketResponseException as ex:
3256 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003257 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003258 except Exception as e:
3259 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3260 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3261 e, stacktrace)
3262 return 1
3263 else:
3264 try:
3265 cl.RpcServer().trigger_distributed_try_jobs(
3266 cl.GetIssue(), patchset, options.name, options.clobber,
3267 options.revision, masters)
3268 except urllib2.HTTPError as e:
3269 if e.code == 404:
3270 print('404 from rietveld; '
3271 'did you mean to use "git try" instead of "git cl try"?')
3272 return 1
3273 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003274
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003275 for (master, builders) in sorted(masters.iteritems()):
3276 if master:
3277 print 'Master: %s' % master
3278 length = max(len(builder) for builder in builders)
3279 for builder in sorted(builders):
3280 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003281 return 0
3282
3283
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003284@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003285def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003286 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003287 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003288 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003289 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003290
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003291 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003292 if args:
3293 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003294 branch = cl.GetBranch()
3295 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003296 cl = Changelist()
3297 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003298
3299 # Clear configured merge-base, if there is one.
3300 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003301 else:
3302 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003303 return 0
3304
3305
thestig@chromium.org00858c82013-12-02 23:08:03 +00003306def CMDweb(parser, args):
3307 """Opens the current CL in the web browser."""
3308 _, args = parser.parse_args(args)
3309 if args:
3310 parser.error('Unrecognized args: %s' % ' '.join(args))
3311
3312 issue_url = Changelist().GetIssueURL()
3313 if not issue_url:
3314 print >> sys.stderr, 'ERROR No issue to open'
3315 return 1
3316
3317 webbrowser.open(issue_url)
3318 return 0
3319
3320
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003321def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003322 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003323 auth.add_auth_options(parser)
3324 options, args = parser.parse_args(args)
3325 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003326 if args:
3327 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003328 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003329 props = cl.GetIssueProperties()
3330 if props.get('private'):
3331 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003332 cl.SetFlag('commit', '1')
3333 return 0
3334
3335
groby@chromium.org411034a2013-02-26 15:12:01 +00003336def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003337 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003338 auth.add_auth_options(parser)
3339 options, args = parser.parse_args(args)
3340 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003341 if args:
3342 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003343 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003344 # Ensure there actually is an issue to close.
3345 cl.GetDescription()
3346 cl.CloseIssue()
3347 return 0
3348
3349
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003350def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003351 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003352 auth.add_auth_options(parser)
3353 options, args = parser.parse_args(args)
3354 auth_config = auth.extract_auth_config_from_options(options)
3355 if args:
3356 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003357
3358 # Uncommitted (staged and unstaged) changes will be destroyed by
3359 # "git reset --hard" if there are merging conflicts in PatchIssue().
3360 # Staged changes would be committed along with the patch from last
3361 # upload, hence counted toward the "last upload" side in the final
3362 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003363 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003364 return 1
3365
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003366 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003367 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003368 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003369 if not issue:
3370 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003371 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003372 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003373
3374 # Create a new branch based on the merge-base
3375 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3376 try:
3377 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003378 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003379 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003380 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003381 return rtn
3382
wychen@chromium.org06928532015-02-03 02:11:29 +00003383 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003384 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003385 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003386 finally:
3387 RunGit(['checkout', '-q', branch])
3388 RunGit(['branch', '-D', TMP_BRANCH])
3389
3390 return 0
3391
3392
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003393def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003394 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003395 parser.add_option(
3396 '--no-color',
3397 action='store_true',
3398 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003399 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003400 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003401 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003402
3403 author = RunGit(['config', 'user.email']).strip() or None
3404
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003405 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003406
3407 if args:
3408 if len(args) > 1:
3409 parser.error('Unknown args')
3410 base_branch = args[0]
3411 else:
3412 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003413 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003414
3415 change = cl.GetChange(base_branch, None)
3416 return owners_finder.OwnersFinder(
3417 [f.LocalPath() for f in
3418 cl.GetChange(base_branch, None).AffectedFiles()],
3419 change.RepositoryRoot(), author,
3420 fopen=file, os_path=os.path, glob=glob.glob,
3421 disable_color=options.no_color).run()
3422
3423
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003424def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
3425 """Generates a diff command."""
3426 # Generate diff for the current branch's changes.
3427 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3428 upstream_commit, '--' ]
3429
3430 if args:
3431 for arg in args:
3432 if os.path.isdir(arg):
3433 diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
3434 elif os.path.isfile(arg):
3435 diff_cmd.append(arg)
3436 else:
3437 DieWithError('Argument "%s" is not a file or a directory' % arg)
3438 else:
3439 diff_cmd.extend('*' + ext for ext in extensions)
3440
3441 return diff_cmd
3442
3443
enne@chromium.org555cfe42014-01-29 18:21:39 +00003444@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003445def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003446 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003447 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003448 parser.add_option('--full', action='store_true',
3449 help='Reformat the full content of all touched files')
3450 parser.add_option('--dry-run', action='store_true',
3451 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003452 parser.add_option('--python', action='store_true',
3453 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003454 parser.add_option('--diff', action='store_true',
3455 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003456 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003457
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003458 # git diff generates paths against the root of the repository. Change
3459 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003460 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003461 if rel_base_path:
3462 os.chdir(rel_base_path)
3463
digit@chromium.org29e47272013-05-17 17:01:46 +00003464 # Grab the merge-base commit, i.e. the upstream commit of the current
3465 # branch when it was created or the last time it was rebased. This is
3466 # to cover the case where the user may have called "git fetch origin",
3467 # moving the origin branch to a newer commit, but hasn't rebased yet.
3468 upstream_commit = None
3469 cl = Changelist()
3470 upstream_branch = cl.GetUpstreamBranch()
3471 if upstream_branch:
3472 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3473 upstream_commit = upstream_commit.strip()
3474
3475 if not upstream_commit:
3476 DieWithError('Could not find base commit for this branch. '
3477 'Are you in detached state?')
3478
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003479 if opts.full:
3480 # Only list the names of modified files.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003481 diff_type = '--name-only'
enne@chromium.org555cfe42014-01-29 18:21:39 +00003482 else:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003483 # Only generate context-less patches.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003484 diff_type = '-U0'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003485
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003486 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, CLANG_EXTS)
digit@chromium.org29e47272013-05-17 17:01:46 +00003487 diff_output = RunGit(diff_cmd)
3488
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003489 top_dir = os.path.normpath(
3490 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3491
3492 # Locate the clang-format binary in the checkout
3493 try:
3494 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3495 except clang_format.NotFoundError, e:
3496 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003497
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003498 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3499 # formatted. This is used to block during the presubmit.
3500 return_value = 0
3501
digit@chromium.org29e47272013-05-17 17:01:46 +00003502 if opts.full:
3503 # diff_output is a list of files to send to clang-format.
3504 files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003505 if files:
3506 cmd = [clang_format_tool]
3507 if not opts.dry_run and not opts.diff:
3508 cmd.append('-i')
3509 stdout = RunCommand(cmd + files, cwd=top_dir)
3510 if opts.diff:
3511 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003512 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003513 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003514 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00003515 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003516 try:
3517 script = clang_format.FindClangFormatScriptInChromiumTree(
3518 'clang-format-diff.py')
3519 except clang_format.NotFoundError, e:
3520 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003521
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003522 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003523 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003524 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003525
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003526 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003527 if opts.diff:
3528 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003529 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003530 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003531
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003532 # Similar code to above, but using yapf on .py files rather than clang-format
3533 # on C/C++ files
3534 if opts.python:
3535 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, ['.py'])
3536 diff_output = RunGit(diff_cmd)
3537 yapf_tool = gclient_utils.FindExecutable('yapf')
3538 if yapf_tool is None:
3539 DieWithError('yapf not found in PATH')
3540
3541 if opts.full:
3542 files = diff_output.splitlines()
3543 if files:
3544 cmd = [yapf_tool]
3545 if not opts.dry_run and not opts.diff:
3546 cmd.append('-i')
3547 stdout = RunCommand(cmd + files, cwd=top_dir)
3548 if opts.diff:
3549 sys.stdout.write(stdout)
3550 else:
3551 # TODO(sbc): yapf --lines mode still has some issues.
3552 # https://github.com/google/yapf/issues/154
3553 DieWithError('--python currently only works with --full')
3554
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003555 # Build a diff command that only operates on dart files. dart's formatter
3556 # does not have the nice property of only operating on modified chunks, so
3557 # hard code full.
3558 dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3559 args, ['.dart'])
3560 dart_diff_output = RunGit(dart_diff_cmd)
3561 if dart_diff_output:
3562 try:
3563 command = [dart_format.FindDartFmtToolInChromiumTree()]
3564 if not opts.dry_run and not opts.diff:
3565 command.append('-w')
3566 command.extend(dart_diff_output.splitlines())
3567
3568 stdout = RunCommand(command, cwd=top_dir, env=env)
3569 if opts.dry_run and stdout:
3570 return_value = 2
3571 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00003572 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
3573 'found in this checkout. Files in other languages are still ' +
3574 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003575
3576 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003577
3578
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003579@subcommand.usage('<codereview url or issue id>')
3580def CMDcheckout(parser, args):
3581 """Checks out a branch associated with a given Rietveld issue."""
3582 _, args = parser.parse_args(args)
3583
3584 if len(args) != 1:
3585 parser.print_help()
3586 return 1
3587
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003588 target_issue = ParseIssueNum(args[0])
3589 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003590 parser.print_help()
3591 return 1
3592
3593 key_and_issues = [x.split() for x in RunGit(
3594 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
3595 .splitlines()]
3596 branches = []
3597 for key, issue in key_and_issues:
3598 if issue == target_issue:
3599 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
3600
3601 if len(branches) == 0:
3602 print 'No branch found for issue %s.' % target_issue
3603 return 1
3604 if len(branches) == 1:
3605 RunGit(['checkout', branches[0]])
3606 else:
3607 print 'Multiple branches match issue %s:' % target_issue
3608 for i in range(len(branches)):
3609 print '%d: %s' % (i, branches[i])
3610 which = raw_input('Choose by index: ')
3611 try:
3612 RunGit(['checkout', branches[int(which)]])
3613 except (IndexError, ValueError):
3614 print 'Invalid selection, not checking out any branch.'
3615 return 1
3616
3617 return 0
3618
3619
maruel@chromium.org29404b52014-09-08 22:58:00 +00003620def CMDlol(parser, args):
3621 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003622 print zlib.decompress(base64.b64decode(
3623 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3624 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3625 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3626 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003627 return 0
3628
3629
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003630class OptionParser(optparse.OptionParser):
3631 """Creates the option parse and add --verbose support."""
3632 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003633 optparse.OptionParser.__init__(
3634 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003635 self.add_option(
3636 '-v', '--verbose', action='count', default=0,
3637 help='Use 2 times for more debugging info')
3638
3639 def parse_args(self, args=None, values=None):
3640 options, args = optparse.OptionParser.parse_args(self, args, values)
3641 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3642 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3643 return options, args
3644
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003645
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003646def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003647 if sys.hexversion < 0x02060000:
3648 print >> sys.stderr, (
3649 '\nYour python version %s is unsupported, please upgrade.\n' %
3650 sys.version.split(' ', 1)[0])
3651 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003652
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003653 # Reload settings.
3654 global settings
3655 settings = Settings()
3656
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003657 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003658 dispatcher = subcommand.CommandDispatcher(__name__)
3659 try:
3660 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003661 except auth.AuthenticationError as e:
3662 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003663 except urllib2.HTTPError, e:
3664 if e.code != 500:
3665 raise
3666 DieWithError(
3667 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3668 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003669 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003670
3671
3672if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003673 # These affect sys.stdout so do it outside of main() to simplify mocks in
3674 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003675 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003676 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003677 try:
3678 sys.exit(main(sys.argv[1:]))
3679 except KeyboardInterrupt:
3680 sys.stderr.write('interrupted\n')
3681 sys.exit(1)