blob: e99509f1954776fc31fdf2996b0d18b7a9b234eb [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
tandrii@chromium.org71184c02016-01-13 15:18:44 +000045import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000046import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000047import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000048import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000049import git_common
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +000050from git_footers import get_footer_svn_id
piman@chromium.org336f9122014-09-04 02:16:55 +000051import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000053import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000054import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000055import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000056import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000057import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import watchlists
59
maruel@chromium.org0633fb42013-08-16 20:06:14 +000060__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000062DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000063POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000064DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000065GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000066CHANGE_ID = 'Change-Id:'
rmistry@google.comc68112d2015-03-03 12:48:06 +000067REFS_THAT_ALIAS_TO_OTHER_REFS = {
68 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
69 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
70}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000071
thestig@chromium.org44202a22014-03-11 19:22:18 +000072# Valid extensions for files we want to lint.
73DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
74DEFAULT_LINT_IGNORE_REGEX = r"$^"
75
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000076# Shortcut since it quickly becomes redundant.
77Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000078
maruel@chromium.orgddd59412011-11-30 14:20:38 +000079# Initialized in main()
80settings = None
81
82
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000083def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000084 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000085 sys.exit(1)
86
87
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000088def GetNoGitPagerEnv():
89 env = os.environ.copy()
90 # 'cat' is a magical git string that disables pagers on all platforms.
91 env['GIT_PAGER'] = 'cat'
92 return env
93
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000094
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000095def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000096 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000097 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000098 except subprocess2.CalledProcessError as e:
99 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000100 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000101 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000102 'Command "%s" failed.\n%s' % (
103 ' '.join(args), error_message or e.stdout or ''))
104 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000105
106
107def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000108 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000109 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000110
111
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000112def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000113 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000114 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115 if suppress_stderr:
116 stderr = subprocess2.VOID
117 else:
118 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000119 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000120 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000121 stdout=subprocess2.PIPE,
122 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000123 return code, out[0]
124 except ValueError:
125 # When the subprocess fails, it returns None. That triggers a ValueError
126 # when trying to unpack the return value into (out, code).
127 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000128
129
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000130def RunGitSilent(args):
131 """Returns stdout, suppresses stderr and ingores the return code."""
132 return RunGitWithCode(args, suppress_stderr=True)[1]
133
134
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000135def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000136 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000137 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000138 return (version.startswith(prefix) and
139 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140
141
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000142def BranchExists(branch):
143 """Return True if specified branch exists."""
144 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
145 suppress_stderr=True)
146 return not code
147
148
maruel@chromium.org90541732011-04-01 17:54:18 +0000149def ask_for_data(prompt):
150 try:
151 return raw_input(prompt)
152 except KeyboardInterrupt:
153 # Hide the exception.
154 sys.exit(1)
155
156
iannucci@chromium.org79540052012-10-19 23:15:26 +0000157def git_set_branch_value(key, value):
158 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000159 if not branch:
160 return
161
162 cmd = ['config']
163 if isinstance(value, int):
164 cmd.append('--int')
165 git_key = 'branch.%s.%s' % (branch, key)
166 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000167
168
169def git_get_branch_default(key, default):
170 branch = Changelist().GetBranch()
171 if branch:
172 git_key = 'branch.%s.%s' % (branch, key)
173 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
174 try:
175 return int(stdout.strip())
176 except ValueError:
177 pass
178 return default
179
180
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000181def add_git_similarity(parser):
182 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000183 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000184 help='Sets the percentage that a pair of files need to match in order to'
185 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000186 parser.add_option(
187 '--find-copies', action='store_true',
188 help='Allows git to look for copies.')
189 parser.add_option(
190 '--no-find-copies', action='store_false', dest='find_copies',
191 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000192
193 old_parser_args = parser.parse_args
194 def Parse(args):
195 options, args = old_parser_args(args)
196
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000197 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000198 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000199 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000200 print('Note: Saving similarity of %d%% in git config.'
201 % options.similarity)
202 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000203
iannucci@chromium.org79540052012-10-19 23:15:26 +0000204 options.similarity = max(0, min(options.similarity, 100))
205
206 if options.find_copies is None:
207 options.find_copies = bool(
208 git_get_branch_default('git-find-copies', True))
209 else:
210 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000211
212 print('Using %d%% similarity for rename/copy detection. '
213 'Override with --similarity.' % options.similarity)
214
215 return options, args
216 parser.parse_args = Parse
217
218
machenbach@chromium.org45453142015-09-15 08:45:22 +0000219def _get_properties_from_options(options):
220 properties = dict(x.split('=', 1) for x in options.properties)
221 for key, val in properties.iteritems():
222 try:
223 properties[key] = json.loads(val)
224 except ValueError:
225 pass # If a value couldn't be evaluated, treat it as a string.
226 return properties
227
228
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000229def _prefix_master(master):
230 """Convert user-specified master name to full master name.
231
232 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
233 name, while the developers always use shortened master name
234 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
235 function does the conversion for buildbucket migration.
236 """
237 prefix = 'master.'
238 if master.startswith(prefix):
239 return master
240 return '%s%s' % (prefix, master)
241
242
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000243def trigger_luci_job(changelist, masters, options):
244 """Send a job to run on LUCI."""
245 issue_props = changelist.GetIssueProperties()
246 issue = changelist.GetIssue()
247 patchset = changelist.GetMostRecentPatchset()
248 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000249 # TODO(hinoka et al): add support for other properties.
250 # Currently, this completely ignores testfilter and other properties.
251 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000252 luci_trigger.trigger(
253 builder, 'HEAD', issue, patchset, issue_props['project'])
254
255
machenbach@chromium.org45453142015-09-15 08:45:22 +0000256def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000257 rietveld_url = settings.GetDefaultServerUrl()
258 rietveld_host = urlparse.urlparse(rietveld_url).hostname
259 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
260 http = authenticator.authorize(httplib2.Http())
261 http.force_exception_to_status_code = True
262 issue_props = changelist.GetIssueProperties()
263 issue = changelist.GetIssue()
264 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000265 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000266
267 buildbucket_put_url = (
268 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000269 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000270 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
271 hostname=rietveld_host,
272 issue=issue,
273 patch=patchset)
274
275 batch_req_body = {'builds': []}
276 print_text = []
277 print_text.append('Tried jobs on:')
278 for master, builders_and_tests in sorted(masters.iteritems()):
279 print_text.append('Master: %s' % master)
280 bucket = _prefix_master(master)
281 for builder, tests in sorted(builders_and_tests.iteritems()):
282 print_text.append(' %s: %s' % (builder, tests))
283 parameters = {
284 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000285 'changes': [{
286 'author': {'email': issue_props['owner_email']},
287 'revision': options.revision,
288 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000289 'properties': {
290 'category': category,
291 'issue': issue,
292 'master': master,
293 'patch_project': issue_props['project'],
294 'patch_storage': 'rietveld',
295 'patchset': patchset,
296 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000297 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000298 },
299 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000300 if tests:
301 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000302 if properties:
303 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000304 if options.clobber:
305 parameters['properties']['clobber'] = True
306 batch_req_body['builds'].append(
307 {
308 'bucket': bucket,
309 'parameters_json': json.dumps(parameters),
310 'tags': ['builder:%s' % builder,
311 'buildset:%s' % buildset,
312 'master:%s' % master,
313 'user_agent:git_cl_try']
314 }
315 )
316
317 for try_count in xrange(3):
318 response, content = http.request(
319 buildbucket_put_url,
320 'PUT',
321 body=json.dumps(batch_req_body),
322 headers={'Content-Type': 'application/json'},
323 )
324 content_json = None
325 try:
326 content_json = json.loads(content)
327 except ValueError:
328 pass
329
330 # Buildbucket could return an error even if status==200.
331 if content_json and content_json.get('error'):
332 msg = 'Error in response. Code: %d. Reason: %s. Message: %s.' % (
333 content_json['error'].get('code', ''),
334 content_json['error'].get('reason', ''),
335 content_json['error'].get('message', ''))
336 raise BuildbucketResponseException(msg)
337
338 if response.status == 200:
339 if not content_json:
340 raise BuildbucketResponseException(
341 'Buildbucket returns invalid json content: %s.\n'
342 'Please file bugs at crbug.com, label "Infra-BuildBucket".' %
343 content)
344 break
345 if response.status < 500 or try_count >= 2:
346 raise httplib2.HttpLib2Error(content)
347
348 # status >= 500 means transient failures.
349 logging.debug('Transient errors when triggering tryjobs. Will retry.')
350 time.sleep(0.5 + 1.5*try_count)
351
352 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000353
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000354
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000355def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
356 """Return the corresponding git ref if |base_url| together with |glob_spec|
357 matches the full |url|.
358
359 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
360 """
361 fetch_suburl, as_ref = glob_spec.split(':')
362 if allow_wildcards:
363 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
364 if glob_match:
365 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
366 # "branches/{472,597,648}/src:refs/remotes/svn/*".
367 branch_re = re.escape(base_url)
368 if glob_match.group(1):
369 branch_re += '/' + re.escape(glob_match.group(1))
370 wildcard = glob_match.group(2)
371 if wildcard == '*':
372 branch_re += '([^/]*)'
373 else:
374 # Escape and replace surrounding braces with parentheses and commas
375 # with pipe symbols.
376 wildcard = re.escape(wildcard)
377 wildcard = re.sub('^\\\\{', '(', wildcard)
378 wildcard = re.sub('\\\\,', '|', wildcard)
379 wildcard = re.sub('\\\\}$', ')', wildcard)
380 branch_re += wildcard
381 if glob_match.group(3):
382 branch_re += re.escape(glob_match.group(3))
383 match = re.match(branch_re, url)
384 if match:
385 return re.sub('\*$', match.group(1), as_ref)
386
387 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
388 if fetch_suburl:
389 full_url = base_url + '/' + fetch_suburl
390 else:
391 full_url = base_url
392 if full_url == url:
393 return as_ref
394 return None
395
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000396
iannucci@chromium.org79540052012-10-19 23:15:26 +0000397def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000398 """Prints statistics about the change to the user."""
399 # --no-ext-diff is broken in some versions of Git, so try to work around
400 # this by overriding the environment (but there is still a problem if the
401 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000402 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000403 if 'GIT_EXTERNAL_DIFF' in env:
404 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000405
406 if find_copies:
407 similarity_options = ['--find-copies-harder', '-l100000',
408 '-C%s' % similarity]
409 else:
410 similarity_options = ['-M%s' % similarity]
411
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000412 try:
413 stdout = sys.stdout.fileno()
414 except AttributeError:
415 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000416 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000417 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000418 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000419 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000420
421
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000422class BuildbucketResponseException(Exception):
423 pass
424
425
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000426class Settings(object):
427 def __init__(self):
428 self.default_server = None
429 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000430 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000431 self.is_git_svn = None
432 self.svn_branch = None
433 self.tree_status_url = None
434 self.viewvc_url = None
435 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000436 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000437 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000438 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000439 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000440 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000441
442 def LazyUpdateIfNeeded(self):
443 """Updates the settings from a codereview.settings file, if available."""
444 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000445 # The only value that actually changes the behavior is
446 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000447 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000448 error_ok=True
449 ).strip().lower()
450
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000451 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000452 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000453 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000454 # set updated to True to avoid infinite calling loop
455 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000456 self.updated = True
457 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000458 self.updated = True
459
460 def GetDefaultServerUrl(self, error_ok=False):
461 if not self.default_server:
462 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000463 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000464 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000465 if error_ok:
466 return self.default_server
467 if not self.default_server:
468 error_message = ('Could not find settings file. You must configure '
469 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000470 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000471 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000472 return self.default_server
473
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000474 @staticmethod
475 def GetRelativeRoot():
476 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000477
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000478 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000479 if self.root is None:
480 self.root = os.path.abspath(self.GetRelativeRoot())
481 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000482
483 def GetIsGitSvn(self):
484 """Return true if this repo looks like it's using git-svn."""
485 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000486 if self.GetPendingRefPrefix():
487 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
488 self.is_git_svn = False
489 else:
490 # If you have any "svn-remote.*" config keys, we think you're using svn.
491 self.is_git_svn = RunGitWithCode(
492 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000493 return self.is_git_svn
494
495 def GetSVNBranch(self):
496 if self.svn_branch is None:
497 if not self.GetIsGitSvn():
498 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
499
500 # Try to figure out which remote branch we're based on.
501 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000502 # 1) iterate through our branch history and find the svn URL.
503 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000504
505 # regexp matching the git-svn line that contains the URL.
506 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
507
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000508 # We don't want to go through all of history, so read a line from the
509 # pipe at a time.
510 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000511 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000512 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
513 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000514 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000515 for line in proc.stdout:
516 match = git_svn_re.match(line)
517 if match:
518 url = match.group(1)
519 proc.stdout.close() # Cut pipe.
520 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000521
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000522 if url:
523 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
524 remotes = RunGit(['config', '--get-regexp',
525 r'^svn-remote\..*\.url']).splitlines()
526 for remote in remotes:
527 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000528 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000529 remote = match.group(1)
530 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000531 rewrite_root = RunGit(
532 ['config', 'svn-remote.%s.rewriteRoot' % remote],
533 error_ok=True).strip()
534 if rewrite_root:
535 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000536 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000537 ['config', 'svn-remote.%s.fetch' % remote],
538 error_ok=True).strip()
539 if fetch_spec:
540 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
541 if self.svn_branch:
542 break
543 branch_spec = RunGit(
544 ['config', 'svn-remote.%s.branches' % remote],
545 error_ok=True).strip()
546 if branch_spec:
547 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
548 if self.svn_branch:
549 break
550 tag_spec = RunGit(
551 ['config', 'svn-remote.%s.tags' % remote],
552 error_ok=True).strip()
553 if tag_spec:
554 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
555 if self.svn_branch:
556 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000557
558 if not self.svn_branch:
559 DieWithError('Can\'t guess svn branch -- try specifying it on the '
560 'command line')
561
562 return self.svn_branch
563
564 def GetTreeStatusUrl(self, error_ok=False):
565 if not self.tree_status_url:
566 error_message = ('You must configure your tree status URL by running '
567 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000568 self.tree_status_url = self._GetRietveldConfig(
569 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000570 return self.tree_status_url
571
572 def GetViewVCUrl(self):
573 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000574 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000575 return self.viewvc_url
576
rmistry@google.com90752582014-01-14 21:04:50 +0000577 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000578 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000579
rmistry@google.com78948ed2015-07-08 23:09:57 +0000580 def GetIsSkipDependencyUpload(self, branch_name):
581 """Returns true if specified branch should skip dep uploads."""
582 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
583 error_ok=True)
584
rmistry@google.com5626a922015-02-26 14:03:30 +0000585 def GetRunPostUploadHook(self):
586 run_post_upload_hook = self._GetRietveldConfig(
587 'run-post-upload-hook', error_ok=True)
588 return run_post_upload_hook == "True"
589
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000590 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000591 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000592
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000593 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000594 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000595
ukai@chromium.orge8077812012-02-03 03:41:46 +0000596 def GetIsGerrit(self):
597 """Return true if this repo is assosiated with gerrit code review system."""
598 if self.is_gerrit is None:
599 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
600 return self.is_gerrit
601
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000602 def GetGitEditor(self):
603 """Return the editor specified in the git config, or None if none is."""
604 if self.git_editor is None:
605 self.git_editor = self._GetConfig('core.editor', error_ok=True)
606 return self.git_editor or None
607
thestig@chromium.org44202a22014-03-11 19:22:18 +0000608 def GetLintRegex(self):
609 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
610 DEFAULT_LINT_REGEX)
611
612 def GetLintIgnoreRegex(self):
613 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
614 DEFAULT_LINT_IGNORE_REGEX)
615
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000616 def GetProject(self):
617 if not self.project:
618 self.project = self._GetRietveldConfig('project', error_ok=True)
619 return self.project
620
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000621 def GetForceHttpsCommitUrl(self):
622 if not self.force_https_commit_url:
623 self.force_https_commit_url = self._GetRietveldConfig(
624 'force-https-commit-url', error_ok=True)
625 return self.force_https_commit_url
626
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000627 def GetPendingRefPrefix(self):
628 if not self.pending_ref_prefix:
629 self.pending_ref_prefix = self._GetRietveldConfig(
630 'pending-ref-prefix', error_ok=True)
631 return self.pending_ref_prefix
632
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000633 def _GetRietveldConfig(self, param, **kwargs):
634 return self._GetConfig('rietveld.' + param, **kwargs)
635
rmistry@google.com78948ed2015-07-08 23:09:57 +0000636 def _GetBranchConfig(self, branch_name, param, **kwargs):
637 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
638
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000639 def _GetConfig(self, param, **kwargs):
640 self.LazyUpdateIfNeeded()
641 return RunGit(['config', param], **kwargs).strip()
642
643
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000644def ShortBranchName(branch):
645 """Convert a name like 'refs/heads/foo' to just 'foo'."""
646 return branch.replace('refs/heads/', '')
647
648
649class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000650 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000651 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000652 global settings
653 if not settings:
654 # Happens when git_cl.py is used as a utility library.
655 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000656 settings.GetDefaultServerUrl()
657 self.branchref = branchref
658 if self.branchref:
659 self.branch = ShortBranchName(self.branchref)
660 else:
661 self.branch = None
662 self.rietveld_server = None
663 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000664 self.lookedup_issue = False
665 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000666 self.has_description = False
667 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000668 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000669 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000670 self.cc = None
671 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000672 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000673 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000674 self._remote = None
675 self._rpc_server = None
676
677 @property
678 def auth_config(self):
679 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000680
681 def GetCCList(self):
682 """Return the users cc'd on this CL.
683
684 Return is a string suitable for passing to gcl with the --cc flag.
685 """
686 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000687 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000688 more_cc = ','.join(self.watchers)
689 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
690 return self.cc
691
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000692 def GetCCListWithoutDefault(self):
693 """Return the users cc'd on this CL excluding default ones."""
694 if self.cc is None:
695 self.cc = ','.join(self.watchers)
696 return self.cc
697
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000698 def SetWatchers(self, watchers):
699 """Set the list of email addresses that should be cc'd based on the changed
700 files in this CL.
701 """
702 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000703
704 def GetBranch(self):
705 """Returns the short branch name, e.g. 'master'."""
706 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000707 branchref = RunGit(['symbolic-ref', 'HEAD'],
708 stderr=subprocess2.VOID, error_ok=True).strip()
709 if not branchref:
710 return None
711 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000712 self.branch = ShortBranchName(self.branchref)
713 return self.branch
714
715 def GetBranchRef(self):
716 """Returns the full branch name, e.g. 'refs/heads/master'."""
717 self.GetBranch() # Poke the lazy loader.
718 return self.branchref
719
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000720 @staticmethod
721 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000722 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000723 e.g. 'origin', 'refs/heads/master'
724 """
725 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000726 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
727 error_ok=True).strip()
728 if upstream_branch:
729 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
730 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000731 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
732 error_ok=True).strip()
733 if upstream_branch:
734 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000735 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000736 # Fall back on trying a git-svn upstream branch.
737 if settings.GetIsGitSvn():
738 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000740 # Else, try to guess the origin remote.
741 remote_branches = RunGit(['branch', '-r']).split()
742 if 'origin/master' in remote_branches:
743 # Fall back on origin/master if it exits.
744 remote = 'origin'
745 upstream_branch = 'refs/heads/master'
746 elif 'origin/trunk' in remote_branches:
747 # Fall back on origin/trunk if it exists. Generally a shared
748 # git-svn clone
749 remote = 'origin'
750 upstream_branch = 'refs/heads/trunk'
751 else:
752 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000753Either pass complete "git diff"-style arguments, like
754 git cl upload origin/master
755or verify this branch is set up to track another (via the --track argument to
756"git checkout -b ...").""")
757
758 return remote, upstream_branch
759
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000760 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000761 upstream_branch = self.GetUpstreamBranch()
762 if not BranchExists(upstream_branch):
763 DieWithError('The upstream for the current branch (%s) does not exist '
764 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000765 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000766 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000767
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000768 def GetUpstreamBranch(self):
769 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000770 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000771 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000772 upstream_branch = upstream_branch.replace('refs/heads/',
773 'refs/remotes/%s/' % remote)
774 upstream_branch = upstream_branch.replace('refs/branch-heads/',
775 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000776 self.upstream_branch = upstream_branch
777 return self.upstream_branch
778
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000779 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000780 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000781 remote, branch = None, self.GetBranch()
782 seen_branches = set()
783 while branch not in seen_branches:
784 seen_branches.add(branch)
785 remote, branch = self.FetchUpstreamTuple(branch)
786 branch = ShortBranchName(branch)
787 if remote != '.' or branch.startswith('refs/remotes'):
788 break
789 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000790 remotes = RunGit(['remote'], error_ok=True).split()
791 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000792 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000793 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000794 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000795 logging.warning('Could not determine which remote this change is '
796 'associated with, so defaulting to "%s". This may '
797 'not be what you want. You may prevent this message '
798 'by running "git svn info" as documented here: %s',
799 self._remote,
800 GIT_INSTRUCTIONS_URL)
801 else:
802 logging.warn('Could not determine which remote this change is '
803 'associated with. You may prevent this message by '
804 'running "git svn info" as documented here: %s',
805 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000806 branch = 'HEAD'
807 if branch.startswith('refs/remotes'):
808 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000809 elif branch.startswith('refs/branch-heads/'):
810 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000811 else:
812 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000813 return self._remote
814
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000815 def GitSanityChecks(self, upstream_git_obj):
816 """Checks git repo status and ensures diff is from local commits."""
817
sbc@chromium.org79706062015-01-14 21:18:12 +0000818 if upstream_git_obj is None:
819 if self.GetBranch() is None:
820 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +0000821 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +0000822 else:
823 print >> sys.stderr, (
824 'ERROR: no upstream branch')
825 return False
826
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000827 # Verify the commit we're diffing against is in our current branch.
828 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
829 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
830 if upstream_sha != common_ancestor:
831 print >> sys.stderr, (
832 'ERROR: %s is not in the current branch. You may need to rebase '
833 'your tracking branch' % upstream_sha)
834 return False
835
836 # List the commits inside the diff, and verify they are all local.
837 commits_in_diff = RunGit(
838 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
839 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
840 remote_branch = remote_branch.strip()
841 if code != 0:
842 _, remote_branch = self.GetRemoteBranch()
843
844 commits_in_remote = RunGit(
845 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
846
847 common_commits = set(commits_in_diff) & set(commits_in_remote)
848 if common_commits:
849 print >> sys.stderr, (
850 'ERROR: Your diff contains %d commits already in %s.\n'
851 'Run "git log --oneline %s..HEAD" to get a list of commits in '
852 'the diff. If you are using a custom git flow, you can override'
853 ' the reference used for this check with "git config '
854 'gitcl.remotebranch <git-ref>".' % (
855 len(common_commits), remote_branch, upstream_git_obj))
856 return False
857 return True
858
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000859 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000860 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000861
862 Returns None if it is not set.
863 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000864 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
865 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000866
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000867 def GetGitSvnRemoteUrl(self):
868 """Return the configured git-svn remote URL parsed from git svn info.
869
870 Returns None if it is not set.
871 """
872 # URL is dependent on the current directory.
873 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
874 if data:
875 keys = dict(line.split(': ', 1) for line in data.splitlines()
876 if ': ' in line)
877 return keys.get('URL', None)
878 return None
879
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000880 def GetRemoteUrl(self):
881 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
882
883 Returns None if there is no remote.
884 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000885 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000886 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
887
888 # If URL is pointing to a local directory, it is probably a git cache.
889 if os.path.isdir(url):
890 url = RunGit(['config', 'remote.%s.url' % remote],
891 error_ok=True,
892 cwd=url).strip()
893 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000894
895 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000896 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000897 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000898 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000899 self.issue = int(issue) or None if issue else None
900 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000901 return self.issue
902
903 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000904 if not self.rietveld_server:
905 # If we're on a branch then get the server potentially associated
906 # with that branch.
907 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000908 rietveld_server_config = self._RietveldServer()
909 if rietveld_server_config:
910 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
911 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000912 if not self.rietveld_server:
913 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000914 return self.rietveld_server
915
916 def GetIssueURL(self):
917 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000918 if not self.GetIssue():
919 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000920 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
921
922 def GetDescription(self, pretty=False):
923 if not self.has_description:
924 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000925 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000926 try:
927 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000928 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000929 if e.code == 404:
930 DieWithError(
931 ('\nWhile fetching the description for issue %d, received a '
932 '404 (not found)\n'
933 'error. It is likely that you deleted this '
934 'issue on the server. If this is the\n'
935 'case, please run\n\n'
936 ' git cl issue 0\n\n'
937 'to clear the association with the deleted issue. Then run '
938 'this command again.') % issue)
939 else:
940 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000941 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000942 except urllib2.URLError as e:
943 print >> sys.stderr, (
944 'Warning: Failed to retrieve CL description due to network '
945 'failure.')
946 self.description = ''
947
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000948 self.has_description = True
949 if pretty:
950 wrapper = textwrap.TextWrapper()
951 wrapper.initial_indent = wrapper.subsequent_indent = ' '
952 return wrapper.fill(self.description)
953 return self.description
954
955 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000956 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000957 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000958 patchset = RunGit(['config', self._PatchsetSetting()],
959 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000960 self.patchset = int(patchset) or None if patchset else None
961 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000962 return self.patchset
963
964 def SetPatchset(self, patchset):
965 """Set this branch's patchset. If patchset=0, clears the patchset."""
966 if patchset:
967 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000968 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000969 else:
970 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000971 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000972 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000973
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000974 def GetMostRecentPatchset(self):
975 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000976
977 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000978 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000979 '/download/issue%s_%s.diff' % (issue, patchset))
980
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000981 def GetIssueProperties(self):
982 if self._props is None:
983 issue = self.GetIssue()
984 if not issue:
985 self._props = {}
986 else:
987 self._props = self.RpcServer().get_issue_properties(issue, True)
988 return self._props
989
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000990 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000991 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000992
apavlov@chromium.orge4efd512014-11-05 09:05:29 +0000993 def AddComment(self, message):
994 return self.RpcServer().add_comment(self.GetIssue(), message)
995
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000996 def SetIssue(self, issue):
997 """Set this branch's issue. If issue=0, clears the issue."""
998 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000999 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001000 RunGit(['config', self._IssueSetting(), str(issue)])
1001 if self.rietveld_server:
1002 RunGit(['config', self._RietveldServer(), self.rietveld_server])
1003 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001004 current_issue = self.GetIssue()
1005 if current_issue:
1006 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001007 self.issue = None
1008 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001009
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001010 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001011 if not self.GitSanityChecks(upstream_branch):
1012 DieWithError('\nGit sanity check failure')
1013
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001014 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001015 if not root:
1016 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001017 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001018
1019 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001020 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001021 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001022 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001023 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001024 except subprocess2.CalledProcessError:
1025 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001026 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001027 'This branch probably doesn\'t exist anymore. To reset the\n'
1028 'tracking branch, please run\n'
1029 ' git branch --set-upstream %s trunk\n'
1030 'replacing trunk with origin/master or the relevant branch') %
1031 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001032
maruel@chromium.org52424302012-08-29 15:14:30 +00001033 issue = self.GetIssue()
1034 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001035 if issue:
1036 description = self.GetDescription()
1037 else:
1038 # If the change was never uploaded, use the log messages of all commits
1039 # up to the branch point, as git cl upload will prefill the description
1040 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001041 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1042 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001043
1044 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001045 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001046 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001047 name,
1048 description,
1049 absroot,
1050 files,
1051 issue,
1052 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001053 author,
1054 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001055
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001056 def GetStatus(self):
1057 """Apply a rough heuristic to give a simple summary of an issue's review
1058 or CQ status, assuming adherence to a common workflow.
1059
1060 Returns None if no issue for this branch, or one of the following keywords:
1061 * 'error' - error from review tool (including deleted issues)
1062 * 'unsent' - not sent for review
1063 * 'waiting' - waiting for review
1064 * 'reply' - waiting for owner to reply to review
1065 * 'lgtm' - LGTM from at least one approved reviewer
1066 * 'commit' - in the commit queue
1067 * 'closed' - closed
1068 """
1069 if not self.GetIssue():
1070 return None
1071
1072 try:
1073 props = self.GetIssueProperties()
1074 except urllib2.HTTPError:
1075 return 'error'
1076
1077 if props.get('closed'):
1078 # Issue is closed.
1079 return 'closed'
1080 if props.get('commit'):
1081 # Issue is in the commit queue.
1082 return 'commit'
1083
1084 try:
1085 reviewers = self.GetApprovingReviewers()
1086 except urllib2.HTTPError:
1087 return 'error'
1088
1089 if reviewers:
1090 # Was LGTM'ed.
1091 return 'lgtm'
1092
1093 messages = props.get('messages') or []
1094
1095 if not messages:
1096 # No message was sent.
1097 return 'unsent'
1098 if messages[-1]['sender'] != props.get('owner_email'):
1099 # Non-LGTM reply from non-owner
1100 return 'reply'
1101 return 'waiting'
1102
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001103 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001104 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001105
1106 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001107 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001108 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001109 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001110 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001111 except presubmit_support.PresubmitFailure, e:
1112 DieWithError(
1113 ('%s\nMaybe your depot_tools is out of date?\n'
1114 'If all fails, contact maruel@') % e)
1115
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001116 def UpdateDescription(self, description):
1117 self.description = description
1118 return self.RpcServer().update_description(
1119 self.GetIssue(), self.description)
1120
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001121 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001122 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001123 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001124
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001125 def SetFlag(self, flag, value):
1126 """Patchset must match."""
1127 if not self.GetPatchset():
1128 DieWithError('The patchset needs to match. Send another patchset.')
1129 try:
1130 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001131 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001132 except urllib2.HTTPError, e:
1133 if e.code == 404:
1134 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1135 if e.code == 403:
1136 DieWithError(
1137 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1138 'match?') % (self.GetIssue(), self.GetPatchset()))
1139 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001141 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142 """Returns an upload.RpcServer() to access this review's rietveld instance.
1143 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001144 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001145 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001146 self.GetRietveldServer(),
1147 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001148 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149
1150 def _IssueSetting(self):
1151 """Return the git setting that stores this change's issue."""
1152 return 'branch.%s.rietveldissue' % self.GetBranch()
1153
1154 def _PatchsetSetting(self):
1155 """Return the git setting that stores this change's most recent patchset."""
1156 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1157
1158 def _RietveldServer(self):
1159 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001160 branch = self.GetBranch()
1161 if branch:
1162 return 'branch.%s.rietveldserver' % branch
1163 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001164
1165
1166def GetCodereviewSettingsInteractively():
1167 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001168 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169 server = settings.GetDefaultServerUrl(error_ok=True)
1170 prompt = 'Rietveld server (host[:port])'
1171 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001172 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173 if not server and not newserver:
1174 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001175 if newserver:
1176 newserver = gclient_utils.UpgradeToHttps(newserver)
1177 if newserver != server:
1178 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001179
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001180 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001181 prompt = caption
1182 if initial:
1183 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001184 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 if new_val == 'x':
1186 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001187 elif new_val:
1188 if is_url:
1189 new_val = gclient_utils.UpgradeToHttps(new_val)
1190 if new_val != initial:
1191 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001192
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001193 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001194 SetProperty(settings.GetDefaultPrivateFlag(),
1195 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001196 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001197 'tree-status-url', False)
1198 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001199 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001200 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1201 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001202
1203 # TODO: configure a default branch to diff against, rather than this
1204 # svn-based hackery.
1205
1206
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001207class ChangeDescription(object):
1208 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001209 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001210 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001211
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001212 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001213 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001214
agable@chromium.org42c20792013-09-12 17:34:49 +00001215 @property # www.logilab.org/ticket/89786
1216 def description(self): # pylint: disable=E0202
1217 return '\n'.join(self._description_lines)
1218
1219 def set_description(self, desc):
1220 if isinstance(desc, basestring):
1221 lines = desc.splitlines()
1222 else:
1223 lines = [line.rstrip() for line in desc]
1224 while lines and not lines[0]:
1225 lines.pop(0)
1226 while lines and not lines[-1]:
1227 lines.pop(-1)
1228 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001229
piman@chromium.org336f9122014-09-04 02:16:55 +00001230 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001231 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001232 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001233 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001234 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001235 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001236
agable@chromium.org42c20792013-09-12 17:34:49 +00001237 # Get the set of R= and TBR= lines and remove them from the desciption.
1238 regexp = re.compile(self.R_LINE)
1239 matches = [regexp.match(line) for line in self._description_lines]
1240 new_desc = [l for i, l in enumerate(self._description_lines)
1241 if not matches[i]]
1242 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001243
agable@chromium.org42c20792013-09-12 17:34:49 +00001244 # Construct new unified R= and TBR= lines.
1245 r_names = []
1246 tbr_names = []
1247 for match in matches:
1248 if not match:
1249 continue
1250 people = cleanup_list([match.group(2).strip()])
1251 if match.group(1) == 'TBR':
1252 tbr_names.extend(people)
1253 else:
1254 r_names.extend(people)
1255 for name in r_names:
1256 if name not in reviewers:
1257 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001258 if add_owners_tbr:
1259 owners_db = owners.Database(change.RepositoryRoot(),
1260 fopen=file, os_path=os.path, glob=glob.glob)
1261 all_reviewers = set(tbr_names + reviewers)
1262 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1263 all_reviewers)
1264 tbr_names.extend(owners_db.reviewers_for(missing_files,
1265 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001266 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1267 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1268
1269 # Put the new lines in the description where the old first R= line was.
1270 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1271 if 0 <= line_loc < len(self._description_lines):
1272 if new_tbr_line:
1273 self._description_lines.insert(line_loc, new_tbr_line)
1274 if new_r_line:
1275 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001276 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001277 if new_r_line:
1278 self.append_footer(new_r_line)
1279 if new_tbr_line:
1280 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001281
1282 def prompt(self):
1283 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001284 self.set_description([
1285 '# Enter a description of the change.',
1286 '# This will be displayed on the codereview site.',
1287 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001288 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001289 '--------------------',
1290 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001291
agable@chromium.org42c20792013-09-12 17:34:49 +00001292 regexp = re.compile(self.BUG_LINE)
1293 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001294 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001295 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001296 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001297 if not content:
1298 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001299 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001300
1301 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001302 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1303 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001304 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001305 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001306
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001307 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001308 if self._description_lines:
1309 # Add an empty line if either the last line or the new line isn't a tag.
1310 last_line = self._description_lines[-1]
1311 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1312 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1313 self._description_lines.append('')
1314 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001315
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001316 def get_reviewers(self):
1317 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001318 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1319 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001320 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001321
1322
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001323def get_approving_reviewers(props):
1324 """Retrieves the reviewers that approved a CL from the issue properties with
1325 messages.
1326
1327 Note that the list may contain reviewers that are not committer, thus are not
1328 considered by the CQ.
1329 """
1330 return sorted(
1331 set(
1332 message['sender']
1333 for message in props['messages']
1334 if message['approval'] and message['sender'] in props['reviewers']
1335 )
1336 )
1337
1338
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339def FindCodereviewSettingsFile(filename='codereview.settings'):
1340 """Finds the given file starting in the cwd and going up.
1341
1342 Only looks up to the top of the repository unless an
1343 'inherit-review-settings-ok' file exists in the root of the repository.
1344 """
1345 inherit_ok_file = 'inherit-review-settings-ok'
1346 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001347 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001348 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1349 root = '/'
1350 while True:
1351 if filename in os.listdir(cwd):
1352 if os.path.isfile(os.path.join(cwd, filename)):
1353 return open(os.path.join(cwd, filename))
1354 if cwd == root:
1355 break
1356 cwd = os.path.dirname(cwd)
1357
1358
1359def LoadCodereviewSettingsFromFile(fileobj):
1360 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001361 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001363 def SetProperty(name, setting, unset_error_ok=False):
1364 fullname = 'rietveld.' + name
1365 if setting in keyvals:
1366 RunGit(['config', fullname, keyvals[setting]])
1367 else:
1368 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1369
1370 SetProperty('server', 'CODE_REVIEW_SERVER')
1371 # Only server setting is required. Other settings can be absent.
1372 # In that case, we ignore errors raised during option deletion attempt.
1373 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001374 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001375 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1376 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001377 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001378 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001379 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1380 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001381 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001382 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001383 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001384 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1385 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001386
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001387 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001388 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001389
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001390 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1391 #should be of the form
1392 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1393 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1394 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1395 keyvals['ORIGIN_URL_CONFIG']])
1396
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001397
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001398def urlretrieve(source, destination):
1399 """urllib is broken for SSL connections via a proxy therefore we
1400 can't use urllib.urlretrieve()."""
1401 with open(destination, 'w') as f:
1402 f.write(urllib2.urlopen(source).read())
1403
1404
ukai@chromium.org712d6102013-11-27 00:52:58 +00001405def hasSheBang(fname):
1406 """Checks fname is a #! script."""
1407 with open(fname) as f:
1408 return f.read(2).startswith('#!')
1409
1410
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001411def DownloadHooks(force):
1412 """downloads hooks
1413
1414 Args:
1415 force: True to update hooks. False to install hooks if not present.
1416 """
1417 if not settings.GetIsGerrit():
1418 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001419 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001420 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1421 if not os.access(dst, os.X_OK):
1422 if os.path.exists(dst):
1423 if not force:
1424 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001425 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001426 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001427 if not hasSheBang(dst):
1428 DieWithError('Not a script: %s\n'
1429 'You need to download from\n%s\n'
1430 'into .git/hooks/commit-msg and '
1431 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001432 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1433 except Exception:
1434 if os.path.exists(dst):
1435 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001436 DieWithError('\nFailed to download hooks.\n'
1437 'You need to download from\n%s\n'
1438 'into .git/hooks/commit-msg and '
1439 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001440
1441
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001442@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001444 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001445
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001446 parser.add_option('--activate-update', action='store_true',
1447 help='activate auto-updating [rietveld] section in '
1448 '.git/config')
1449 parser.add_option('--deactivate-update', action='store_true',
1450 help='deactivate auto-updating [rietveld] section in '
1451 '.git/config')
1452 options, args = parser.parse_args(args)
1453
1454 if options.deactivate_update:
1455 RunGit(['config', 'rietveld.autoupdate', 'false'])
1456 return
1457
1458 if options.activate_update:
1459 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1460 return
1461
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001462 if len(args) == 0:
1463 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001464 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001465 return 0
1466
1467 url = args[0]
1468 if not url.endswith('codereview.settings'):
1469 url = os.path.join(url, 'codereview.settings')
1470
1471 # Load code review settings and download hooks (if available).
1472 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001473 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001474 return 0
1475
1476
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001477def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001478 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001479 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1480 branch = ShortBranchName(branchref)
1481 _, args = parser.parse_args(args)
1482 if not args:
1483 print("Current base-url:")
1484 return RunGit(['config', 'branch.%s.base-url' % branch],
1485 error_ok=False).strip()
1486 else:
1487 print("Setting base-url to %s" % args[0])
1488 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1489 error_ok=False).strip()
1490
1491
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001492def color_for_status(status):
1493 """Maps a Changelist status to color, for CMDstatus and other tools."""
1494 return {
1495 'unsent': Fore.RED,
1496 'waiting': Fore.BLUE,
1497 'reply': Fore.YELLOW,
1498 'lgtm': Fore.GREEN,
1499 'commit': Fore.MAGENTA,
1500 'closed': Fore.CYAN,
1501 'error': Fore.WHITE,
1502 }.get(status, Fore.WHITE)
1503
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001504def fetch_cl_status(branch, auth_config=None):
1505 """Fetches information for an issue and returns (branch, issue, status)."""
1506 cl = Changelist(branchref=branch, auth_config=auth_config)
1507 url = cl.GetIssueURL()
1508 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001509
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001510 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001511 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001512 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001513
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001514 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001515
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001516def get_cl_statuses(
1517 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001518 """Returns a blocking iterable of (branch, issue, color) for given branches.
1519
1520 If fine_grained is true, this will fetch CL statuses from the server.
1521 Otherwise, simply indicate if there's a matching url for the given branches.
1522
1523 If max_processes is specified, it is used as the maximum number of processes
1524 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1525 spawned.
1526 """
1527 # Silence upload.py otherwise it becomes unwieldly.
1528 upload.verbosity = 0
1529
1530 if fine_grained:
1531 # Process one branch synchronously to work through authentication, then
1532 # spawn processes to process all the other branches in parallel.
1533 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001534 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1535 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001536
1537 branches_to_fetch = branches[1:]
1538 pool = ThreadPool(
1539 min(max_processes, len(branches_to_fetch))
1540 if max_processes is not None
1541 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001542 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001543 yield x
1544 else:
1545 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1546 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001547 cl = Changelist(branchref=b, auth_config=auth_config)
1548 url = cl.GetIssueURL()
1549 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001550
rmistry@google.com2dd99862015-06-22 12:22:18 +00001551
1552def upload_branch_deps(cl, args):
1553 """Uploads CLs of local branches that are dependents of the current branch.
1554
1555 If the local branch dependency tree looks like:
1556 test1 -> test2.1 -> test3.1
1557 -> test3.2
1558 -> test2.2 -> test3.3
1559
1560 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1561 run on the dependent branches in this order:
1562 test2.1, test3.1, test3.2, test2.2, test3.3
1563
1564 Note: This function does not rebase your local dependent branches. Use it when
1565 you make a change to the parent branch that will not conflict with its
1566 dependent branches, and you would like their dependencies updated in
1567 Rietveld.
1568 """
1569 if git_common.is_dirty_git_tree('upload-branch-deps'):
1570 return 1
1571
1572 root_branch = cl.GetBranch()
1573 if root_branch is None:
1574 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1575 'Get on a branch!')
1576 if not cl.GetIssue() or not cl.GetPatchset():
1577 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1578 'patchset dependencies without an uploaded CL.')
1579
1580 branches = RunGit(['for-each-ref',
1581 '--format=%(refname:short) %(upstream:short)',
1582 'refs/heads'])
1583 if not branches:
1584 print('No local branches found.')
1585 return 0
1586
1587 # Create a dictionary of all local branches to the branches that are dependent
1588 # on it.
1589 tracked_to_dependents = collections.defaultdict(list)
1590 for b in branches.splitlines():
1591 tokens = b.split()
1592 if len(tokens) == 2:
1593 branch_name, tracked = tokens
1594 tracked_to_dependents[tracked].append(branch_name)
1595
1596 print
1597 print 'The dependent local branches of %s are:' % root_branch
1598 dependents = []
1599 def traverse_dependents_preorder(branch, padding=''):
1600 dependents_to_process = tracked_to_dependents.get(branch, [])
1601 padding += ' '
1602 for dependent in dependents_to_process:
1603 print '%s%s' % (padding, dependent)
1604 dependents.append(dependent)
1605 traverse_dependents_preorder(dependent, padding)
1606 traverse_dependents_preorder(root_branch)
1607 print
1608
1609 if not dependents:
1610 print 'There are no dependent local branches for %s' % root_branch
1611 return 0
1612
1613 print ('This command will checkout all dependent branches and run '
1614 '"git cl upload".')
1615 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1616
1617 # Add a default patchset title to all upload calls.
1618 args.extend(['-t', 'Updated patchset dependency'])
1619 # Record all dependents that failed to upload.
1620 failures = {}
1621 # Go through all dependents, checkout the branch and upload.
1622 try:
1623 for dependent_branch in dependents:
1624 print
1625 print '--------------------------------------'
1626 print 'Running "git cl upload" from %s:' % dependent_branch
1627 RunGit(['checkout', '-q', dependent_branch])
1628 print
1629 try:
1630 if CMDupload(OptionParser(), args) != 0:
1631 print 'Upload failed for %s!' % dependent_branch
1632 failures[dependent_branch] = 1
1633 except: # pylint: disable=W0702
1634 failures[dependent_branch] = 1
1635 print
1636 finally:
1637 # Swap back to the original root branch.
1638 RunGit(['checkout', '-q', root_branch])
1639
1640 print
1641 print 'Upload complete for dependent branches!'
1642 for dependent_branch in dependents:
1643 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1644 print ' %s : %s' % (dependent_branch, upload_status)
1645 print
1646
1647 return 0
1648
1649
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001650def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001651 """Show status of changelists.
1652
1653 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001654 - Red not sent for review or broken
1655 - Blue waiting for review
1656 - Yellow waiting for you to reply to review
1657 - Green LGTM'ed
1658 - Magenta in the commit queue
1659 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001660
1661 Also see 'git cl comments'.
1662 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001663 parser.add_option('--field',
1664 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001665 parser.add_option('-f', '--fast', action='store_true',
1666 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001667 parser.add_option(
1668 '-j', '--maxjobs', action='store', type=int,
1669 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001670
1671 auth.add_auth_options(parser)
1672 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001673 if args:
1674 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001675 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001676
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001677 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001678 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001679 if options.field.startswith('desc'):
1680 print cl.GetDescription()
1681 elif options.field == 'id':
1682 issueid = cl.GetIssue()
1683 if issueid:
1684 print issueid
1685 elif options.field == 'patch':
1686 patchset = cl.GetPatchset()
1687 if patchset:
1688 print patchset
1689 elif options.field == 'url':
1690 url = cl.GetIssueURL()
1691 if url:
1692 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001693 return 0
1694
1695 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1696 if not branches:
1697 print('No local branch found.')
1698 return 0
1699
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001700 changes = (
1701 Changelist(branchref=b, auth_config=auth_config)
1702 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001703 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001704 alignment = max(5, max(len(b) for b in branches))
1705 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001706 output = get_cl_statuses(branches,
1707 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001708 max_processes=options.maxjobs,
1709 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001710
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001711 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001712 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001713 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001714 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001715 b, i, status = output.next()
1716 branch_statuses[b] = (i, status)
1717 issue_url, status = branch_statuses.pop(branch)
1718 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001719 reset = Fore.RESET
1720 if not sys.stdout.isatty():
1721 color = ''
1722 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001723 status_str = '(%s)' % status if status else ''
1724 print ' %*s : %s%s %s%s' % (
1725 alignment, ShortBranchName(branch), color, issue_url, status_str,
1726 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001727
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001728 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001729 print
1730 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001731 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001732 if not cl.GetIssue():
1733 print 'No issue assigned.'
1734 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001735 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001736 if not options.fast:
1737 print 'Issue description:'
1738 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001739 return 0
1740
1741
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001742def colorize_CMDstatus_doc():
1743 """To be called once in main() to add colors to git cl status help."""
1744 colors = [i for i in dir(Fore) if i[0].isupper()]
1745
1746 def colorize_line(line):
1747 for color in colors:
1748 if color in line.upper():
1749 # Extract whitespaces first and the leading '-'.
1750 indent = len(line) - len(line.lstrip(' ')) + 1
1751 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1752 return line
1753
1754 lines = CMDstatus.__doc__.splitlines()
1755 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1756
1757
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001758@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001759def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001760 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001761
1762 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001763 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001764 parser.add_option('-r', '--reverse', action='store_true',
1765 help='Lookup the branch(es) for the specified issues. If '
1766 'no issues are specified, all branches with mapped '
1767 'issues will be listed.')
1768 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001769
dnj@chromium.org406c4402015-03-03 17:22:28 +00001770 if options.reverse:
1771 branches = RunGit(['for-each-ref', 'refs/heads',
1772 '--format=%(refname:short)']).splitlines()
1773
1774 # Reverse issue lookup.
1775 issue_branch_map = {}
1776 for branch in branches:
1777 cl = Changelist(branchref=branch)
1778 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1779 if not args:
1780 args = sorted(issue_branch_map.iterkeys())
1781 for issue in args:
1782 if not issue:
1783 continue
1784 print 'Branch for issue number %s: %s' % (
1785 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1786 else:
1787 cl = Changelist()
1788 if len(args) > 0:
1789 try:
1790 issue = int(args[0])
1791 except ValueError:
1792 DieWithError('Pass a number to set the issue or none to list it.\n'
1793 'Maybe you want to run git cl status?')
1794 cl.SetIssue(issue)
1795 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001796 return 0
1797
1798
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001799def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001800 """Shows or posts review comments for any changelist."""
1801 parser.add_option('-a', '--add-comment', dest='comment',
1802 help='comment to add to an issue')
1803 parser.add_option('-i', dest='issue',
1804 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00001805 parser.add_option('-j', '--json-file',
1806 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001807 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001808 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001809 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001810
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001811 issue = None
1812 if options.issue:
1813 try:
1814 issue = int(options.issue)
1815 except ValueError:
1816 DieWithError('A review issue id is expected to be a number')
1817
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001818 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001819
1820 if options.comment:
1821 cl.AddComment(options.comment)
1822 return 0
1823
1824 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00001825 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001826 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00001827 summary.append({
1828 'date': message['date'],
1829 'lgtm': False,
1830 'message': message['text'],
1831 'not_lgtm': False,
1832 'sender': message['sender'],
1833 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001834 if message['disapproval']:
1835 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00001836 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001837 elif message['approval']:
1838 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00001839 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001840 elif message['sender'] == data['owner_email']:
1841 color = Fore.MAGENTA
1842 else:
1843 color = Fore.BLUE
1844 print '\n%s%s %s%s' % (
1845 color, message['date'].split('.', 1)[0], message['sender'],
1846 Fore.RESET)
1847 if message['text'].strip():
1848 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00001849 if options.json_file:
1850 with open(options.json_file, 'wb') as f:
1851 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001852 return 0
1853
1854
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001855def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001856 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00001857 parser.add_option('-d', '--display', action='store_true',
1858 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001859 auth.add_auth_options(parser)
1860 options, _ = parser.parse_args(args)
1861 auth_config = auth.extract_auth_config_from_options(options)
1862 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001863 if not cl.GetIssue():
1864 DieWithError('This branch has no associated changelist.')
1865 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00001866 if options.display:
1867 print description.description
1868 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001869 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00001870 if cl.GetDescription() != description.description:
1871 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001872 return 0
1873
1874
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001875def CreateDescriptionFromLog(args):
1876 """Pulls out the commit log to use as a base for the CL description."""
1877 log_args = []
1878 if len(args) == 1 and not args[0].endswith('.'):
1879 log_args = [args[0] + '..']
1880 elif len(args) == 1 and args[0].endswith('...'):
1881 log_args = [args[0][:-1]]
1882 elif len(args) == 2:
1883 log_args = [args[0] + '..' + args[1]]
1884 else:
1885 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001886 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001887
1888
thestig@chromium.org44202a22014-03-11 19:22:18 +00001889def CMDlint(parser, args):
1890 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001891 parser.add_option('--filter', action='append', metavar='-x,+y',
1892 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001893 auth.add_auth_options(parser)
1894 options, args = parser.parse_args(args)
1895 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001896
1897 # Access to a protected member _XX of a client class
1898 # pylint: disable=W0212
1899 try:
1900 import cpplint
1901 import cpplint_chromium
1902 except ImportError:
1903 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1904 return 1
1905
1906 # Change the current working directory before calling lint so that it
1907 # shows the correct base.
1908 previous_cwd = os.getcwd()
1909 os.chdir(settings.GetRoot())
1910 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001911 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001912 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1913 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001914 if not files:
1915 print "Cannot lint an empty CL"
1916 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001917
1918 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001919 command = args + files
1920 if options.filter:
1921 command = ['--filter=' + ','.join(options.filter)] + command
1922 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001923
1924 white_regex = re.compile(settings.GetLintRegex())
1925 black_regex = re.compile(settings.GetLintIgnoreRegex())
1926 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1927 for filename in filenames:
1928 if white_regex.match(filename):
1929 if black_regex.match(filename):
1930 print "Ignoring file %s" % filename
1931 else:
1932 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1933 extra_check_functions)
1934 else:
1935 print "Skipping file %s" % filename
1936 finally:
1937 os.chdir(previous_cwd)
1938 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1939 if cpplint._cpplint_state.error_count != 0:
1940 return 1
1941 return 0
1942
1943
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001944def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001945 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001946 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001947 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001948 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001949 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001950 auth.add_auth_options(parser)
1951 options, args = parser.parse_args(args)
1952 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001953
sbc@chromium.org71437c02015-04-09 19:29:40 +00001954 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00001955 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001956 return 1
1957
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001958 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001959 if args:
1960 base_branch = args[0]
1961 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001962 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001963 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001964
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001965 cl.RunHook(
1966 committing=not options.upload,
1967 may_prompt=False,
1968 verbose=options.verbose,
1969 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001970 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001971
1972
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001973def AddChangeIdToCommitMessage(options, args):
1974 """Re-commits using the current message, assumes the commit hook is in
1975 place.
1976 """
1977 log_desc = options.message or CreateDescriptionFromLog(args)
1978 git_command = ['commit', '--amend', '-m', log_desc]
1979 RunGit(git_command)
1980 new_log_desc = CreateDescriptionFromLog(args)
1981 if CHANGE_ID in new_log_desc:
1982 print 'git-cl: Added Change-Id to commit message.'
1983 else:
1984 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1985
1986
piman@chromium.org336f9122014-09-04 02:16:55 +00001987def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001988 """upload the current branch to gerrit."""
1989 # We assume the remote called "origin" is the one we want.
1990 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001991 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00001992
1993 remote, remote_branch = cl.GetRemoteBranch()
1994 branch = GetTargetRef(remote, remote_branch, options.target_branch,
1995 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001996
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001997 change_desc = ChangeDescription(
1998 options.message or CreateDescriptionFromLog(args))
1999 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00002000 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002001 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002002
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002003 if options.squash:
2004 # Try to get the message from a previous upload.
2005 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
2006 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
2007 if not message:
2008 if not options.force:
2009 change_desc.prompt()
2010
2011 if CHANGE_ID not in change_desc.description:
2012 # Run the commit-msg hook without modifying the head commit by writing
2013 # the commit message to a temporary file and running the hook over it,
2014 # then reading the file back in.
2015 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
2016 'commit-msg')
2017 file_handle, msg_file = tempfile.mkstemp(text=True,
2018 prefix='commit_msg')
2019 try:
2020 try:
2021 with os.fdopen(file_handle, 'w') as fileobj:
2022 fileobj.write(change_desc.description)
2023 finally:
2024 os.close(file_handle)
2025 RunCommand([commit_msg_hook, msg_file])
2026 change_desc.set_description(gclient_utils.FileRead(msg_file))
2027 finally:
2028 os.remove(msg_file)
2029
2030 if not change_desc.description:
2031 print "Description is empty; aborting."
2032 return 1
2033
2034 message = change_desc.description
2035
2036 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2037 if remote is '.':
2038 # If our upstream branch is local, we base our squashed commit on its
2039 # squashed version.
2040 parent = ('refs/heads/git_cl_uploads/' +
2041 scm.GIT.ShortBranchName(upstream_branch))
2042
2043 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2044 # will create additional CLs when uploading.
2045 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2046 RunGitSilent(['rev-parse', parent + ':'])):
2047 print 'Upload upstream branch ' + upstream_branch + ' first.'
2048 return 1
2049 else:
2050 parent = cl.GetCommonAncestorWithUpstream()
2051
2052 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2053 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2054 '-m', message]).strip()
2055 else:
2056 if CHANGE_ID not in change_desc.description:
2057 AddChangeIdToCommitMessage(options, args)
2058 ref_to_push = 'HEAD'
2059 parent = '%s/%s' % (gerrit_remote, branch)
2060
2061 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2062 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002063 if len(commits) > 1:
2064 print('WARNING: This will upload %d commits. Run the following command '
2065 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002066 print('git log %s..%s' % (parent, ref_to_push))
2067 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002068 'commit.')
2069 ask_for_data('About to upload; enter to confirm.')
2070
piman@chromium.org336f9122014-09-04 02:16:55 +00002071 if options.reviewers or options.tbr_owners:
2072 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002073
ukai@chromium.orge8077812012-02-03 03:41:46 +00002074 receive_options = []
2075 cc = cl.GetCCList().split(',')
2076 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002077 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002078 cc = filter(None, cc)
2079 if cc:
2080 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002081 if change_desc.get_reviewers():
2082 receive_options.extend(
2083 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002084
ukai@chromium.orge8077812012-02-03 03:41:46 +00002085 git_command = ['push']
2086 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002087 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002088 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002089 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002090 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002091
2092 if options.squash:
2093 head = RunGit(['rev-parse', 'HEAD']).strip()
2094 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2095
ukai@chromium.orge8077812012-02-03 03:41:46 +00002096 # TODO(ukai): parse Change-Id: and set issue number?
2097 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002098
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002099
wittman@chromium.org455dc922015-01-26 20:15:50 +00002100def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2101 """Computes the remote branch ref to use for the CL.
2102
2103 Args:
2104 remote (str): The git remote for the CL.
2105 remote_branch (str): The git remote branch for the CL.
2106 target_branch (str): The target branch specified by the user.
2107 pending_prefix (str): The pending prefix from the settings.
2108 """
2109 if not (remote and remote_branch):
2110 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002111
wittman@chromium.org455dc922015-01-26 20:15:50 +00002112 if target_branch:
2113 # Cannonicalize branch references to the equivalent local full symbolic
2114 # refs, which are then translated into the remote full symbolic refs
2115 # below.
2116 if '/' not in target_branch:
2117 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2118 else:
2119 prefix_replacements = (
2120 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2121 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2122 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2123 )
2124 match = None
2125 for regex, replacement in prefix_replacements:
2126 match = re.search(regex, target_branch)
2127 if match:
2128 remote_branch = target_branch.replace(match.group(0), replacement)
2129 break
2130 if not match:
2131 # This is a branch path but not one we recognize; use as-is.
2132 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002133 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2134 # Handle the refs that need to land in different refs.
2135 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002136
wittman@chromium.org455dc922015-01-26 20:15:50 +00002137 # Create the true path to the remote branch.
2138 # Does the following translation:
2139 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2140 # * refs/remotes/origin/master -> refs/heads/master
2141 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2142 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2143 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2144 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2145 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2146 'refs/heads/')
2147 elif remote_branch.startswith('refs/remotes/branch-heads'):
2148 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2149 # If a pending prefix exists then replace refs/ with it.
2150 if pending_prefix:
2151 remote_branch = remote_branch.replace('refs/', pending_prefix)
2152 return remote_branch
2153
2154
piman@chromium.org336f9122014-09-04 02:16:55 +00002155def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002156 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002157 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2158 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002159 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002160 if options.emulate_svn_auto_props:
2161 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002162
2163 change_desc = None
2164
pgervais@chromium.org91141372014-01-09 23:27:20 +00002165 if options.email is not None:
2166 upload_args.extend(['--email', options.email])
2167
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002168 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002169 if options.title:
2170 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002171 if options.message:
2172 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002173 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002174 print ("This branch is associated with issue %s. "
2175 "Adding patch to that issue." % cl.GetIssue())
2176 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002177 if options.title:
2178 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002179 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002180 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002181 if options.reviewers or options.tbr_owners:
2182 change_desc.update_reviewers(options.reviewers,
2183 options.tbr_owners,
2184 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002185 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002186 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002187
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002188 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002189 print "Description is empty; aborting."
2190 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002191
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002192 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002193 if change_desc.get_reviewers():
2194 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002195 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002196 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002197 DieWithError("Must specify reviewers to send email.")
2198 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002199
2200 # We check this before applying rietveld.private assuming that in
2201 # rietveld.cc only addresses which we can send private CLs to are listed
2202 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2203 # --private is specified explicitly on the command line.
2204 if options.private:
2205 logging.warn('rietveld.cc is ignored since private flag is specified. '
2206 'You need to review and add them manually if necessary.')
2207 cc = cl.GetCCListWithoutDefault()
2208 else:
2209 cc = cl.GetCCList()
2210 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002211 if cc:
2212 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002213
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002214 if options.private or settings.GetDefaultPrivateFlag() == "True":
2215 upload_args.append('--private')
2216
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002217 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002218 if not options.find_copies:
2219 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002220
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002221 # Include the upstream repo's URL in the change -- this is useful for
2222 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002223 remote_url = cl.GetGitBaseUrlFromConfig()
2224 if not remote_url:
2225 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002226 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002227 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002228 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2229 remote_url = (cl.GetRemoteUrl() + '@'
2230 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002231 if remote_url:
2232 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002233 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002234 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2235 settings.GetPendingRefPrefix())
2236 if target_ref:
2237 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002238
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002239 # Look for dependent patchsets. See crbug.com/480453 for more details.
2240 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2241 upstream_branch = ShortBranchName(upstream_branch)
2242 if remote is '.':
2243 # A local branch is being tracked.
2244 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002245 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002246 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002247 print ('Skipping dependency patchset upload because git config '
2248 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002249 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002250 else:
2251 auth_config = auth.extract_auth_config_from_options(options)
2252 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2253 branch_cl_issue_url = branch_cl.GetIssueURL()
2254 branch_cl_issue = branch_cl.GetIssue()
2255 branch_cl_patchset = branch_cl.GetPatchset()
2256 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2257 upload_args.extend(
2258 ['--depends_on_patchset', '%s:%s' % (
2259 branch_cl_issue, branch_cl_patchset)])
2260 print
2261 print ('The current branch (%s) is tracking a local branch (%s) with '
2262 'an associated CL.') % (cl.GetBranch(), local_branch)
2263 print 'Adding %s/#ps%s as a dependency patchset.' % (
2264 branch_cl_issue_url, branch_cl_patchset)
2265 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002266
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002267 project = settings.GetProject()
2268 if project:
2269 upload_args.extend(['--project', project])
2270
rmistry@google.comef966222015-04-07 11:15:01 +00002271 if options.cq_dry_run:
2272 upload_args.extend(['--cq_dry_run'])
2273
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002274 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002275 upload_args = ['upload'] + upload_args + args
2276 logging.info('upload.RealMain(%s)', upload_args)
2277 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002278 issue = int(issue)
2279 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002280 except KeyboardInterrupt:
2281 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002282 except:
2283 # If we got an exception after the user typed a description for their
2284 # change, back up the description before re-raising.
2285 if change_desc:
2286 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2287 print '\nGot exception while uploading -- saving description to %s\n' \
2288 % backup_path
2289 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002290 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002291 backup_file.close()
2292 raise
2293
2294 if not cl.GetIssue():
2295 cl.SetIssue(issue)
2296 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002297
2298 if options.use_commit_queue:
2299 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002300 return 0
2301
2302
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002303def cleanup_list(l):
2304 """Fixes a list so that comma separated items are put as individual items.
2305
2306 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2307 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2308 """
2309 items = sum((i.split(',') for i in l), [])
2310 stripped_items = (i.strip() for i in items)
2311 return sorted(filter(None, stripped_items))
2312
2313
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002314@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002315def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002316 """Uploads the current changelist to codereview.
2317
2318 Can skip dependency patchset uploads for a branch by running:
2319 git config branch.branch_name.skip-deps-uploads True
2320 To unset run:
2321 git config --unset branch.branch_name.skip-deps-uploads
2322 Can also set the above globally by using the --global flag.
2323 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002324 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2325 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002326 parser.add_option('--bypass-watchlists', action='store_true',
2327 dest='bypass_watchlists',
2328 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002329 parser.add_option('-f', action='store_true', dest='force',
2330 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002331 parser.add_option('-m', dest='message', help='message for patchset')
2332 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002333 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002334 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002335 help='reviewer email addresses')
2336 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002337 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002338 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002339 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002340 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002341 parser.add_option('--emulate_svn_auto_props',
2342 '--emulate-svn-auto-props',
2343 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002344 dest="emulate_svn_auto_props",
2345 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002346 parser.add_option('-c', '--use-commit-queue', action='store_true',
2347 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002348 parser.add_option('--private', action='store_true',
2349 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002350 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002351 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002352 metavar='TARGET',
2353 help='Apply CL to remote ref TARGET. ' +
2354 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002355 parser.add_option('--squash', action='store_true',
2356 help='Squash multiple commits into one (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002357 parser.add_option('--email', default=None,
2358 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002359 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2360 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002361 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2362 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002363 help='Send the patchset to do a CQ dry run right after '
2364 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002365 parser.add_option('--dependencies', action='store_true',
2366 help='Uploads CLs of all the local branches that depend on '
2367 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002368
rmistry@google.com2dd99862015-06-22 12:22:18 +00002369 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002370 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002371 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002372 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002373 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002374
sbc@chromium.org71437c02015-04-09 19:29:40 +00002375 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002376 return 1
2377
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002378 options.reviewers = cleanup_list(options.reviewers)
2379 options.cc = cleanup_list(options.cc)
2380
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002381 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002382 if args:
2383 # TODO(ukai): is it ok for gerrit case?
2384 base_branch = args[0]
2385 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002386 if cl.GetBranch() is None:
2387 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2388
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002389 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002390 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002391 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002392
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002393 # Make sure authenticated to Rietveld before running expensive hooks. It is
2394 # a fast, best efforts check. Rietveld still can reject the authentication
2395 # during the actual upload.
2396 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2397 authenticator = auth.get_authenticator_for_host(
2398 cl.GetRietveldServer(), auth_config)
2399 if not authenticator.has_cached_credentials():
2400 raise auth.LoginRequiredError(cl.GetRietveldServer())
2401
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002402 # Apply watchlists on upload.
2403 change = cl.GetChange(base_branch, None)
2404 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2405 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002406 if not options.bypass_watchlists:
2407 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002408
ukai@chromium.orge8077812012-02-03 03:41:46 +00002409 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002410 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002411 # Set the reviewer list now so that presubmit checks can access it.
2412 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002413 change_description.update_reviewers(options.reviewers,
2414 options.tbr_owners,
2415 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002416 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002417 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002418 may_prompt=not options.force,
2419 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002420 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002421 if not hook_results.should_continue():
2422 return 1
2423 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002424 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002425
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002426 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002427 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002428 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002429 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002430 print ('The last upload made from this repository was patchset #%d but '
2431 'the most recent patchset on the server is #%d.'
2432 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002433 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2434 'from another machine or branch the patch you\'re uploading now '
2435 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002436 ask_for_data('About to upload; enter to confirm.')
2437
iannucci@chromium.org79540052012-10-19 23:15:26 +00002438 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002439 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00002440 return GerritUpload(options, args, cl, change)
2441 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002442 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002443 git_set_branch_value('last-upload-hash',
2444 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002445 # Run post upload hooks, if specified.
2446 if settings.GetRunPostUploadHook():
2447 presubmit_support.DoPostUploadExecuter(
2448 change,
2449 cl,
2450 settings.GetRoot(),
2451 options.verbose,
2452 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002453
rmistry@google.com2dd99862015-06-22 12:22:18 +00002454 # Upload all dependencies if specified.
2455 if options.dependencies:
2456 print
2457 print '--dependencies has been specified.'
2458 print 'All dependent local branches will be re-uploaded.'
2459 print
2460 # Remove the dependencies flag from args so that we do not end up in a
2461 # loop.
2462 orig_args.remove('--dependencies')
2463 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002464 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002465
2466
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002467def IsSubmoduleMergeCommit(ref):
2468 # When submodules are added to the repo, we expect there to be a single
2469 # non-git-svn merge commit at remote HEAD with a signature comment.
2470 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002471 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002472 return RunGit(cmd) != ''
2473
2474
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002475def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002476 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002477
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002478 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002479 Updates changelog with metadata (e.g. pointer to review).
2480 Pushes/dcommits the code upstream.
2481 Updates review and closes.
2482 """
2483 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2484 help='bypass upload presubmit hook')
2485 parser.add_option('-m', dest='message',
2486 help="override review description")
2487 parser.add_option('-f', action='store_true', dest='force',
2488 help="force yes to questions (don't prompt)")
2489 parser.add_option('-c', dest='contributor',
2490 help="external contributor for patch (appended to " +
2491 "description and used as author for git). Should be " +
2492 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002493 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002494 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002495 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002496 auth_config = auth.extract_auth_config_from_options(options)
2497
2498 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002499
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002500 current = cl.GetBranch()
2501 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2502 if not settings.GetIsGitSvn() and remote == '.':
2503 print
2504 print 'Attempting to push branch %r into another local branch!' % current
2505 print
2506 print 'Either reparent this branch on top of origin/master:'
2507 print ' git reparent-branch --root'
2508 print
2509 print 'OR run `git rebase-update` if you think the parent branch is already'
2510 print 'committed.'
2511 print
2512 print ' Current parent: %r' % upstream_branch
2513 return 1
2514
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002515 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002516 # Default to merging against our best guess of the upstream branch.
2517 args = [cl.GetUpstreamBranch()]
2518
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002519 if options.contributor:
2520 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2521 print "Please provide contibutor as 'First Last <email@example.com>'"
2522 return 1
2523
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002524 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002525 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002526
sbc@chromium.org71437c02015-04-09 19:29:40 +00002527 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002528 return 1
2529
2530 # This rev-list syntax means "show all commits not in my branch that
2531 # are in base_branch".
2532 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2533 base_branch]).splitlines()
2534 if upstream_commits:
2535 print ('Base branch "%s" has %d commits '
2536 'not in this branch.' % (base_branch, len(upstream_commits)))
2537 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2538 return 1
2539
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002540 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002541 svn_head = None
2542 if cmd == 'dcommit' or base_has_submodules:
2543 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2544 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002545
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002546 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002547 # If the base_head is a submodule merge commit, the first parent of the
2548 # base_head should be a git-svn commit, which is what we're interested in.
2549 base_svn_head = base_branch
2550 if base_has_submodules:
2551 base_svn_head += '^1'
2552
2553 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002554 if extra_commits:
2555 print ('This branch has %d additional commits not upstreamed yet.'
2556 % len(extra_commits.splitlines()))
2557 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2558 'before attempting to %s.' % (base_branch, cmd))
2559 return 1
2560
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002561 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002562 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002563 author = None
2564 if options.contributor:
2565 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002566 hook_results = cl.RunHook(
2567 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002568 may_prompt=not options.force,
2569 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002570 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002571 if not hook_results.should_continue():
2572 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002573
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002574 # Check the tree status if the tree status URL is set.
2575 status = GetTreeStatus()
2576 if 'closed' == status:
2577 print('The tree is closed. Please wait for it to reopen. Use '
2578 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2579 return 1
2580 elif 'unknown' == status:
2581 print('Unable to determine tree status. Please verify manually and '
2582 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2583 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002584 else:
2585 breakpad.SendStack(
2586 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002587 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2588 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002589 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002590
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002591 change_desc = ChangeDescription(options.message)
2592 if not change_desc.description and cl.GetIssue():
2593 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002594
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002595 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002596 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002597 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002598 else:
2599 print 'No description set.'
2600 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2601 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002602
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002603 # Keep a separate copy for the commit message, because the commit message
2604 # contains the link to the Rietveld issue, while the Rietveld message contains
2605 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002606 # Keep a separate copy for the commit message.
2607 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002608 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002609
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002610 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002611 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002612 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002613 # after it. Add a period on a new line to circumvent this. Also add a space
2614 # before the period to make sure that Gitiles continues to correctly resolve
2615 # the URL.
2616 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002617 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002618 commit_desc.append_footer('Patch from %s.' % options.contributor)
2619
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002620 print('Description:')
2621 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002622
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002623 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002624 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002625 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002626
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002627 # We want to squash all this branch's commits into one commit with the proper
2628 # description. We do this by doing a "reset --soft" to the base branch (which
2629 # keeps the working copy the same), then dcommitting that. If origin/master
2630 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2631 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002632 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002633 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2634 # Delete the branches if they exist.
2635 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2636 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2637 result = RunGitWithCode(showref_cmd)
2638 if result[0] == 0:
2639 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002640
2641 # We might be in a directory that's present in this branch but not in the
2642 # trunk. Move up to the top of the tree so that git commands that expect a
2643 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002644 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002645 if rel_base_path:
2646 os.chdir(rel_base_path)
2647
2648 # Stuff our change into the merge branch.
2649 # We wrap in a try...finally block so if anything goes wrong,
2650 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002651 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002652 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002653 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002654 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002655 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002656 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002657 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002658 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002659 RunGit(
2660 [
2661 'commit', '--author', options.contributor,
2662 '-m', commit_desc.description,
2663 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002664 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002665 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002666 if base_has_submodules:
2667 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2668 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2669 RunGit(['checkout', CHERRY_PICK_BRANCH])
2670 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002671 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002672 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002673 pending_prefix = settings.GetPendingRefPrefix()
2674 if not pending_prefix or branch.startswith(pending_prefix):
2675 # If not using refs/pending/heads/* at all, or target ref is already set
2676 # to pending, then push to the target ref directly.
2677 retcode, output = RunGitWithCode(
2678 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002679 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002680 else:
2681 # Cherry-pick the change on top of pending ref and then push it.
2682 assert branch.startswith('refs/'), branch
2683 assert pending_prefix[-1] == '/', pending_prefix
2684 pending_ref = pending_prefix + branch[len('refs/'):]
2685 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002686 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002687 if retcode == 0:
2688 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002689 else:
2690 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002691 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002692 'svn', 'dcommit',
2693 '-C%s' % options.similarity,
2694 '--no-rebase', '--rmdir',
2695 ]
2696 if settings.GetForceHttpsCommitUrl():
2697 # Allow forcing https commit URLs for some projects that don't allow
2698 # committing to http URLs (like Google Code).
2699 remote_url = cl.GetGitSvnRemoteUrl()
2700 if urlparse.urlparse(remote_url).scheme == 'http':
2701 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002702 cmd_args.append('--commit-url=%s' % remote_url)
2703 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002704 if 'Committed r' in output:
2705 revision = re.match(
2706 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2707 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002708 finally:
2709 # And then swap back to the original branch and clean up.
2710 RunGit(['checkout', '-q', cl.GetBranch()])
2711 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002712 if base_has_submodules:
2713 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002714
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002715 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002716 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002717 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002718
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002719 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002720 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002721 try:
2722 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2723 # We set pushed_to_pending to False, since it made it all the way to the
2724 # real ref.
2725 pushed_to_pending = False
2726 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002727 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002728
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002729 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002730 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002731 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002732 if not to_pending:
2733 if viewvc_url and revision:
2734 change_desc.append_footer(
2735 'Committed: %s%s' % (viewvc_url, revision))
2736 elif revision:
2737 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002738 print ('Closing issue '
2739 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002740 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002741 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002742 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002743 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002744 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002745 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002746 if options.bypass_hooks:
2747 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2748 else:
2749 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002750 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002751 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002752
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002753 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002754 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2755 print 'The commit is in the pending queue (%s).' % pending_ref
2756 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002757 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002758 'footer.' % branch)
2759
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002760 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2761 if os.path.isfile(hook):
2762 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002763
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002764 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002765
2766
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002767def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2768 print
2769 print 'Waiting for commit to be landed on %s...' % real_ref
2770 print '(If you are impatient, you may Ctrl-C once without harm)'
2771 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2772 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2773
2774 loop = 0
2775 while True:
2776 sys.stdout.write('fetching (%d)... \r' % loop)
2777 sys.stdout.flush()
2778 loop += 1
2779
2780 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2781 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2782 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2783 for commit in commits.splitlines():
2784 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2785 print 'Found commit on %s' % real_ref
2786 return commit
2787
2788 current_rev = to_rev
2789
2790
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002791def PushToGitPending(remote, pending_ref, upstream_ref):
2792 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2793
2794 Returns:
2795 (retcode of last operation, output log of last operation).
2796 """
2797 assert pending_ref.startswith('refs/'), pending_ref
2798 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2799 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2800 code = 0
2801 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002802 max_attempts = 3
2803 attempts_left = max_attempts
2804 while attempts_left:
2805 if attempts_left != max_attempts:
2806 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2807 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002808
2809 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002810 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002811 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002812 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002813 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002814 print 'Fetch failed with exit code %d.' % code
2815 if out.strip():
2816 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002817 continue
2818
2819 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002820 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002821 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002822 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002823 if code:
2824 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002825 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2826 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002827 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2828 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002829 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002830 return code, out
2831
2832 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002833 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002834 code, out = RunGitWithCode(
2835 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2836 if code == 0:
2837 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002838 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002839 return code, out
2840
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002841 print 'Push failed with exit code %d.' % code
2842 if out.strip():
2843 print out.strip()
2844 if IsFatalPushFailure(out):
2845 print (
2846 'Fatal push error. Make sure your .netrc credentials and git '
2847 'user.email are correct and you have push access to the repo.')
2848 return code, out
2849
2850 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002851 return code, out
2852
2853
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002854def IsFatalPushFailure(push_stdout):
2855 """True if retrying push won't help."""
2856 return '(prohibited by Gerrit)' in push_stdout
2857
2858
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002859@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002860def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002861 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002862 if not settings.GetIsGitSvn():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002863 if get_footer_svn_id():
2864 # If it looks like previous commits were mirrored with git-svn.
2865 message = """This repository appears to be a git-svn mirror, but no
2866upstream SVN master is set. You probably need to run 'git auto-svn' once."""
2867 else:
2868 message = """This doesn't appear to be an SVN repository.
2869If your project has a true, writeable git repository, you probably want to run
2870'git cl land' instead.
2871If your project has a git mirror of an upstream SVN master, you probably need
2872to run 'git svn init'.
2873
2874Using the wrong command might cause your commit to appear to succeed, and the
2875review to be closed, without actually landing upstream. If you choose to
2876proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002877 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002878 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002879 return SendUpstream(parser, args, 'dcommit')
2880
2881
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002882@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002883def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002884 """Commits the current changelist via git."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002885 if settings.GetIsGitSvn() or get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002886 print('This appears to be an SVN repository.')
2887 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002888 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00002889 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002890 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002891
2892
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00002893def ParseIssueNum(arg):
2894 """Parses the issue number from args if present otherwise returns None."""
2895 if re.match(r'\d+', arg):
2896 return arg
2897 if arg.startswith('http'):
2898 return re.sub(r'.*/(\d+)/?', r'\1', arg)
2899 return None
2900
2901
2902@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002903def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002904 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002905 parser.add_option('-b', dest='newbranch',
2906 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002907 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002908 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002909 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2910 help='Change to the directory DIR immediately, '
2911 'before doing anything else.')
2912 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002913 help='failed patches spew .rej files rather than '
2914 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002915 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2916 help="don't commit after patch applies")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002917 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002918 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002919 auth_config = auth.extract_auth_config_from_options(options)
2920
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002921 if len(args) != 1:
2922 parser.print_help()
2923 return 1
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00002924
2925 issue_arg = ParseIssueNum(args[0])
2926 # The patch URL works because ParseIssueNum won't do any substitution
2927 # as the re.sub pattern fails to match and just returns it.
2928 if issue_arg == None:
2929 parser.print_help()
2930 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002931
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002932 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002933 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002934 return 1
2935
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002936 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002937 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002938
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002939 if options.newbranch:
2940 if options.force:
2941 RunGit(['branch', '-D', options.newbranch],
2942 stderr=subprocess2.PIPE, error_ok=True)
2943 RunGit(['checkout', '-b', options.newbranch,
2944 Changelist().GetUpstreamBranch()])
2945
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002946 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002947 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002948
2949
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002950def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00002951 # PatchIssue should never be called with a dirty tree. It is up to the
2952 # caller to check this, but just in case we assert here since the
2953 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002954 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002955
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002956 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002957 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002958 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002959 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002960 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002961 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002962 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002963 # Assume it's a URL to the patch. Default to https.
2964 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00002965 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002966 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002967 DieWithError('Must pass an issue ID or full URL for '
2968 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00002969 issue = int(match.group(2))
2970 cl = Changelist(issue=issue, auth_config=auth_config)
2971 cl.rietveld_server = match.group(1)
2972 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002973 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002974
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002975 # Switch up to the top-level directory, if necessary, in preparation for
2976 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002977 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002978 if top:
2979 os.chdir(top)
2980
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002981 # Git patches have a/ at the beginning of source paths. We strip that out
2982 # with a sed script rather than the -p flag to patch so we can feed either
2983 # Git or svn-style patches into the same apply command.
2984 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002985 try:
2986 patch_data = subprocess2.check_output(
2987 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2988 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002989 DieWithError('Git patch mungling failed.')
2990 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002991
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002992 # We use "git apply" to apply the patch instead of "patch" so that we can
2993 # pick up file adds.
2994 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002995 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002996 if directory:
2997 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002998 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002999 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003000 elif IsGitVersionAtLeast('1.7.12'):
3001 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003002 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003003 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003004 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003005 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003006 print 'Failed to apply the patch'
3007 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003008
3009 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003010 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003011 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3012 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003013 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3014 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003015 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003016 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003017 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003018 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003019 else:
3020 print "Patch applied to index."
3021 return 0
3022
3023
3024def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003025 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003026 # Provide a wrapper for git svn rebase to help avoid accidental
3027 # git svn dcommit.
3028 # It's the only command that doesn't use parser at all since we just defer
3029 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003030
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003031 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003032
3033
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003034def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003035 """Fetches the tree status and returns either 'open', 'closed',
3036 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003037 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003038 if url:
3039 status = urllib2.urlopen(url).read().lower()
3040 if status.find('closed') != -1 or status == '0':
3041 return 'closed'
3042 elif status.find('open') != -1 or status == '1':
3043 return 'open'
3044 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003045 return 'unset'
3046
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003047
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003048def GetTreeStatusReason():
3049 """Fetches the tree status from a json url and returns the message
3050 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003051 url = settings.GetTreeStatusUrl()
3052 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003053 connection = urllib2.urlopen(json_url)
3054 status = json.loads(connection.read())
3055 connection.close()
3056 return status['message']
3057
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003058
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003059def GetBuilderMaster(bot_list):
3060 """For a given builder, fetch the master from AE if available."""
3061 map_url = 'https://builders-map.appspot.com/'
3062 try:
3063 master_map = json.load(urllib2.urlopen(map_url))
3064 except urllib2.URLError as e:
3065 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3066 (map_url, e))
3067 except ValueError as e:
3068 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3069 if not master_map:
3070 return None, 'Failed to build master map.'
3071
3072 result_master = ''
3073 for bot in bot_list:
3074 builder = bot.split(':', 1)[0]
3075 master_list = master_map.get(builder, [])
3076 if not master_list:
3077 return None, ('No matching master for builder %s.' % builder)
3078 elif len(master_list) > 1:
3079 return None, ('The builder name %s exists in multiple masters %s.' %
3080 (builder, master_list))
3081 else:
3082 cur_master = master_list[0]
3083 if not result_master:
3084 result_master = cur_master
3085 elif result_master != cur_master:
3086 return None, 'The builders do not belong to the same master.'
3087 return result_master, None
3088
3089
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003090def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003091 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003092 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003093 status = GetTreeStatus()
3094 if 'unset' == status:
3095 print 'You must configure your tree status URL by running "git cl config".'
3096 return 2
3097
3098 print "The tree is %s" % status
3099 print
3100 print GetTreeStatusReason()
3101 if status != 'open':
3102 return 1
3103 return 0
3104
3105
maruel@chromium.org15192402012-09-06 12:38:29 +00003106def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003107 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003108 group = optparse.OptionGroup(parser, "Try job options")
3109 group.add_option(
3110 "-b", "--bot", action="append",
3111 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3112 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003113 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003114 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003115 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003116 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003117 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003118 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003119 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003120 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003121 "-r", "--revision",
3122 help="Revision to use for the try job; default: the "
3123 "revision will be determined by the try server; see "
3124 "its waterfall for more info")
3125 group.add_option(
3126 "-c", "--clobber", action="store_true", default=False,
3127 help="Force a clobber before building; e.g. don't do an "
3128 "incremental build")
3129 group.add_option(
3130 "--project",
3131 help="Override which project to use. Projects are defined "
3132 "server-side to define what default bot set to use")
3133 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003134 "-p", "--property", dest="properties", action="append", default=[],
3135 help="Specify generic properties in the form -p key1=value1 -p "
3136 "key2=value2 etc (buildbucket only). The value will be treated as "
3137 "json if decodable, or as string otherwise.")
3138 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003139 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003140 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003141 "--use-rietveld", action="store_true", default=False,
3142 help="Use Rietveld to trigger try jobs.")
3143 group.add_option(
3144 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3145 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003146 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003147 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003148 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003149 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003150
machenbach@chromium.org45453142015-09-15 08:45:22 +00003151 if options.use_rietveld and options.properties:
3152 parser.error('Properties can only be specified with buildbucket')
3153
3154 # Make sure that all properties are prop=value pairs.
3155 bad_params = [x for x in options.properties if '=' not in x]
3156 if bad_params:
3157 parser.error('Got properties with missing "=": %s' % bad_params)
3158
maruel@chromium.org15192402012-09-06 12:38:29 +00003159 if args:
3160 parser.error('Unknown arguments: %s' % args)
3161
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003162 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003163 if not cl.GetIssue():
3164 parser.error('Need to upload first')
3165
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003166 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003167 if props.get('closed'):
3168 parser.error('Cannot send tryjobs for a closed CL')
3169
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003170 if props.get('private'):
3171 parser.error('Cannot use trybots with private issue')
3172
maruel@chromium.org15192402012-09-06 12:38:29 +00003173 if not options.name:
3174 options.name = cl.GetBranch()
3175
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003176 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003177 options.master, err_msg = GetBuilderMaster(options.bot)
3178 if err_msg:
3179 parser.error('Tryserver master cannot be found because: %s\n'
3180 'Please manually specify the tryserver master'
3181 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003182
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003183 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003184 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003185 if not options.bot:
3186 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003187
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003188 # Get try masters from PRESUBMIT.py files.
3189 masters = presubmit_support.DoGetTryMasters(
3190 change,
3191 change.LocalPaths(),
3192 settings.GetRoot(),
3193 None,
3194 None,
3195 options.verbose,
3196 sys.stdout)
3197 if masters:
3198 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003199
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003200 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3201 options.bot = presubmit_support.DoGetTrySlaves(
3202 change,
3203 change.LocalPaths(),
3204 settings.GetRoot(),
3205 None,
3206 None,
3207 options.verbose,
3208 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003209
3210 if not options.bot:
3211 # Get try masters from cq.cfg if any.
3212 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3213 # location.
3214 cq_cfg = os.path.join(change.RepositoryRoot(),
3215 'infra', 'config', 'cq.cfg')
3216 if os.path.exists(cq_cfg):
3217 masters = {}
3218 cq_masters = commit_queue.get_master_builder_map(cq_cfg)
3219 for master, builders in cq_masters.iteritems():
3220 for builder in builders:
3221 # Skip presubmit builders, because these will fail without LGTM.
3222 if 'presubmit' not in builder.lower():
3223 masters.setdefault(master, {})[builder] = ['defaulttests']
3224 if masters:
3225 return masters
3226
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003227 if not options.bot:
3228 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003229
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003230 builders_and_tests = {}
3231 # TODO(machenbach): The old style command-line options don't support
3232 # multiple try masters yet.
3233 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3234 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3235
3236 for bot in old_style:
3237 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003238 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003239 elif ',' in bot:
3240 parser.error('Specify one bot per --bot flag')
3241 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003242 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003243
3244 for bot, tests in new_style:
3245 builders_and_tests.setdefault(bot, []).extend(tests)
3246
3247 # Return a master map with one master to be backwards compatible. The
3248 # master name defaults to an empty string, which will cause the master
3249 # not to be set on rietveld (deprecated).
3250 return {options.master: builders_and_tests}
3251
3252 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003253
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003254 for builders in masters.itervalues():
3255 if any('triggered' in b for b in builders):
3256 print >> sys.stderr, (
3257 'ERROR You are trying to send a job to a triggered bot. This type of'
3258 ' bot requires an\ninitial job from a parent (usually a builder). '
3259 'Instead send your job to the parent.\n'
3260 'Bot list: %s' % builders)
3261 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003262
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003263 patchset = cl.GetMostRecentPatchset()
3264 if patchset and patchset != cl.GetPatchset():
3265 print(
3266 '\nWARNING Mismatch between local config and server. Did a previous '
3267 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3268 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003269 if options.luci:
3270 trigger_luci_job(cl, masters, options)
3271 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003272 try:
3273 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3274 except BuildbucketResponseException as ex:
3275 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003276 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003277 except Exception as e:
3278 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3279 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3280 e, stacktrace)
3281 return 1
3282 else:
3283 try:
3284 cl.RpcServer().trigger_distributed_try_jobs(
3285 cl.GetIssue(), patchset, options.name, options.clobber,
3286 options.revision, masters)
3287 except urllib2.HTTPError as e:
3288 if e.code == 404:
3289 print('404 from rietveld; '
3290 'did you mean to use "git try" instead of "git cl try"?')
3291 return 1
3292 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003293
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003294 for (master, builders) in sorted(masters.iteritems()):
3295 if master:
3296 print 'Master: %s' % master
3297 length = max(len(builder) for builder in builders)
3298 for builder in sorted(builders):
3299 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003300 return 0
3301
3302
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003303@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003304def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003305 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003306 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003307 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003308 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003309
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003310 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003311 if args:
3312 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003313 branch = cl.GetBranch()
3314 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003315 cl = Changelist()
3316 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003317
3318 # Clear configured merge-base, if there is one.
3319 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003320 else:
3321 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003322 return 0
3323
3324
thestig@chromium.org00858c82013-12-02 23:08:03 +00003325def CMDweb(parser, args):
3326 """Opens the current CL in the web browser."""
3327 _, args = parser.parse_args(args)
3328 if args:
3329 parser.error('Unrecognized args: %s' % ' '.join(args))
3330
3331 issue_url = Changelist().GetIssueURL()
3332 if not issue_url:
3333 print >> sys.stderr, 'ERROR No issue to open'
3334 return 1
3335
3336 webbrowser.open(issue_url)
3337 return 0
3338
3339
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003340def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003341 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003342 auth.add_auth_options(parser)
3343 options, args = parser.parse_args(args)
3344 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003345 if args:
3346 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003347 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003348 props = cl.GetIssueProperties()
3349 if props.get('private'):
3350 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003351 cl.SetFlag('commit', '1')
3352 return 0
3353
3354
groby@chromium.org411034a2013-02-26 15:12:01 +00003355def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003356 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003357 auth.add_auth_options(parser)
3358 options, args = parser.parse_args(args)
3359 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003360 if args:
3361 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003362 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003363 # Ensure there actually is an issue to close.
3364 cl.GetDescription()
3365 cl.CloseIssue()
3366 return 0
3367
3368
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003369def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003370 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003371 auth.add_auth_options(parser)
3372 options, args = parser.parse_args(args)
3373 auth_config = auth.extract_auth_config_from_options(options)
3374 if args:
3375 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003376
3377 # Uncommitted (staged and unstaged) changes will be destroyed by
3378 # "git reset --hard" if there are merging conflicts in PatchIssue().
3379 # Staged changes would be committed along with the patch from last
3380 # upload, hence counted toward the "last upload" side in the final
3381 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003382 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003383 return 1
3384
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003385 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003386 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003387 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003388 if not issue:
3389 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003390 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003391 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003392
3393 # Create a new branch based on the merge-base
3394 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3395 try:
3396 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003397 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003398 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003399 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003400 return rtn
3401
wychen@chromium.org06928532015-02-03 02:11:29 +00003402 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003403 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003404 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003405 finally:
3406 RunGit(['checkout', '-q', branch])
3407 RunGit(['branch', '-D', TMP_BRANCH])
3408
3409 return 0
3410
3411
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003412def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003413 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003414 parser.add_option(
3415 '--no-color',
3416 action='store_true',
3417 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003418 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003419 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003420 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003421
3422 author = RunGit(['config', 'user.email']).strip() or None
3423
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003424 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003425
3426 if args:
3427 if len(args) > 1:
3428 parser.error('Unknown args')
3429 base_branch = args[0]
3430 else:
3431 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003432 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003433
3434 change = cl.GetChange(base_branch, None)
3435 return owners_finder.OwnersFinder(
3436 [f.LocalPath() for f in
3437 cl.GetChange(base_branch, None).AffectedFiles()],
3438 change.RepositoryRoot(), author,
3439 fopen=file, os_path=os.path, glob=glob.glob,
3440 disable_color=options.no_color).run()
3441
3442
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003443def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
3444 """Generates a diff command."""
3445 # Generate diff for the current branch's changes.
3446 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3447 upstream_commit, '--' ]
3448
3449 if args:
3450 for arg in args:
3451 if os.path.isdir(arg):
3452 diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
3453 elif os.path.isfile(arg):
3454 diff_cmd.append(arg)
3455 else:
3456 DieWithError('Argument "%s" is not a file or a directory' % arg)
3457 else:
3458 diff_cmd.extend('*' + ext for ext in extensions)
3459
3460 return diff_cmd
3461
3462
enne@chromium.org555cfe42014-01-29 18:21:39 +00003463@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003464def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003465 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003466 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003467 parser.add_option('--full', action='store_true',
3468 help='Reformat the full content of all touched files')
3469 parser.add_option('--dry-run', action='store_true',
3470 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003471 parser.add_option('--python', action='store_true',
3472 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003473 parser.add_option('--diff', action='store_true',
3474 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003475 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003476
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003477 # git diff generates paths against the root of the repository. Change
3478 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003479 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003480 if rel_base_path:
3481 os.chdir(rel_base_path)
3482
digit@chromium.org29e47272013-05-17 17:01:46 +00003483 # Grab the merge-base commit, i.e. the upstream commit of the current
3484 # branch when it was created or the last time it was rebased. This is
3485 # to cover the case where the user may have called "git fetch origin",
3486 # moving the origin branch to a newer commit, but hasn't rebased yet.
3487 upstream_commit = None
3488 cl = Changelist()
3489 upstream_branch = cl.GetUpstreamBranch()
3490 if upstream_branch:
3491 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3492 upstream_commit = upstream_commit.strip()
3493
3494 if not upstream_commit:
3495 DieWithError('Could not find base commit for this branch. '
3496 'Are you in detached state?')
3497
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003498 if opts.full:
3499 # Only list the names of modified files.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003500 diff_type = '--name-only'
enne@chromium.org555cfe42014-01-29 18:21:39 +00003501 else:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003502 # Only generate context-less patches.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003503 diff_type = '-U0'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003504
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003505 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, CLANG_EXTS)
digit@chromium.org29e47272013-05-17 17:01:46 +00003506 diff_output = RunGit(diff_cmd)
3507
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003508 top_dir = os.path.normpath(
3509 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3510
3511 # Locate the clang-format binary in the checkout
3512 try:
3513 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3514 except clang_format.NotFoundError, e:
3515 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003516
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003517 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3518 # formatted. This is used to block during the presubmit.
3519 return_value = 0
3520
digit@chromium.org29e47272013-05-17 17:01:46 +00003521 if opts.full:
3522 # diff_output is a list of files to send to clang-format.
3523 files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003524 if files:
3525 cmd = [clang_format_tool]
3526 if not opts.dry_run and not opts.diff:
3527 cmd.append('-i')
3528 stdout = RunCommand(cmd + files, cwd=top_dir)
3529 if opts.diff:
3530 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003531 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003532 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003533 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00003534 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003535 try:
3536 script = clang_format.FindClangFormatScriptInChromiumTree(
3537 'clang-format-diff.py')
3538 except clang_format.NotFoundError, e:
3539 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003540
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003541 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003542 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003543 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003544
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003545 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003546 if opts.diff:
3547 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003548 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003549 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003550
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003551 # Similar code to above, but using yapf on .py files rather than clang-format
3552 # on C/C++ files
3553 if opts.python:
3554 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, ['.py'])
3555 diff_output = RunGit(diff_cmd)
3556 yapf_tool = gclient_utils.FindExecutable('yapf')
3557 if yapf_tool is None:
3558 DieWithError('yapf not found in PATH')
3559
3560 if opts.full:
3561 files = diff_output.splitlines()
3562 if files:
3563 cmd = [yapf_tool]
3564 if not opts.dry_run and not opts.diff:
3565 cmd.append('-i')
3566 stdout = RunCommand(cmd + files, cwd=top_dir)
3567 if opts.diff:
3568 sys.stdout.write(stdout)
3569 else:
3570 # TODO(sbc): yapf --lines mode still has some issues.
3571 # https://github.com/google/yapf/issues/154
3572 DieWithError('--python currently only works with --full')
3573
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003574 # Build a diff command that only operates on dart files. dart's formatter
3575 # does not have the nice property of only operating on modified chunks, so
3576 # hard code full.
3577 dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3578 args, ['.dart'])
3579 dart_diff_output = RunGit(dart_diff_cmd)
3580 if dart_diff_output:
3581 try:
3582 command = [dart_format.FindDartFmtToolInChromiumTree()]
3583 if not opts.dry_run and not opts.diff:
3584 command.append('-w')
3585 command.extend(dart_diff_output.splitlines())
3586
3587 stdout = RunCommand(command, cwd=top_dir, env=env)
3588 if opts.dry_run and stdout:
3589 return_value = 2
3590 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00003591 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
3592 'found in this checkout. Files in other languages are still ' +
3593 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003594
3595 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003596
3597
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003598@subcommand.usage('<codereview url or issue id>')
3599def CMDcheckout(parser, args):
3600 """Checks out a branch associated with a given Rietveld issue."""
3601 _, args = parser.parse_args(args)
3602
3603 if len(args) != 1:
3604 parser.print_help()
3605 return 1
3606
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003607 target_issue = ParseIssueNum(args[0])
3608 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003609 parser.print_help()
3610 return 1
3611
3612 key_and_issues = [x.split() for x in RunGit(
3613 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
3614 .splitlines()]
3615 branches = []
3616 for key, issue in key_and_issues:
3617 if issue == target_issue:
3618 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
3619
3620 if len(branches) == 0:
3621 print 'No branch found for issue %s.' % target_issue
3622 return 1
3623 if len(branches) == 1:
3624 RunGit(['checkout', branches[0]])
3625 else:
3626 print 'Multiple branches match issue %s:' % target_issue
3627 for i in range(len(branches)):
3628 print '%d: %s' % (i, branches[i])
3629 which = raw_input('Choose by index: ')
3630 try:
3631 RunGit(['checkout', branches[int(which)]])
3632 except (IndexError, ValueError):
3633 print 'Invalid selection, not checking out any branch.'
3634 return 1
3635
3636 return 0
3637
3638
maruel@chromium.org29404b52014-09-08 22:58:00 +00003639def CMDlol(parser, args):
3640 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003641 print zlib.decompress(base64.b64decode(
3642 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3643 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3644 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3645 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003646 return 0
3647
3648
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003649class OptionParser(optparse.OptionParser):
3650 """Creates the option parse and add --verbose support."""
3651 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003652 optparse.OptionParser.__init__(
3653 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003654 self.add_option(
3655 '-v', '--verbose', action='count', default=0,
3656 help='Use 2 times for more debugging info')
3657
3658 def parse_args(self, args=None, values=None):
3659 options, args = optparse.OptionParser.parse_args(self, args, values)
3660 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3661 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3662 return options, args
3663
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003664
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003665def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003666 if sys.hexversion < 0x02060000:
3667 print >> sys.stderr, (
3668 '\nYour python version %s is unsupported, please upgrade.\n' %
3669 sys.version.split(' ', 1)[0])
3670 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003671
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003672 # Reload settings.
3673 global settings
3674 settings = Settings()
3675
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003676 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003677 dispatcher = subcommand.CommandDispatcher(__name__)
3678 try:
3679 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003680 except auth.AuthenticationError as e:
3681 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003682 except urllib2.HTTPError, e:
3683 if e.code != 500:
3684 raise
3685 DieWithError(
3686 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3687 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003688 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003689
3690
3691if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003692 # These affect sys.stdout so do it outside of main() to simplify mocks in
3693 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003694 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003695 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003696 try:
3697 sys.exit(main(sys.argv[1:]))
3698 except KeyboardInterrupt:
3699 sys.stderr.write('interrupted\n')
3700 sys.exit(1)