blob: ef974d50f33f07faf2a5e7cb1049cc5813649d4c [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
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000028import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000030import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000032import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000033import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034
35try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000036 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037except ImportError:
38 pass
39
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000043import auth
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +000044from luci_hacks import trigger_luci_job as luci_trigger
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000045import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000046import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000047import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000050import git_common
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +000051from git_footers import get_footer_svn_id
piman@chromium.org336f9122014-09-04 02:16:55 +000052import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000054import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000055import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000057import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000058import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import watchlists
60
maruel@chromium.org0633fb42013-08-16 20:06:14 +000061__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000063DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000064POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000065DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000066GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000067CHANGE_ID = 'Change-Id:'
rmistry@google.comc68112d2015-03-03 12:48:06 +000068REFS_THAT_ALIAS_TO_OTHER_REFS = {
69 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
70 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
71}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000072
thestig@chromium.org44202a22014-03-11 19:22:18 +000073# Valid extensions for files we want to lint.
74DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
75DEFAULT_LINT_IGNORE_REGEX = r"$^"
76
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000077# Shortcut since it quickly becomes redundant.
78Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000079
maruel@chromium.orgddd59412011-11-30 14:20:38 +000080# Initialized in main()
81settings = None
82
83
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000084def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000085 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086 sys.exit(1)
87
88
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000089def GetNoGitPagerEnv():
90 env = os.environ.copy()
91 # 'cat' is a magical git string that disables pagers on all platforms.
92 env['GIT_PAGER'] = 'cat'
93 return env
94
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000095
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000096def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000097 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000098 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000099 except subprocess2.CalledProcessError as e:
100 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000101 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 'Command "%s" failed.\n%s' % (
104 ' '.join(args), error_message or e.stdout or ''))
105 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000106
107
108def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000109 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000110 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000111
112
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000113def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000114 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000115 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000116 if suppress_stderr:
117 stderr = subprocess2.VOID
118 else:
119 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000120 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000121 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000122 stdout=subprocess2.PIPE,
123 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000124 return code, out[0]
125 except ValueError:
126 # When the subprocess fails, it returns None. That triggers a ValueError
127 # when trying to unpack the return value into (out, code).
128 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000129
130
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000131def RunGitSilent(args):
132 """Returns stdout, suppresses stderr and ingores the return code."""
133 return RunGitWithCode(args, suppress_stderr=True)[1]
134
135
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000136def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000137 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 return (version.startswith(prefix) and
140 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000141
142
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000143def BranchExists(branch):
144 """Return True if specified branch exists."""
145 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
146 suppress_stderr=True)
147 return not code
148
149
maruel@chromium.org90541732011-04-01 17:54:18 +0000150def ask_for_data(prompt):
151 try:
152 return raw_input(prompt)
153 except KeyboardInterrupt:
154 # Hide the exception.
155 sys.exit(1)
156
157
iannucci@chromium.org79540052012-10-19 23:15:26 +0000158def git_set_branch_value(key, value):
159 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000160 if not branch:
161 return
162
163 cmd = ['config']
164 if isinstance(value, int):
165 cmd.append('--int')
166 git_key = 'branch.%s.%s' % (branch, key)
167 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000168
169
170def git_get_branch_default(key, default):
171 branch = Changelist().GetBranch()
172 if branch:
173 git_key = 'branch.%s.%s' % (branch, key)
174 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
175 try:
176 return int(stdout.strip())
177 except ValueError:
178 pass
179 return default
180
181
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000182def add_git_similarity(parser):
183 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000184 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000185 help='Sets the percentage that a pair of files need to match in order to'
186 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000187 parser.add_option(
188 '--find-copies', action='store_true',
189 help='Allows git to look for copies.')
190 parser.add_option(
191 '--no-find-copies', action='store_false', dest='find_copies',
192 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000193
194 old_parser_args = parser.parse_args
195 def Parse(args):
196 options, args = old_parser_args(args)
197
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000198 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000199 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000200 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 print('Note: Saving similarity of %d%% in git config.'
202 % options.similarity)
203 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000204
iannucci@chromium.org79540052012-10-19 23:15:26 +0000205 options.similarity = max(0, min(options.similarity, 100))
206
207 if options.find_copies is None:
208 options.find_copies = bool(
209 git_get_branch_default('git-find-copies', True))
210 else:
211 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000212
213 print('Using %d%% similarity for rename/copy detection. '
214 'Override with --similarity.' % options.similarity)
215
216 return options, args
217 parser.parse_args = Parse
218
219
machenbach@chromium.org45453142015-09-15 08:45:22 +0000220def _get_properties_from_options(options):
221 properties = dict(x.split('=', 1) for x in options.properties)
222 for key, val in properties.iteritems():
223 try:
224 properties[key] = json.loads(val)
225 except ValueError:
226 pass # If a value couldn't be evaluated, treat it as a string.
227 return properties
228
229
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000230def _prefix_master(master):
231 """Convert user-specified master name to full master name.
232
233 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
234 name, while the developers always use shortened master name
235 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
236 function does the conversion for buildbucket migration.
237 """
238 prefix = 'master.'
239 if master.startswith(prefix):
240 return master
241 return '%s%s' % (prefix, master)
242
243
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000244def _buildbucket_retry(operation_name, http, *args, **kwargs):
245 """Retries requests to buildbucket service and returns parsed json content."""
246 try_count = 0
247 while True:
248 response, content = http.request(*args, **kwargs)
249 try:
250 content_json = json.loads(content)
251 except ValueError:
252 content_json = None
253
254 # Buildbucket could return an error even if status==200.
255 if content_json and content_json.get('error'):
256 msg = 'Error in response. Reason: %s. Message: %s.' % (
257 content_json['error'].get('reason', ''),
258 content_json['error'].get('message', ''))
259 raise BuildbucketResponseException(msg)
260
261 if response.status == 200:
262 if not content_json:
263 raise BuildbucketResponseException(
264 'Buildbucket returns invalid json content: %s.\n'
265 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
266 content)
267 return content_json
268 if response.status < 500 or try_count >= 2:
269 raise httplib2.HttpLib2Error(content)
270
271 # status >= 500 means transient failures.
272 logging.debug('Transient errors when %s. Will retry.', operation_name)
273 time.sleep(0.5 + 1.5*try_count)
274 try_count += 1
275 assert False, 'unreachable'
276
277
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000278def trigger_luci_job(changelist, masters, options):
279 """Send a job to run on LUCI."""
280 issue_props = changelist.GetIssueProperties()
281 issue = changelist.GetIssue()
282 patchset = changelist.GetMostRecentPatchset()
283 for builders_and_tests in sorted(masters.itervalues()):
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000284 # TODO(hinoka et al): add support for other properties.
285 # Currently, this completely ignores testfilter and other properties.
286 for builder in sorted(builders_and_tests):
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +0000287 luci_trigger.trigger(
288 builder, 'HEAD', issue, patchset, issue_props['project'])
289
290
machenbach@chromium.org45453142015-09-15 08:45:22 +0000291def trigger_try_jobs(auth_config, changelist, options, masters, category):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000292 rietveld_url = settings.GetDefaultServerUrl()
293 rietveld_host = urlparse.urlparse(rietveld_url).hostname
294 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
295 http = authenticator.authorize(httplib2.Http())
296 http.force_exception_to_status_code = True
297 issue_props = changelist.GetIssueProperties()
298 issue = changelist.GetIssue()
299 patchset = changelist.GetMostRecentPatchset()
machenbach@chromium.org45453142015-09-15 08:45:22 +0000300 properties = _get_properties_from_options(options)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000301
302 buildbucket_put_url = (
303 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000304 hostname=options.buildbucket_host))
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000305 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
306 hostname=rietveld_host,
307 issue=issue,
308 patch=patchset)
309
310 batch_req_body = {'builds': []}
311 print_text = []
312 print_text.append('Tried jobs on:')
313 for master, builders_and_tests in sorted(masters.iteritems()):
314 print_text.append('Master: %s' % master)
315 bucket = _prefix_master(master)
316 for builder, tests in sorted(builders_and_tests.iteritems()):
317 print_text.append(' %s: %s' % (builder, tests))
318 parameters = {
319 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000320 'changes': [{
321 'author': {'email': issue_props['owner_email']},
322 'revision': options.revision,
323 }],
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000324 'properties': {
325 'category': category,
326 'issue': issue,
327 'master': master,
328 'patch_project': issue_props['project'],
329 'patch_storage': 'rietveld',
330 'patchset': patchset,
331 'reason': options.name,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000332 'rietveld': rietveld_url,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000333 },
334 }
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000335 if tests:
336 parameters['properties']['testfilter'] = tests
machenbach@chromium.org45453142015-09-15 08:45:22 +0000337 if properties:
338 parameters['properties'].update(properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000339 if options.clobber:
340 parameters['properties']['clobber'] = True
341 batch_req_body['builds'].append(
342 {
343 'bucket': bucket,
344 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000345 'client_operation_id': str(uuid.uuid4()),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000346 'tags': ['builder:%s' % builder,
347 'buildset:%s' % buildset,
348 'master:%s' % master,
349 'user_agent:git_cl_try']
350 }
351 )
352
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000353 _buildbucket_retry(
354 'triggering tryjobs',
355 http,
356 buildbucket_put_url,
357 'PUT',
358 body=json.dumps(batch_req_body),
359 headers={'Content-Type': 'application/json'}
360 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000361 print_text.append('To see results here, run: git cl try-results')
362 print_text.append('To see results in browser, run: git cl web')
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000363 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000364
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000365
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000366def fetch_try_jobs(auth_config, changelist, options):
367 """Fetches tryjobs from buildbucket.
368
369 Returns a map from build id to build info as json dictionary.
370 """
371 rietveld_url = settings.GetDefaultServerUrl()
372 rietveld_host = urlparse.urlparse(rietveld_url).hostname
373 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
374 if authenticator.has_cached_credentials():
375 http = authenticator.authorize(httplib2.Http())
376 else:
377 print ('Warning: Some results might be missing because %s' %
378 # Get the message on how to login.
379 auth.LoginRequiredError(rietveld_host).message)
380 http = httplib2.Http()
381
382 http.force_exception_to_status_code = True
383
384 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
385 hostname=rietveld_host,
386 issue=changelist.GetIssue(),
387 patch=options.patchset)
388 params = {'tag': 'buildset:%s' % buildset}
389
390 builds = {}
391 while True:
392 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
393 hostname=options.buildbucket_host,
394 params=urllib.urlencode(params))
395 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
396 for build in content.get('builds', []):
397 builds[build['id']] = build
398 if 'next_cursor' in content:
399 params['start_cursor'] = content['next_cursor']
400 else:
401 break
402 return builds
403
404
405def print_tryjobs(options, builds):
406 """Prints nicely result of fetch_try_jobs."""
407 if not builds:
408 print 'No tryjobs scheduled'
409 return
410
411 # Make a copy, because we'll be modifying builds dictionary.
412 builds = builds.copy()
413 builder_names_cache = {}
414
415 def get_builder(b):
416 try:
417 return builder_names_cache[b['id']]
418 except KeyError:
419 try:
420 parameters = json.loads(b['parameters_json'])
421 name = parameters['builder_name']
422 except (ValueError, KeyError) as error:
423 print 'WARNING: failed to get builder name for build %s: %s' % (
424 b['id'], error)
425 name = None
426 builder_names_cache[b['id']] = name
427 return name
428
429 def get_bucket(b):
430 bucket = b['bucket']
431 if bucket.startswith('master.'):
432 return bucket[len('master.'):]
433 return bucket
434
435 if options.print_master:
436 name_fmt = '%%-%ds %%-%ds' % (
437 max(len(str(get_bucket(b))) for b in builds.itervalues()),
438 max(len(str(get_builder(b))) for b in builds.itervalues()))
439 def get_name(b):
440 return name_fmt % (get_bucket(b), get_builder(b))
441 else:
442 name_fmt = '%%-%ds' % (
443 max(len(str(get_builder(b))) for b in builds.itervalues()))
444 def get_name(b):
445 return name_fmt % get_builder(b)
446
447 def sort_key(b):
448 return b['status'], b.get('result'), get_name(b), b.get('url')
449
450 def pop(title, f, color=None, **kwargs):
451 """Pop matching builds from `builds` dict and print them."""
452
453 if not sys.stdout.isatty() or color is None:
454 colorize = str
455 else:
456 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
457
458 result = []
459 for b in builds.values():
460 if all(b.get(k) == v for k, v in kwargs.iteritems()):
461 builds.pop(b['id'])
462 result.append(b)
463 if result:
464 print colorize(title)
465 for b in sorted(result, key=sort_key):
466 print ' ', colorize('\t'.join(map(str, f(b))))
467
468 total = len(builds)
469 pop(status='COMPLETED', result='SUCCESS',
470 title='Successes:', color=Fore.GREEN,
471 f=lambda b: (get_name(b), b.get('url')))
472 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
473 title='Infra Failures:', color=Fore.MAGENTA,
474 f=lambda b: (get_name(b), b.get('url')))
475 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
476 title='Failures:', color=Fore.RED,
477 f=lambda b: (get_name(b), b.get('url')))
478 pop(status='COMPLETED', result='CANCELED',
479 title='Canceled:', color=Fore.MAGENTA,
480 f=lambda b: (get_name(b),))
481 pop(status='COMPLETED', result='FAILURE',
482 failure_reason='INVALID_BUILD_DEFINITION',
483 title='Wrong master/builder name:', color=Fore.MAGENTA,
484 f=lambda b: (get_name(b),))
485 pop(status='COMPLETED', result='FAILURE',
486 title='Other failures:',
487 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
488 pop(status='COMPLETED',
489 title='Other finished:',
490 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
491 pop(status='STARTED',
492 title='Started:', color=Fore.YELLOW,
493 f=lambda b: (get_name(b), b.get('url')))
494 pop(status='SCHEDULED',
495 title='Scheduled:',
496 f=lambda b: (get_name(b), 'id=%s' % b['id']))
497 # The last section is just in case buildbucket API changes OR there is a bug.
498 pop(title='Other:',
499 f=lambda b: (get_name(b), 'id=%s' % b['id']))
500 assert len(builds) == 0
501 print 'Total: %d tryjobs' % total
502
503
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000504def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
505 """Return the corresponding git ref if |base_url| together with |glob_spec|
506 matches the full |url|.
507
508 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
509 """
510 fetch_suburl, as_ref = glob_spec.split(':')
511 if allow_wildcards:
512 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
513 if glob_match:
514 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
515 # "branches/{472,597,648}/src:refs/remotes/svn/*".
516 branch_re = re.escape(base_url)
517 if glob_match.group(1):
518 branch_re += '/' + re.escape(glob_match.group(1))
519 wildcard = glob_match.group(2)
520 if wildcard == '*':
521 branch_re += '([^/]*)'
522 else:
523 # Escape and replace surrounding braces with parentheses and commas
524 # with pipe symbols.
525 wildcard = re.escape(wildcard)
526 wildcard = re.sub('^\\\\{', '(', wildcard)
527 wildcard = re.sub('\\\\,', '|', wildcard)
528 wildcard = re.sub('\\\\}$', ')', wildcard)
529 branch_re += wildcard
530 if glob_match.group(3):
531 branch_re += re.escape(glob_match.group(3))
532 match = re.match(branch_re, url)
533 if match:
534 return re.sub('\*$', match.group(1), as_ref)
535
536 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
537 if fetch_suburl:
538 full_url = base_url + '/' + fetch_suburl
539 else:
540 full_url = base_url
541 if full_url == url:
542 return as_ref
543 return None
544
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000545
iannucci@chromium.org79540052012-10-19 23:15:26 +0000546def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000547 """Prints statistics about the change to the user."""
548 # --no-ext-diff is broken in some versions of Git, so try to work around
549 # this by overriding the environment (but there is still a problem if the
550 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000551 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000552 if 'GIT_EXTERNAL_DIFF' in env:
553 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000554
555 if find_copies:
556 similarity_options = ['--find-copies-harder', '-l100000',
557 '-C%s' % similarity]
558 else:
559 similarity_options = ['-M%s' % similarity]
560
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000561 try:
562 stdout = sys.stdout.fileno()
563 except AttributeError:
564 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000565 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000566 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000567 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000568 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000569
570
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000571class BuildbucketResponseException(Exception):
572 pass
573
574
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000575class Settings(object):
576 def __init__(self):
577 self.default_server = None
578 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000579 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000580 self.is_git_svn = None
581 self.svn_branch = None
582 self.tree_status_url = None
583 self.viewvc_url = None
584 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000585 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000586 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000587 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000588 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000589 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000590 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000591
592 def LazyUpdateIfNeeded(self):
593 """Updates the settings from a codereview.settings file, if available."""
594 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000595 # The only value that actually changes the behavior is
596 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000597 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000598 error_ok=True
599 ).strip().lower()
600
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000601 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000602 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000603 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000604 # set updated to True to avoid infinite calling loop
605 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000606 self.updated = True
607 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 self.updated = True
609
610 def GetDefaultServerUrl(self, error_ok=False):
611 if not self.default_server:
612 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000613 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000614 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000615 if error_ok:
616 return self.default_server
617 if not self.default_server:
618 error_message = ('Could not find settings file. You must configure '
619 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000620 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000621 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000622 return self.default_server
623
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000624 @staticmethod
625 def GetRelativeRoot():
626 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000627
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000628 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000629 if self.root is None:
630 self.root = os.path.abspath(self.GetRelativeRoot())
631 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000632
633 def GetIsGitSvn(self):
634 """Return true if this repo looks like it's using git-svn."""
635 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000636 if self.GetPendingRefPrefix():
637 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
638 self.is_git_svn = False
639 else:
640 # If you have any "svn-remote.*" config keys, we think you're using svn.
641 self.is_git_svn = RunGitWithCode(
642 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000643 return self.is_git_svn
644
645 def GetSVNBranch(self):
646 if self.svn_branch is None:
647 if not self.GetIsGitSvn():
648 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
649
650 # Try to figure out which remote branch we're based on.
651 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000652 # 1) iterate through our branch history and find the svn URL.
653 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000654
655 # regexp matching the git-svn line that contains the URL.
656 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
657
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000658 # We don't want to go through all of history, so read a line from the
659 # pipe at a time.
660 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000661 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000662 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
663 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000664 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000665 for line in proc.stdout:
666 match = git_svn_re.match(line)
667 if match:
668 url = match.group(1)
669 proc.stdout.close() # Cut pipe.
670 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000671
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000672 if url:
673 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
674 remotes = RunGit(['config', '--get-regexp',
675 r'^svn-remote\..*\.url']).splitlines()
676 for remote in remotes:
677 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000678 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000679 remote = match.group(1)
680 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000681 rewrite_root = RunGit(
682 ['config', 'svn-remote.%s.rewriteRoot' % remote],
683 error_ok=True).strip()
684 if rewrite_root:
685 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000686 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000687 ['config', 'svn-remote.%s.fetch' % remote],
688 error_ok=True).strip()
689 if fetch_spec:
690 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
691 if self.svn_branch:
692 break
693 branch_spec = RunGit(
694 ['config', 'svn-remote.%s.branches' % remote],
695 error_ok=True).strip()
696 if branch_spec:
697 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
698 if self.svn_branch:
699 break
700 tag_spec = RunGit(
701 ['config', 'svn-remote.%s.tags' % remote],
702 error_ok=True).strip()
703 if tag_spec:
704 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
705 if self.svn_branch:
706 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000707
708 if not self.svn_branch:
709 DieWithError('Can\'t guess svn branch -- try specifying it on the '
710 'command line')
711
712 return self.svn_branch
713
714 def GetTreeStatusUrl(self, error_ok=False):
715 if not self.tree_status_url:
716 error_message = ('You must configure your tree status URL by running '
717 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000718 self.tree_status_url = self._GetRietveldConfig(
719 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000720 return self.tree_status_url
721
722 def GetViewVCUrl(self):
723 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000724 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725 return self.viewvc_url
726
rmistry@google.com90752582014-01-14 21:04:50 +0000727 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000728 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000729
rmistry@google.com78948ed2015-07-08 23:09:57 +0000730 def GetIsSkipDependencyUpload(self, branch_name):
731 """Returns true if specified branch should skip dep uploads."""
732 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
733 error_ok=True)
734
rmistry@google.com5626a922015-02-26 14:03:30 +0000735 def GetRunPostUploadHook(self):
736 run_post_upload_hook = self._GetRietveldConfig(
737 'run-post-upload-hook', error_ok=True)
738 return run_post_upload_hook == "True"
739
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000740 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000741 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000742
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000743 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000744 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000745
ukai@chromium.orge8077812012-02-03 03:41:46 +0000746 def GetIsGerrit(self):
747 """Return true if this repo is assosiated with gerrit code review system."""
748 if self.is_gerrit is None:
749 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
750 return self.is_gerrit
751
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000752 def GetSquashGerritUploads(self):
753 """Return true if uploads to Gerrit should be squashed by default."""
754 if self.squash_gerrit_uploads is None:
755 self.squash_gerrit_uploads = (
756 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
757 error_ok=True).strip() == 'true')
758 return self.squash_gerrit_uploads
759
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000760 def GetGitEditor(self):
761 """Return the editor specified in the git config, or None if none is."""
762 if self.git_editor is None:
763 self.git_editor = self._GetConfig('core.editor', error_ok=True)
764 return self.git_editor or None
765
thestig@chromium.org44202a22014-03-11 19:22:18 +0000766 def GetLintRegex(self):
767 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
768 DEFAULT_LINT_REGEX)
769
770 def GetLintIgnoreRegex(self):
771 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
772 DEFAULT_LINT_IGNORE_REGEX)
773
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000774 def GetProject(self):
775 if not self.project:
776 self.project = self._GetRietveldConfig('project', error_ok=True)
777 return self.project
778
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000779 def GetForceHttpsCommitUrl(self):
780 if not self.force_https_commit_url:
781 self.force_https_commit_url = self._GetRietveldConfig(
782 'force-https-commit-url', error_ok=True)
783 return self.force_https_commit_url
784
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000785 def GetPendingRefPrefix(self):
786 if not self.pending_ref_prefix:
787 self.pending_ref_prefix = self._GetRietveldConfig(
788 'pending-ref-prefix', error_ok=True)
789 return self.pending_ref_prefix
790
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000791 def _GetRietveldConfig(self, param, **kwargs):
792 return self._GetConfig('rietveld.' + param, **kwargs)
793
rmistry@google.com78948ed2015-07-08 23:09:57 +0000794 def _GetBranchConfig(self, branch_name, param, **kwargs):
795 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
796
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797 def _GetConfig(self, param, **kwargs):
798 self.LazyUpdateIfNeeded()
799 return RunGit(['config', param], **kwargs).strip()
800
801
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000802def ShortBranchName(branch):
803 """Convert a name like 'refs/heads/foo' to just 'foo'."""
804 return branch.replace('refs/heads/', '')
805
806
807class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000808 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000809 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000810 global settings
811 if not settings:
812 # Happens when git_cl.py is used as a utility library.
813 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814 settings.GetDefaultServerUrl()
815 self.branchref = branchref
816 if self.branchref:
817 self.branch = ShortBranchName(self.branchref)
818 else:
819 self.branch = None
820 self.rietveld_server = None
821 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000822 self.lookedup_issue = False
823 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824 self.has_description = False
825 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000826 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000827 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000828 self.cc = None
829 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000830 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000831 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000832 self._remote = None
833 self._rpc_server = None
834
835 @property
836 def auth_config(self):
837 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000838
839 def GetCCList(self):
840 """Return the users cc'd on this CL.
841
842 Return is a string suitable for passing to gcl with the --cc flag.
843 """
844 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000845 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000846 more_cc = ','.join(self.watchers)
847 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
848 return self.cc
849
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000850 def GetCCListWithoutDefault(self):
851 """Return the users cc'd on this CL excluding default ones."""
852 if self.cc is None:
853 self.cc = ','.join(self.watchers)
854 return self.cc
855
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000856 def SetWatchers(self, watchers):
857 """Set the list of email addresses that should be cc'd based on the changed
858 files in this CL.
859 """
860 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000861
862 def GetBranch(self):
863 """Returns the short branch name, e.g. 'master'."""
864 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000865 branchref = RunGit(['symbolic-ref', 'HEAD'],
866 stderr=subprocess2.VOID, error_ok=True).strip()
867 if not branchref:
868 return None
869 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000870 self.branch = ShortBranchName(self.branchref)
871 return self.branch
872
873 def GetBranchRef(self):
874 """Returns the full branch name, e.g. 'refs/heads/master'."""
875 self.GetBranch() # Poke the lazy loader.
876 return self.branchref
877
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000878 @staticmethod
879 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000880 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000881 e.g. 'origin', 'refs/heads/master'
882 """
883 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000884 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
885 error_ok=True).strip()
886 if upstream_branch:
887 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
888 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000889 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
890 error_ok=True).strip()
891 if upstream_branch:
892 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000893 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000894 # Fall back on trying a git-svn upstream branch.
895 if settings.GetIsGitSvn():
896 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000897 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000898 # Else, try to guess the origin remote.
899 remote_branches = RunGit(['branch', '-r']).split()
900 if 'origin/master' in remote_branches:
901 # Fall back on origin/master if it exits.
902 remote = 'origin'
903 upstream_branch = 'refs/heads/master'
904 elif 'origin/trunk' in remote_branches:
905 # Fall back on origin/trunk if it exists. Generally a shared
906 # git-svn clone
907 remote = 'origin'
908 upstream_branch = 'refs/heads/trunk'
909 else:
910 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000911Either pass complete "git diff"-style arguments, like
912 git cl upload origin/master
913or verify this branch is set up to track another (via the --track argument to
914"git checkout -b ...").""")
915
916 return remote, upstream_branch
917
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000918 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000919 upstream_branch = self.GetUpstreamBranch()
920 if not BranchExists(upstream_branch):
921 DieWithError('The upstream for the current branch (%s) does not exist '
922 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000923 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000924 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000925
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926 def GetUpstreamBranch(self):
927 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000928 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000929 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000930 upstream_branch = upstream_branch.replace('refs/heads/',
931 'refs/remotes/%s/' % remote)
932 upstream_branch = upstream_branch.replace('refs/branch-heads/',
933 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000934 self.upstream_branch = upstream_branch
935 return self.upstream_branch
936
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000937 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000938 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000939 remote, branch = None, self.GetBranch()
940 seen_branches = set()
941 while branch not in seen_branches:
942 seen_branches.add(branch)
943 remote, branch = self.FetchUpstreamTuple(branch)
944 branch = ShortBranchName(branch)
945 if remote != '.' or branch.startswith('refs/remotes'):
946 break
947 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000948 remotes = RunGit(['remote'], error_ok=True).split()
949 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000950 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000951 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000952 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000953 logging.warning('Could not determine which remote this change is '
954 'associated with, so defaulting to "%s". This may '
955 'not be what you want. You may prevent this message '
956 'by running "git svn info" as documented here: %s',
957 self._remote,
958 GIT_INSTRUCTIONS_URL)
959 else:
960 logging.warn('Could not determine which remote this change is '
961 'associated with. You may prevent this message by '
962 'running "git svn info" as documented here: %s',
963 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000964 branch = 'HEAD'
965 if branch.startswith('refs/remotes'):
966 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000967 elif branch.startswith('refs/branch-heads/'):
968 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000969 else:
970 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000971 return self._remote
972
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000973 def GitSanityChecks(self, upstream_git_obj):
974 """Checks git repo status and ensures diff is from local commits."""
975
sbc@chromium.org79706062015-01-14 21:18:12 +0000976 if upstream_git_obj is None:
977 if self.GetBranch() is None:
978 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +0000979 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +0000980 else:
981 print >> sys.stderr, (
982 'ERROR: no upstream branch')
983 return False
984
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000985 # Verify the commit we're diffing against is in our current branch.
986 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
987 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
988 if upstream_sha != common_ancestor:
989 print >> sys.stderr, (
990 'ERROR: %s is not in the current branch. You may need to rebase '
991 'your tracking branch' % upstream_sha)
992 return False
993
994 # List the commits inside the diff, and verify they are all local.
995 commits_in_diff = RunGit(
996 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
997 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
998 remote_branch = remote_branch.strip()
999 if code != 0:
1000 _, remote_branch = self.GetRemoteBranch()
1001
1002 commits_in_remote = RunGit(
1003 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1004
1005 common_commits = set(commits_in_diff) & set(commits_in_remote)
1006 if common_commits:
1007 print >> sys.stderr, (
1008 'ERROR: Your diff contains %d commits already in %s.\n'
1009 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1010 'the diff. If you are using a custom git flow, you can override'
1011 ' the reference used for this check with "git config '
1012 'gitcl.remotebranch <git-ref>".' % (
1013 len(common_commits), remote_branch, upstream_git_obj))
1014 return False
1015 return True
1016
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001017 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001018 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001019
1020 Returns None if it is not set.
1021 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001022 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1023 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001024
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001025 def GetGitSvnRemoteUrl(self):
1026 """Return the configured git-svn remote URL parsed from git svn info.
1027
1028 Returns None if it is not set.
1029 """
1030 # URL is dependent on the current directory.
1031 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1032 if data:
1033 keys = dict(line.split(': ', 1) for line in data.splitlines()
1034 if ': ' in line)
1035 return keys.get('URL', None)
1036 return None
1037
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001038 def GetRemoteUrl(self):
1039 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1040
1041 Returns None if there is no remote.
1042 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001043 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001044 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1045
1046 # If URL is pointing to a local directory, it is probably a git cache.
1047 if os.path.isdir(url):
1048 url = RunGit(['config', 'remote.%s.url' % remote],
1049 error_ok=True,
1050 cwd=url).strip()
1051 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001052
1053 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001054 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001055 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001056 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001057 self.issue = int(issue) or None if issue else None
1058 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001059 return self.issue
1060
1061 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +00001062 if not self.rietveld_server:
1063 # If we're on a branch then get the server potentially associated
1064 # with that branch.
1065 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001066 rietveld_server_config = self._RietveldServer()
1067 if rietveld_server_config:
1068 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1069 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +00001070 if not self.rietveld_server:
1071 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072 return self.rietveld_server
1073
1074 def GetIssueURL(self):
1075 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001076 if not self.GetIssue():
1077 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001078 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
1079
1080 def GetDescription(self, pretty=False):
1081 if not self.has_description:
1082 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +00001083 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +00001084 try:
1085 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +00001086 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +00001087 if e.code == 404:
1088 DieWithError(
1089 ('\nWhile fetching the description for issue %d, received a '
1090 '404 (not found)\n'
1091 'error. It is likely that you deleted this '
1092 'issue on the server. If this is the\n'
1093 'case, please run\n\n'
1094 ' git cl issue 0\n\n'
1095 'to clear the association with the deleted issue. Then run '
1096 'this command again.') % issue)
1097 else:
1098 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +00001099 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +00001100 except urllib2.URLError as e:
1101 print >> sys.stderr, (
1102 'Warning: Failed to retrieve CL description due to network '
1103 'failure.')
1104 self.description = ''
1105
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106 self.has_description = True
1107 if pretty:
1108 wrapper = textwrap.TextWrapper()
1109 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1110 return wrapper.fill(self.description)
1111 return self.description
1112
1113 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001114 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001115 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001116 patchset = RunGit(['config', self._PatchsetSetting()],
1117 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001118 self.patchset = int(patchset) or None if patchset else None
1119 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120 return self.patchset
1121
1122 def SetPatchset(self, patchset):
1123 """Set this branch's patchset. If patchset=0, clears the patchset."""
1124 if patchset:
1125 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001126 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001127 else:
1128 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001129 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001130 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001131
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001132 def GetMostRecentPatchset(self):
1133 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +00001134
1135 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001136 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001137 '/download/issue%s_%s.diff' % (issue, patchset))
1138
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001139 def GetIssueProperties(self):
1140 if self._props is None:
1141 issue = self.GetIssue()
1142 if not issue:
1143 self._props = {}
1144 else:
1145 self._props = self.RpcServer().get_issue_properties(issue, True)
1146 return self._props
1147
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001148 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001149 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001150
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001151 def AddComment(self, message):
1152 return self.RpcServer().add_comment(self.GetIssue(), message)
1153
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 def SetIssue(self, issue):
1155 """Set this branch's issue. If issue=0, clears the issue."""
1156 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001157 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001158 RunGit(['config', self._IssueSetting(), str(issue)])
1159 if self.rietveld_server:
1160 RunGit(['config', self._RietveldServer(), self.rietveld_server])
1161 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001162 current_issue = self.GetIssue()
1163 if current_issue:
1164 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001165 self.issue = None
1166 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001167
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001168 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001169 if not self.GitSanityChecks(upstream_branch):
1170 DieWithError('\nGit sanity check failure')
1171
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001172 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001173 if not root:
1174 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001175 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001176
1177 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001178 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001179 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001180 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001181 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001182 except subprocess2.CalledProcessError:
1183 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001184 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001185 'This branch probably doesn\'t exist anymore. To reset the\n'
1186 'tracking branch, please run\n'
1187 ' git branch --set-upstream %s trunk\n'
1188 'replacing trunk with origin/master or the relevant branch') %
1189 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001190
maruel@chromium.org52424302012-08-29 15:14:30 +00001191 issue = self.GetIssue()
1192 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001193 if issue:
1194 description = self.GetDescription()
1195 else:
1196 # If the change was never uploaded, use the log messages of all commits
1197 # up to the branch point, as git cl upload will prefill the description
1198 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001199 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1200 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001201
1202 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001203 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001204 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001205 name,
1206 description,
1207 absroot,
1208 files,
1209 issue,
1210 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001211 author,
1212 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001213
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001214 def GetStatus(self):
1215 """Apply a rough heuristic to give a simple summary of an issue's review
1216 or CQ status, assuming adherence to a common workflow.
1217
1218 Returns None if no issue for this branch, or one of the following keywords:
1219 * 'error' - error from review tool (including deleted issues)
1220 * 'unsent' - not sent for review
1221 * 'waiting' - waiting for review
1222 * 'reply' - waiting for owner to reply to review
1223 * 'lgtm' - LGTM from at least one approved reviewer
1224 * 'commit' - in the commit queue
1225 * 'closed' - closed
1226 """
1227 if not self.GetIssue():
1228 return None
1229
1230 try:
1231 props = self.GetIssueProperties()
1232 except urllib2.HTTPError:
1233 return 'error'
1234
1235 if props.get('closed'):
1236 # Issue is closed.
1237 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001238 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001239 # Issue is in the commit queue.
1240 return 'commit'
1241
1242 try:
1243 reviewers = self.GetApprovingReviewers()
1244 except urllib2.HTTPError:
1245 return 'error'
1246
1247 if reviewers:
1248 # Was LGTM'ed.
1249 return 'lgtm'
1250
1251 messages = props.get('messages') or []
1252
1253 if not messages:
1254 # No message was sent.
1255 return 'unsent'
1256 if messages[-1]['sender'] != props.get('owner_email'):
1257 # Non-LGTM reply from non-owner
1258 return 'reply'
1259 return 'waiting'
1260
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001261 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001262 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001263
1264 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001265 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001266 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001267 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001268 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001269 except presubmit_support.PresubmitFailure, e:
1270 DieWithError(
1271 ('%s\nMaybe your depot_tools is out of date?\n'
1272 'If all fails, contact maruel@') % e)
1273
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001274 def UpdateDescription(self, description):
1275 self.description = description
1276 return self.RpcServer().update_description(
1277 self.GetIssue(), self.description)
1278
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001280 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001281 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001282
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001283 def SetFlag(self, flag, value):
1284 """Patchset must match."""
1285 if not self.GetPatchset():
1286 DieWithError('The patchset needs to match. Send another patchset.')
1287 try:
1288 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001289 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001290 except urllib2.HTTPError, e:
1291 if e.code == 404:
1292 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1293 if e.code == 403:
1294 DieWithError(
1295 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1296 'match?') % (self.GetIssue(), self.GetPatchset()))
1297 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001299 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001300 """Returns an upload.RpcServer() to access this review's rietveld instance.
1301 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001302 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001303 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001304 self.GetRietveldServer(),
1305 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001306 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001307
1308 def _IssueSetting(self):
1309 """Return the git setting that stores this change's issue."""
1310 return 'branch.%s.rietveldissue' % self.GetBranch()
1311
1312 def _PatchsetSetting(self):
1313 """Return the git setting that stores this change's most recent patchset."""
1314 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1315
1316 def _RietveldServer(self):
1317 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001318 branch = self.GetBranch()
1319 if branch:
1320 return 'branch.%s.rietveldserver' % branch
1321 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001322
1323
1324def GetCodereviewSettingsInteractively():
1325 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001326 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001327 server = settings.GetDefaultServerUrl(error_ok=True)
1328 prompt = 'Rietveld server (host[:port])'
1329 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001330 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001331 if not server and not newserver:
1332 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001333 if newserver:
1334 newserver = gclient_utils.UpgradeToHttps(newserver)
1335 if newserver != server:
1336 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001338 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339 prompt = caption
1340 if initial:
1341 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001342 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001343 if new_val == 'x':
1344 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001345 elif new_val:
1346 if is_url:
1347 new_val = gclient_utils.UpgradeToHttps(new_val)
1348 if new_val != initial:
1349 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001350
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001351 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001352 SetProperty(settings.GetDefaultPrivateFlag(),
1353 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001354 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001355 'tree-status-url', False)
1356 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001357 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001358 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1359 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360
1361 # TODO: configure a default branch to diff against, rather than this
1362 # svn-based hackery.
1363
1364
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001365class ChangeDescription(object):
1366 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001367 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001368 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001369
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001370 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001371 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001372
agable@chromium.org42c20792013-09-12 17:34:49 +00001373 @property # www.logilab.org/ticket/89786
1374 def description(self): # pylint: disable=E0202
1375 return '\n'.join(self._description_lines)
1376
1377 def set_description(self, desc):
1378 if isinstance(desc, basestring):
1379 lines = desc.splitlines()
1380 else:
1381 lines = [line.rstrip() for line in desc]
1382 while lines and not lines[0]:
1383 lines.pop(0)
1384 while lines and not lines[-1]:
1385 lines.pop(-1)
1386 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001387
piman@chromium.org336f9122014-09-04 02:16:55 +00001388 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001389 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001390 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001391 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001392 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001393 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001394
agable@chromium.org42c20792013-09-12 17:34:49 +00001395 # Get the set of R= and TBR= lines and remove them from the desciption.
1396 regexp = re.compile(self.R_LINE)
1397 matches = [regexp.match(line) for line in self._description_lines]
1398 new_desc = [l for i, l in enumerate(self._description_lines)
1399 if not matches[i]]
1400 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001401
agable@chromium.org42c20792013-09-12 17:34:49 +00001402 # Construct new unified R= and TBR= lines.
1403 r_names = []
1404 tbr_names = []
1405 for match in matches:
1406 if not match:
1407 continue
1408 people = cleanup_list([match.group(2).strip()])
1409 if match.group(1) == 'TBR':
1410 tbr_names.extend(people)
1411 else:
1412 r_names.extend(people)
1413 for name in r_names:
1414 if name not in reviewers:
1415 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001416 if add_owners_tbr:
1417 owners_db = owners.Database(change.RepositoryRoot(),
1418 fopen=file, os_path=os.path, glob=glob.glob)
1419 all_reviewers = set(tbr_names + reviewers)
1420 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1421 all_reviewers)
1422 tbr_names.extend(owners_db.reviewers_for(missing_files,
1423 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001424 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1425 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1426
1427 # Put the new lines in the description where the old first R= line was.
1428 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1429 if 0 <= line_loc < len(self._description_lines):
1430 if new_tbr_line:
1431 self._description_lines.insert(line_loc, new_tbr_line)
1432 if new_r_line:
1433 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001434 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001435 if new_r_line:
1436 self.append_footer(new_r_line)
1437 if new_tbr_line:
1438 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001439
1440 def prompt(self):
1441 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001442 self.set_description([
1443 '# Enter a description of the change.',
1444 '# This will be displayed on the codereview site.',
1445 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001446 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001447 '--------------------',
1448 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001449
agable@chromium.org42c20792013-09-12 17:34:49 +00001450 regexp = re.compile(self.BUG_LINE)
1451 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001452 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001453 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001454 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001455 if not content:
1456 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001457 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001458
1459 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001460 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1461 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001462 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001463 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001464
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001465 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001466 if self._description_lines:
1467 # Add an empty line if either the last line or the new line isn't a tag.
1468 last_line = self._description_lines[-1]
1469 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1470 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1471 self._description_lines.append('')
1472 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001473
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001474 def get_reviewers(self):
1475 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001476 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1477 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001478 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001479
1480
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001481def get_approving_reviewers(props):
1482 """Retrieves the reviewers that approved a CL from the issue properties with
1483 messages.
1484
1485 Note that the list may contain reviewers that are not committer, thus are not
1486 considered by the CQ.
1487 """
1488 return sorted(
1489 set(
1490 message['sender']
1491 for message in props['messages']
1492 if message['approval'] and message['sender'] in props['reviewers']
1493 )
1494 )
1495
1496
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001497def FindCodereviewSettingsFile(filename='codereview.settings'):
1498 """Finds the given file starting in the cwd and going up.
1499
1500 Only looks up to the top of the repository unless an
1501 'inherit-review-settings-ok' file exists in the root of the repository.
1502 """
1503 inherit_ok_file = 'inherit-review-settings-ok'
1504 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001505 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001506 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1507 root = '/'
1508 while True:
1509 if filename in os.listdir(cwd):
1510 if os.path.isfile(os.path.join(cwd, filename)):
1511 return open(os.path.join(cwd, filename))
1512 if cwd == root:
1513 break
1514 cwd = os.path.dirname(cwd)
1515
1516
1517def LoadCodereviewSettingsFromFile(fileobj):
1518 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001519 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001520
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001521 def SetProperty(name, setting, unset_error_ok=False):
1522 fullname = 'rietveld.' + name
1523 if setting in keyvals:
1524 RunGit(['config', fullname, keyvals[setting]])
1525 else:
1526 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1527
1528 SetProperty('server', 'CODE_REVIEW_SERVER')
1529 # Only server setting is required. Other settings can be absent.
1530 # In that case, we ignore errors raised during option deletion attempt.
1531 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001532 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001533 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1534 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001535 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001536 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001537 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1538 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001539 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001540 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001541 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001542 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1543 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001544
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001545 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001546 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001547
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00001548 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
1549 RunGit(['config', 'gerrit.squash-uploads',
1550 keyvals['GERRIT_SQUASH_UPLOADS']])
1551
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001552 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1553 #should be of the form
1554 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1555 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1556 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1557 keyvals['ORIGIN_URL_CONFIG']])
1558
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001559
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001560def urlretrieve(source, destination):
1561 """urllib is broken for SSL connections via a proxy therefore we
1562 can't use urllib.urlretrieve()."""
1563 with open(destination, 'w') as f:
1564 f.write(urllib2.urlopen(source).read())
1565
1566
ukai@chromium.org712d6102013-11-27 00:52:58 +00001567def hasSheBang(fname):
1568 """Checks fname is a #! script."""
1569 with open(fname) as f:
1570 return f.read(2).startswith('#!')
1571
1572
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001573def DownloadHooks(force):
1574 """downloads hooks
1575
1576 Args:
1577 force: True to update hooks. False to install hooks if not present.
1578 """
1579 if not settings.GetIsGerrit():
1580 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001581 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001582 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1583 if not os.access(dst, os.X_OK):
1584 if os.path.exists(dst):
1585 if not force:
1586 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001587 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001588 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001589 if not hasSheBang(dst):
1590 DieWithError('Not a script: %s\n'
1591 'You need to download from\n%s\n'
1592 'into .git/hooks/commit-msg and '
1593 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001594 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1595 except Exception:
1596 if os.path.exists(dst):
1597 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001598 DieWithError('\nFailed to download hooks.\n'
1599 'You need to download from\n%s\n'
1600 'into .git/hooks/commit-msg and '
1601 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001602
1603
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001604@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001605def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001606 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001607
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001608 parser.add_option('--activate-update', action='store_true',
1609 help='activate auto-updating [rietveld] section in '
1610 '.git/config')
1611 parser.add_option('--deactivate-update', action='store_true',
1612 help='deactivate auto-updating [rietveld] section in '
1613 '.git/config')
1614 options, args = parser.parse_args(args)
1615
1616 if options.deactivate_update:
1617 RunGit(['config', 'rietveld.autoupdate', 'false'])
1618 return
1619
1620 if options.activate_update:
1621 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1622 return
1623
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001624 if len(args) == 0:
1625 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001626 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001627 return 0
1628
1629 url = args[0]
1630 if not url.endswith('codereview.settings'):
1631 url = os.path.join(url, 'codereview.settings')
1632
1633 # Load code review settings and download hooks (if available).
1634 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001635 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001636 return 0
1637
1638
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001639def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001640 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001641 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1642 branch = ShortBranchName(branchref)
1643 _, args = parser.parse_args(args)
1644 if not args:
1645 print("Current base-url:")
1646 return RunGit(['config', 'branch.%s.base-url' % branch],
1647 error_ok=False).strip()
1648 else:
1649 print("Setting base-url to %s" % args[0])
1650 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1651 error_ok=False).strip()
1652
1653
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001654def color_for_status(status):
1655 """Maps a Changelist status to color, for CMDstatus and other tools."""
1656 return {
1657 'unsent': Fore.RED,
1658 'waiting': Fore.BLUE,
1659 'reply': Fore.YELLOW,
1660 'lgtm': Fore.GREEN,
1661 'commit': Fore.MAGENTA,
1662 'closed': Fore.CYAN,
1663 'error': Fore.WHITE,
1664 }.get(status, Fore.WHITE)
1665
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001666def fetch_cl_status(branch, auth_config=None):
1667 """Fetches information for an issue and returns (branch, issue, status)."""
1668 cl = Changelist(branchref=branch, auth_config=auth_config)
1669 url = cl.GetIssueURL()
1670 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001671
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001672 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001673 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001674 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001675
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001676 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001677
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001678def get_cl_statuses(
1679 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001680 """Returns a blocking iterable of (branch, issue, color) for given branches.
1681
1682 If fine_grained is true, this will fetch CL statuses from the server.
1683 Otherwise, simply indicate if there's a matching url for the given branches.
1684
1685 If max_processes is specified, it is used as the maximum number of processes
1686 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1687 spawned.
1688 """
1689 # Silence upload.py otherwise it becomes unwieldly.
1690 upload.verbosity = 0
1691
1692 if fine_grained:
1693 # Process one branch synchronously to work through authentication, then
1694 # spawn processes to process all the other branches in parallel.
1695 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001696 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1697 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001698
1699 branches_to_fetch = branches[1:]
1700 pool = ThreadPool(
1701 min(max_processes, len(branches_to_fetch))
1702 if max_processes is not None
1703 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001704 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001705 yield x
1706 else:
1707 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1708 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001709 cl = Changelist(branchref=b, auth_config=auth_config)
1710 url = cl.GetIssueURL()
1711 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001712
rmistry@google.com2dd99862015-06-22 12:22:18 +00001713
1714def upload_branch_deps(cl, args):
1715 """Uploads CLs of local branches that are dependents of the current branch.
1716
1717 If the local branch dependency tree looks like:
1718 test1 -> test2.1 -> test3.1
1719 -> test3.2
1720 -> test2.2 -> test3.3
1721
1722 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1723 run on the dependent branches in this order:
1724 test2.1, test3.1, test3.2, test2.2, test3.3
1725
1726 Note: This function does not rebase your local dependent branches. Use it when
1727 you make a change to the parent branch that will not conflict with its
1728 dependent branches, and you would like their dependencies updated in
1729 Rietveld.
1730 """
1731 if git_common.is_dirty_git_tree('upload-branch-deps'):
1732 return 1
1733
1734 root_branch = cl.GetBranch()
1735 if root_branch is None:
1736 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1737 'Get on a branch!')
1738 if not cl.GetIssue() or not cl.GetPatchset():
1739 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1740 'patchset dependencies without an uploaded CL.')
1741
1742 branches = RunGit(['for-each-ref',
1743 '--format=%(refname:short) %(upstream:short)',
1744 'refs/heads'])
1745 if not branches:
1746 print('No local branches found.')
1747 return 0
1748
1749 # Create a dictionary of all local branches to the branches that are dependent
1750 # on it.
1751 tracked_to_dependents = collections.defaultdict(list)
1752 for b in branches.splitlines():
1753 tokens = b.split()
1754 if len(tokens) == 2:
1755 branch_name, tracked = tokens
1756 tracked_to_dependents[tracked].append(branch_name)
1757
1758 print
1759 print 'The dependent local branches of %s are:' % root_branch
1760 dependents = []
1761 def traverse_dependents_preorder(branch, padding=''):
1762 dependents_to_process = tracked_to_dependents.get(branch, [])
1763 padding += ' '
1764 for dependent in dependents_to_process:
1765 print '%s%s' % (padding, dependent)
1766 dependents.append(dependent)
1767 traverse_dependents_preorder(dependent, padding)
1768 traverse_dependents_preorder(root_branch)
1769 print
1770
1771 if not dependents:
1772 print 'There are no dependent local branches for %s' % root_branch
1773 return 0
1774
1775 print ('This command will checkout all dependent branches and run '
1776 '"git cl upload".')
1777 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1778
andybons@chromium.org962f9462016-02-03 20:00:42 +00001779 # Add a default patchset title to all upload calls in Rietveld.
1780 if not settings.GetIsGerrit():
1781 args.extend(['-t', 'Updated patchset dependency'])
1782
rmistry@google.com2dd99862015-06-22 12:22:18 +00001783 # Record all dependents that failed to upload.
1784 failures = {}
1785 # Go through all dependents, checkout the branch and upload.
1786 try:
1787 for dependent_branch in dependents:
1788 print
1789 print '--------------------------------------'
1790 print 'Running "git cl upload" from %s:' % dependent_branch
1791 RunGit(['checkout', '-q', dependent_branch])
1792 print
1793 try:
1794 if CMDupload(OptionParser(), args) != 0:
1795 print 'Upload failed for %s!' % dependent_branch
1796 failures[dependent_branch] = 1
1797 except: # pylint: disable=W0702
1798 failures[dependent_branch] = 1
1799 print
1800 finally:
1801 # Swap back to the original root branch.
1802 RunGit(['checkout', '-q', root_branch])
1803
1804 print
1805 print 'Upload complete for dependent branches!'
1806 for dependent_branch in dependents:
1807 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1808 print ' %s : %s' % (dependent_branch, upload_status)
1809 print
1810
1811 return 0
1812
1813
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001814def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001815 """Show status of changelists.
1816
1817 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001818 - Red not sent for review or broken
1819 - Blue waiting for review
1820 - Yellow waiting for you to reply to review
1821 - Green LGTM'ed
1822 - Magenta in the commit queue
1823 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001824
1825 Also see 'git cl comments'.
1826 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001827 parser.add_option('--field',
1828 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001829 parser.add_option('-f', '--fast', action='store_true',
1830 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001831 parser.add_option(
1832 '-j', '--maxjobs', action='store', type=int,
1833 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001834
1835 auth.add_auth_options(parser)
1836 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001837 if args:
1838 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001839 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001840
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001841 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001842 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001843 if options.field.startswith('desc'):
1844 print cl.GetDescription()
1845 elif options.field == 'id':
1846 issueid = cl.GetIssue()
1847 if issueid:
1848 print issueid
1849 elif options.field == 'patch':
1850 patchset = cl.GetPatchset()
1851 if patchset:
1852 print patchset
1853 elif options.field == 'url':
1854 url = cl.GetIssueURL()
1855 if url:
1856 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001857 return 0
1858
1859 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1860 if not branches:
1861 print('No local branch found.')
1862 return 0
1863
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001864 changes = (
1865 Changelist(branchref=b, auth_config=auth_config)
1866 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001867 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001868 alignment = max(5, max(len(b) for b in branches))
1869 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001870 output = get_cl_statuses(branches,
1871 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001872 max_processes=options.maxjobs,
1873 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001874
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001875 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001876 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001877 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001878 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001879 b, i, status = output.next()
1880 branch_statuses[b] = (i, status)
1881 issue_url, status = branch_statuses.pop(branch)
1882 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001883 reset = Fore.RESET
1884 if not sys.stdout.isatty():
1885 color = ''
1886 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001887 status_str = '(%s)' % status if status else ''
1888 print ' %*s : %s%s %s%s' % (
1889 alignment, ShortBranchName(branch), color, issue_url, status_str,
1890 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001891
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001892 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001893 print
1894 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001895 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001896 if not cl.GetIssue():
1897 print 'No issue assigned.'
1898 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001899 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001900 if not options.fast:
1901 print 'Issue description:'
1902 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001903 return 0
1904
1905
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001906def colorize_CMDstatus_doc():
1907 """To be called once in main() to add colors to git cl status help."""
1908 colors = [i for i in dir(Fore) if i[0].isupper()]
1909
1910 def colorize_line(line):
1911 for color in colors:
1912 if color in line.upper():
1913 # Extract whitespaces first and the leading '-'.
1914 indent = len(line) - len(line.lstrip(' ')) + 1
1915 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1916 return line
1917
1918 lines = CMDstatus.__doc__.splitlines()
1919 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1920
1921
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001922@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001923def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001924 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001925
1926 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001927 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001928 parser.add_option('-r', '--reverse', action='store_true',
1929 help='Lookup the branch(es) for the specified issues. If '
1930 'no issues are specified, all branches with mapped '
1931 'issues will be listed.')
1932 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001933
dnj@chromium.org406c4402015-03-03 17:22:28 +00001934 if options.reverse:
1935 branches = RunGit(['for-each-ref', 'refs/heads',
1936 '--format=%(refname:short)']).splitlines()
1937
1938 # Reverse issue lookup.
1939 issue_branch_map = {}
1940 for branch in branches:
1941 cl = Changelist(branchref=branch)
1942 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1943 if not args:
1944 args = sorted(issue_branch_map.iterkeys())
1945 for issue in args:
1946 if not issue:
1947 continue
1948 print 'Branch for issue number %s: %s' % (
1949 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1950 else:
1951 cl = Changelist()
1952 if len(args) > 0:
1953 try:
1954 issue = int(args[0])
1955 except ValueError:
1956 DieWithError('Pass a number to set the issue or none to list it.\n'
1957 'Maybe you want to run git cl status?')
1958 cl.SetIssue(issue)
1959 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001960 return 0
1961
1962
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001963def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001964 """Shows or posts review comments for any changelist."""
1965 parser.add_option('-a', '--add-comment', dest='comment',
1966 help='comment to add to an issue')
1967 parser.add_option('-i', dest='issue',
1968 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00001969 parser.add_option('-j', '--json-file',
1970 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001971 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001972 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001973 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001974
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001975 issue = None
1976 if options.issue:
1977 try:
1978 issue = int(options.issue)
1979 except ValueError:
1980 DieWithError('A review issue id is expected to be a number')
1981
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001982 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001983
1984 if options.comment:
1985 cl.AddComment(options.comment)
1986 return 0
1987
1988 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00001989 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001990 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00001991 summary.append({
1992 'date': message['date'],
1993 'lgtm': False,
1994 'message': message['text'],
1995 'not_lgtm': False,
1996 'sender': message['sender'],
1997 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001998 if message['disapproval']:
1999 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00002000 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002001 elif message['approval']:
2002 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002003 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002004 elif message['sender'] == data['owner_email']:
2005 color = Fore.MAGENTA
2006 else:
2007 color = Fore.BLUE
2008 print '\n%s%s %s%s' % (
2009 color, message['date'].split('.', 1)[0], message['sender'],
2010 Fore.RESET)
2011 if message['text'].strip():
2012 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002013 if options.json_file:
2014 with open(options.json_file, 'wb') as f:
2015 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002016 return 0
2017
2018
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002019def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002020 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002021 parser.add_option('-d', '--display', action='store_true',
2022 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002023 auth.add_auth_options(parser)
2024 options, _ = parser.parse_args(args)
2025 auth_config = auth.extract_auth_config_from_options(options)
2026 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002027 if not cl.GetIssue():
2028 DieWithError('This branch has no associated changelist.')
2029 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002030 if options.display:
2031 print description.description
2032 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002033 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002034 if cl.GetDescription() != description.description:
2035 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002036 return 0
2037
2038
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002039def CreateDescriptionFromLog(args):
2040 """Pulls out the commit log to use as a base for the CL description."""
2041 log_args = []
2042 if len(args) == 1 and not args[0].endswith('.'):
2043 log_args = [args[0] + '..']
2044 elif len(args) == 1 and args[0].endswith('...'):
2045 log_args = [args[0][:-1]]
2046 elif len(args) == 2:
2047 log_args = [args[0] + '..' + args[1]]
2048 else:
2049 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002050 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002051
2052
thestig@chromium.org44202a22014-03-11 19:22:18 +00002053def CMDlint(parser, args):
2054 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002055 parser.add_option('--filter', action='append', metavar='-x,+y',
2056 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002057 auth.add_auth_options(parser)
2058 options, args = parser.parse_args(args)
2059 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002060
2061 # Access to a protected member _XX of a client class
2062 # pylint: disable=W0212
2063 try:
2064 import cpplint
2065 import cpplint_chromium
2066 except ImportError:
2067 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2068 return 1
2069
2070 # Change the current working directory before calling lint so that it
2071 # shows the correct base.
2072 previous_cwd = os.getcwd()
2073 os.chdir(settings.GetRoot())
2074 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002075 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002076 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2077 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002078 if not files:
2079 print "Cannot lint an empty CL"
2080 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002081
2082 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002083 command = args + files
2084 if options.filter:
2085 command = ['--filter=' + ','.join(options.filter)] + command
2086 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002087
2088 white_regex = re.compile(settings.GetLintRegex())
2089 black_regex = re.compile(settings.GetLintIgnoreRegex())
2090 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2091 for filename in filenames:
2092 if white_regex.match(filename):
2093 if black_regex.match(filename):
2094 print "Ignoring file %s" % filename
2095 else:
2096 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2097 extra_check_functions)
2098 else:
2099 print "Skipping file %s" % filename
2100 finally:
2101 os.chdir(previous_cwd)
2102 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2103 if cpplint._cpplint_state.error_count != 0:
2104 return 1
2105 return 0
2106
2107
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002108def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002109 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002110 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002111 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002112 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002113 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002114 auth.add_auth_options(parser)
2115 options, args = parser.parse_args(args)
2116 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002117
sbc@chromium.org71437c02015-04-09 19:29:40 +00002118 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002119 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002120 return 1
2121
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002122 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002123 if args:
2124 base_branch = args[0]
2125 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002126 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002127 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002128
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002129 cl.RunHook(
2130 committing=not options.upload,
2131 may_prompt=False,
2132 verbose=options.verbose,
2133 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002134 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002135
2136
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002137def AddChangeIdToCommitMessage(options, args):
2138 """Re-commits using the current message, assumes the commit hook is in
2139 place.
2140 """
2141 log_desc = options.message or CreateDescriptionFromLog(args)
2142 git_command = ['commit', '--amend', '-m', log_desc]
2143 RunGit(git_command)
2144 new_log_desc = CreateDescriptionFromLog(args)
2145 if CHANGE_ID in new_log_desc:
2146 print 'git-cl: Added Change-Id to commit message.'
2147 else:
2148 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2149
2150
piman@chromium.org336f9122014-09-04 02:16:55 +00002151def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002152 """upload the current branch to gerrit."""
2153 # We assume the remote called "origin" is the one we want.
2154 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002155 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00002156
2157 remote, remote_branch = cl.GetRemoteBranch()
2158 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2159 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002160
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002161 change_desc = ChangeDescription(
2162 options.message or CreateDescriptionFromLog(args))
2163 if not change_desc.description:
andybons@chromium.org962f9462016-02-03 20:00:42 +00002164 print "\nDescription is empty. Aborting..."
2165 return 1
2166
2167 if options.title:
2168 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002169 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002170
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002171 if options.squash:
2172 # Try to get the message from a previous upload.
2173 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
bauerb@chromium.org13502e02016-02-18 10:18:29 +00002174 message = RunGitSilent(['show', '--format=%B', '-s', shadow_branch])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002175 if not message:
2176 if not options.force:
2177 change_desc.prompt()
2178
2179 if CHANGE_ID not in change_desc.description:
2180 # Run the commit-msg hook without modifying the head commit by writing
2181 # the commit message to a temporary file and running the hook over it,
2182 # then reading the file back in.
2183 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
2184 'commit-msg')
2185 file_handle, msg_file = tempfile.mkstemp(text=True,
2186 prefix='commit_msg')
tandrii@chromium.orga83663a2016-01-14 16:01:00 +00002187 logging.debug("%s %s", file_handle, msg_file)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002188 try:
2189 try:
tandrii@chromium.orga83663a2016-01-14 16:01:00 +00002190 try:
2191 fileobj = os.fdopen(file_handle, 'w')
2192 except OSError:
2193 # if fdopen fails, file_handle remains open.
2194 # See https://docs.python.org/2/library/os.html#os.fdopen.
2195 os.close(file_handle)
2196 raise
2197 with fileobj:
2198 # This will close the file_handle.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002199 fileobj.write(change_desc.description)
tandrii@chromium.orga83663a2016-01-14 16:01:00 +00002200 logging.debug("%s %s finish editing", file_handle, msg_file)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002201 finally:
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002202 RunCommand([commit_msg_hook, msg_file])
2203 change_desc.set_description(gclient_utils.FileRead(msg_file))
2204 finally:
2205 os.remove(msg_file)
2206
2207 if not change_desc.description:
2208 print "Description is empty; aborting."
2209 return 1
2210
2211 message = change_desc.description
2212
2213 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2214 if remote is '.':
2215 # If our upstream branch is local, we base our squashed commit on its
2216 # squashed version.
2217 parent = ('refs/heads/git_cl_uploads/' +
2218 scm.GIT.ShortBranchName(upstream_branch))
2219
2220 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2221 # will create additional CLs when uploading.
2222 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2223 RunGitSilent(['rev-parse', parent + ':'])):
2224 print 'Upload upstream branch ' + upstream_branch + ' first.'
2225 return 1
2226 else:
2227 parent = cl.GetCommonAncestorWithUpstream()
2228
2229 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2230 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2231 '-m', message]).strip()
2232 else:
2233 if CHANGE_ID not in change_desc.description:
2234 AddChangeIdToCommitMessage(options, args)
2235 ref_to_push = 'HEAD'
2236 parent = '%s/%s' % (gerrit_remote, branch)
2237
2238 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2239 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002240 if len(commits) > 1:
2241 print('WARNING: This will upload %d commits. Run the following command '
2242 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002243 print('git log %s..%s' % (parent, ref_to_push))
2244 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002245 'commit.')
2246 ask_for_data('About to upload; enter to confirm.')
2247
piman@chromium.org336f9122014-09-04 02:16:55 +00002248 if options.reviewers or options.tbr_owners:
2249 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002250
ukai@chromium.orge8077812012-02-03 03:41:46 +00002251 receive_options = []
2252 cc = cl.GetCCList().split(',')
2253 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002254 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002255 cc = filter(None, cc)
2256 if cc:
2257 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002258 if change_desc.get_reviewers():
2259 receive_options.extend(
2260 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002261
ukai@chromium.orge8077812012-02-03 03:41:46 +00002262 git_command = ['push']
2263 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002264 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002265 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002266 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002267 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002268
2269 if options.squash:
2270 head = RunGit(['rev-parse', 'HEAD']).strip()
2271 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2272
ukai@chromium.orge8077812012-02-03 03:41:46 +00002273 # TODO(ukai): parse Change-Id: and set issue number?
2274 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002275
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002276
wittman@chromium.org455dc922015-01-26 20:15:50 +00002277def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2278 """Computes the remote branch ref to use for the CL.
2279
2280 Args:
2281 remote (str): The git remote for the CL.
2282 remote_branch (str): The git remote branch for the CL.
2283 target_branch (str): The target branch specified by the user.
2284 pending_prefix (str): The pending prefix from the settings.
2285 """
2286 if not (remote and remote_branch):
2287 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002288
wittman@chromium.org455dc922015-01-26 20:15:50 +00002289 if target_branch:
2290 # Cannonicalize branch references to the equivalent local full symbolic
2291 # refs, which are then translated into the remote full symbolic refs
2292 # below.
2293 if '/' not in target_branch:
2294 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2295 else:
2296 prefix_replacements = (
2297 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2298 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2299 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2300 )
2301 match = None
2302 for regex, replacement in prefix_replacements:
2303 match = re.search(regex, target_branch)
2304 if match:
2305 remote_branch = target_branch.replace(match.group(0), replacement)
2306 break
2307 if not match:
2308 # This is a branch path but not one we recognize; use as-is.
2309 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002310 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2311 # Handle the refs that need to land in different refs.
2312 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002313
wittman@chromium.org455dc922015-01-26 20:15:50 +00002314 # Create the true path to the remote branch.
2315 # Does the following translation:
2316 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2317 # * refs/remotes/origin/master -> refs/heads/master
2318 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2319 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2320 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2321 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2322 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2323 'refs/heads/')
2324 elif remote_branch.startswith('refs/remotes/branch-heads'):
2325 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2326 # If a pending prefix exists then replace refs/ with it.
2327 if pending_prefix:
2328 remote_branch = remote_branch.replace('refs/', pending_prefix)
2329 return remote_branch
2330
2331
piman@chromium.org336f9122014-09-04 02:16:55 +00002332def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002333 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002334 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2335 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002336 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002337 if options.emulate_svn_auto_props:
2338 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002339
2340 change_desc = None
2341
pgervais@chromium.org91141372014-01-09 23:27:20 +00002342 if options.email is not None:
2343 upload_args.extend(['--email', options.email])
2344
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002345 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002346 if options.title:
2347 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002348 if options.message:
2349 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002350 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002351 print ("This branch is associated with issue %s. "
2352 "Adding patch to that issue." % cl.GetIssue())
2353 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002354 if options.title:
2355 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002356 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002357 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002358 if options.reviewers or options.tbr_owners:
2359 change_desc.update_reviewers(options.reviewers,
2360 options.tbr_owners,
2361 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002362 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002363 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002364
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002365 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002366 print "Description is empty; aborting."
2367 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002368
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002369 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002370 if change_desc.get_reviewers():
2371 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002372 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002373 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002374 DieWithError("Must specify reviewers to send email.")
2375 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002376
2377 # We check this before applying rietveld.private assuming that in
2378 # rietveld.cc only addresses which we can send private CLs to are listed
2379 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2380 # --private is specified explicitly on the command line.
2381 if options.private:
2382 logging.warn('rietveld.cc is ignored since private flag is specified. '
2383 'You need to review and add them manually if necessary.')
2384 cc = cl.GetCCListWithoutDefault()
2385 else:
2386 cc = cl.GetCCList()
2387 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002388 if cc:
2389 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002390
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002391 if options.private or settings.GetDefaultPrivateFlag() == "True":
2392 upload_args.append('--private')
2393
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002394 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002395 if not options.find_copies:
2396 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002397
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002398 # Include the upstream repo's URL in the change -- this is useful for
2399 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002400 remote_url = cl.GetGitBaseUrlFromConfig()
2401 if not remote_url:
2402 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002403 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002404 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002405 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2406 remote_url = (cl.GetRemoteUrl() + '@'
2407 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002408 if remote_url:
2409 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002410 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002411 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2412 settings.GetPendingRefPrefix())
2413 if target_ref:
2414 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002415
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002416 # Look for dependent patchsets. See crbug.com/480453 for more details.
2417 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2418 upstream_branch = ShortBranchName(upstream_branch)
2419 if remote is '.':
2420 # A local branch is being tracked.
2421 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002422 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002423 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002424 print ('Skipping dependency patchset upload because git config '
2425 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002426 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002427 else:
2428 auth_config = auth.extract_auth_config_from_options(options)
2429 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2430 branch_cl_issue_url = branch_cl.GetIssueURL()
2431 branch_cl_issue = branch_cl.GetIssue()
2432 branch_cl_patchset = branch_cl.GetPatchset()
2433 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2434 upload_args.extend(
2435 ['--depends_on_patchset', '%s:%s' % (
2436 branch_cl_issue, branch_cl_patchset)])
2437 print
2438 print ('The current branch (%s) is tracking a local branch (%s) with '
2439 'an associated CL.') % (cl.GetBranch(), local_branch)
2440 print 'Adding %s/#ps%s as a dependency patchset.' % (
2441 branch_cl_issue_url, branch_cl_patchset)
2442 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002443
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002444 project = settings.GetProject()
2445 if project:
2446 upload_args.extend(['--project', project])
2447
rmistry@google.comef966222015-04-07 11:15:01 +00002448 if options.cq_dry_run:
2449 upload_args.extend(['--cq_dry_run'])
2450
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002451 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002452 upload_args = ['upload'] + upload_args + args
2453 logging.info('upload.RealMain(%s)', upload_args)
2454 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002455 issue = int(issue)
2456 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002457 except KeyboardInterrupt:
2458 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002459 except:
2460 # If we got an exception after the user typed a description for their
2461 # change, back up the description before re-raising.
2462 if change_desc:
2463 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2464 print '\nGot exception while uploading -- saving description to %s\n' \
2465 % backup_path
2466 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002467 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002468 backup_file.close()
2469 raise
2470
2471 if not cl.GetIssue():
2472 cl.SetIssue(issue)
2473 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002474
2475 if options.use_commit_queue:
2476 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002477 return 0
2478
2479
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002480def cleanup_list(l):
2481 """Fixes a list so that comma separated items are put as individual items.
2482
2483 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2484 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2485 """
2486 items = sum((i.split(',') for i in l), [])
2487 stripped_items = (i.strip() for i in items)
2488 return sorted(filter(None, stripped_items))
2489
2490
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002491@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002492def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002493 """Uploads the current changelist to codereview.
2494
2495 Can skip dependency patchset uploads for a branch by running:
2496 git config branch.branch_name.skip-deps-uploads True
2497 To unset run:
2498 git config --unset branch.branch_name.skip-deps-uploads
2499 Can also set the above globally by using the --global flag.
2500 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002501 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2502 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002503 parser.add_option('--bypass-watchlists', action='store_true',
2504 dest='bypass_watchlists',
2505 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002506 parser.add_option('-f', action='store_true', dest='force',
2507 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002508 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00002509 parser.add_option('-t', dest='title',
2510 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002511 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002512 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002513 help='reviewer email addresses')
2514 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002515 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002516 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002517 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002518 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002519 parser.add_option('--emulate_svn_auto_props',
2520 '--emulate-svn-auto-props',
2521 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002522 dest="emulate_svn_auto_props",
2523 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002524 parser.add_option('-c', '--use-commit-queue', action='store_true',
2525 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002526 parser.add_option('--private', action='store_true',
2527 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002528 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002529 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002530 metavar='TARGET',
2531 help='Apply CL to remote ref TARGET. ' +
2532 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002533 parser.add_option('--squash', action='store_true',
2534 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002535 parser.add_option('--no-squash', action='store_true',
2536 help='Don\'t squash multiple commits into one ' +
2537 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002538 parser.add_option('--email', default=None,
2539 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002540 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2541 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002542 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2543 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002544 help='Send the patchset to do a CQ dry run right after '
2545 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002546 parser.add_option('--dependencies', action='store_true',
2547 help='Uploads CLs of all the local branches that depend on '
2548 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002549
rmistry@google.com2dd99862015-06-22 12:22:18 +00002550 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002551 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002552 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002553 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002554 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002555
sbc@chromium.org71437c02015-04-09 19:29:40 +00002556 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002557 return 1
2558
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002559 options.reviewers = cleanup_list(options.reviewers)
2560 options.cc = cleanup_list(options.cc)
2561
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002562 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002563 if args:
2564 # TODO(ukai): is it ok for gerrit case?
2565 base_branch = args[0]
2566 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002567 if cl.GetBranch() is None:
2568 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2569
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002570 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002571 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002572 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002573
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002574 # Make sure authenticated to Rietveld before running expensive hooks. It is
2575 # a fast, best efforts check. Rietveld still can reject the authentication
2576 # during the actual upload.
2577 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2578 authenticator = auth.get_authenticator_for_host(
2579 cl.GetRietveldServer(), auth_config)
2580 if not authenticator.has_cached_credentials():
2581 raise auth.LoginRequiredError(cl.GetRietveldServer())
2582
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002583 # Apply watchlists on upload.
2584 change = cl.GetChange(base_branch, None)
2585 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2586 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002587 if not options.bypass_watchlists:
2588 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002589
ukai@chromium.orge8077812012-02-03 03:41:46 +00002590 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002591 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002592 # Set the reviewer list now so that presubmit checks can access it.
2593 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002594 change_description.update_reviewers(options.reviewers,
2595 options.tbr_owners,
2596 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002597 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002598 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002599 may_prompt=not options.force,
2600 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002601 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002602 if not hook_results.should_continue():
2603 return 1
2604 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002605 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002606
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002607 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002608 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002609 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002610 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002611 print ('The last upload made from this repository was patchset #%d but '
2612 'the most recent patchset on the server is #%d.'
2613 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002614 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2615 'from another machine or branch the patch you\'re uploading now '
2616 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002617 ask_for_data('About to upload; enter to confirm.')
2618
iannucci@chromium.org79540052012-10-19 23:15:26 +00002619 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002620 if settings.GetIsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002621 if options.squash and options.no_squash:
2622 DieWithError('Can only use one of --squash or --no-squash')
2623
2624 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2625 not options.no_squash)
2626
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00002627 ret = GerritUpload(options, args, cl, change)
2628 else:
2629 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002630 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002631 git_set_branch_value('last-upload-hash',
2632 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002633 # Run post upload hooks, if specified.
2634 if settings.GetRunPostUploadHook():
2635 presubmit_support.DoPostUploadExecuter(
2636 change,
2637 cl,
2638 settings.GetRoot(),
2639 options.verbose,
2640 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002641
rmistry@google.com2dd99862015-06-22 12:22:18 +00002642 # Upload all dependencies if specified.
2643 if options.dependencies:
2644 print
2645 print '--dependencies has been specified.'
2646 print 'All dependent local branches will be re-uploaded.'
2647 print
2648 # Remove the dependencies flag from args so that we do not end up in a
2649 # loop.
2650 orig_args.remove('--dependencies')
2651 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002652 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002653
2654
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002655def IsSubmoduleMergeCommit(ref):
2656 # When submodules are added to the repo, we expect there to be a single
2657 # non-git-svn merge commit at remote HEAD with a signature comment.
2658 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002659 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002660 return RunGit(cmd) != ''
2661
2662
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002663def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002664 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002665
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002666 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002667 Updates changelog with metadata (e.g. pointer to review).
2668 Pushes/dcommits the code upstream.
2669 Updates review and closes.
2670 """
2671 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2672 help='bypass upload presubmit hook')
2673 parser.add_option('-m', dest='message',
2674 help="override review description")
2675 parser.add_option('-f', action='store_true', dest='force',
2676 help="force yes to questions (don't prompt)")
2677 parser.add_option('-c', dest='contributor',
2678 help="external contributor for patch (appended to " +
2679 "description and used as author for git). Should be " +
2680 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002681 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002682 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002683 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002684 auth_config = auth.extract_auth_config_from_options(options)
2685
2686 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002687
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002688 current = cl.GetBranch()
2689 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2690 if not settings.GetIsGitSvn() and remote == '.':
2691 print
2692 print 'Attempting to push branch %r into another local branch!' % current
2693 print
2694 print 'Either reparent this branch on top of origin/master:'
2695 print ' git reparent-branch --root'
2696 print
2697 print 'OR run `git rebase-update` if you think the parent branch is already'
2698 print 'committed.'
2699 print
2700 print ' Current parent: %r' % upstream_branch
2701 return 1
2702
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002703 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002704 # Default to merging against our best guess of the upstream branch.
2705 args = [cl.GetUpstreamBranch()]
2706
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002707 if options.contributor:
2708 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2709 print "Please provide contibutor as 'First Last <email@example.com>'"
2710 return 1
2711
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002712 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002713 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002714
sbc@chromium.org71437c02015-04-09 19:29:40 +00002715 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002716 return 1
2717
2718 # This rev-list syntax means "show all commits not in my branch that
2719 # are in base_branch".
2720 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2721 base_branch]).splitlines()
2722 if upstream_commits:
2723 print ('Base branch "%s" has %d commits '
2724 'not in this branch.' % (base_branch, len(upstream_commits)))
2725 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2726 return 1
2727
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002728 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002729 svn_head = None
2730 if cmd == 'dcommit' or base_has_submodules:
2731 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2732 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002733
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002734 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002735 # If the base_head is a submodule merge commit, the first parent of the
2736 # base_head should be a git-svn commit, which is what we're interested in.
2737 base_svn_head = base_branch
2738 if base_has_submodules:
2739 base_svn_head += '^1'
2740
2741 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002742 if extra_commits:
2743 print ('This branch has %d additional commits not upstreamed yet.'
2744 % len(extra_commits.splitlines()))
2745 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2746 'before attempting to %s.' % (base_branch, cmd))
2747 return 1
2748
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002749 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002750 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002751 author = None
2752 if options.contributor:
2753 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002754 hook_results = cl.RunHook(
2755 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002756 may_prompt=not options.force,
2757 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002758 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002759 if not hook_results.should_continue():
2760 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002761
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002762 # Check the tree status if the tree status URL is set.
2763 status = GetTreeStatus()
2764 if 'closed' == status:
2765 print('The tree is closed. Please wait for it to reopen. Use '
2766 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2767 return 1
2768 elif 'unknown' == status:
2769 print('Unable to determine tree status. Please verify manually and '
2770 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2771 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002772
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002773 change_desc = ChangeDescription(options.message)
2774 if not change_desc.description and cl.GetIssue():
2775 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002776
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002777 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002778 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002779 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002780 else:
2781 print 'No description set.'
2782 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2783 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002784
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002785 # Keep a separate copy for the commit message, because the commit message
2786 # contains the link to the Rietveld issue, while the Rietveld message contains
2787 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002788 # Keep a separate copy for the commit message.
2789 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002790 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002791
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002792 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002793 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002794 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002795 # after it. Add a period on a new line to circumvent this. Also add a space
2796 # before the period to make sure that Gitiles continues to correctly resolve
2797 # the URL.
2798 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002799 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002800 commit_desc.append_footer('Patch from %s.' % options.contributor)
2801
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002802 print('Description:')
2803 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002804
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002805 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002806 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002807 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002808
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002809 # We want to squash all this branch's commits into one commit with the proper
2810 # description. We do this by doing a "reset --soft" to the base branch (which
2811 # keeps the working copy the same), then dcommitting that. If origin/master
2812 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2813 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002814 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002815 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2816 # Delete the branches if they exist.
2817 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2818 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2819 result = RunGitWithCode(showref_cmd)
2820 if result[0] == 0:
2821 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002822
2823 # We might be in a directory that's present in this branch but not in the
2824 # trunk. Move up to the top of the tree so that git commands that expect a
2825 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002826 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002827 if rel_base_path:
2828 os.chdir(rel_base_path)
2829
2830 # Stuff our change into the merge branch.
2831 # We wrap in a try...finally block so if anything goes wrong,
2832 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002833 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002834 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002835 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002836 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002837 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002838 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002839 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002840 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002841 RunGit(
2842 [
2843 'commit', '--author', options.contributor,
2844 '-m', commit_desc.description,
2845 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002846 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002847 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002848 if base_has_submodules:
2849 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2850 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2851 RunGit(['checkout', CHERRY_PICK_BRANCH])
2852 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002853 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002854 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002855 pending_prefix = settings.GetPendingRefPrefix()
2856 if not pending_prefix or branch.startswith(pending_prefix):
2857 # If not using refs/pending/heads/* at all, or target ref is already set
2858 # to pending, then push to the target ref directly.
2859 retcode, output = RunGitWithCode(
2860 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002861 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002862 else:
2863 # Cherry-pick the change on top of pending ref and then push it.
2864 assert branch.startswith('refs/'), branch
2865 assert pending_prefix[-1] == '/', pending_prefix
2866 pending_ref = pending_prefix + branch[len('refs/'):]
2867 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002868 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002869 if retcode == 0:
2870 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002871 else:
2872 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002873 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002874 'svn', 'dcommit',
2875 '-C%s' % options.similarity,
2876 '--no-rebase', '--rmdir',
2877 ]
2878 if settings.GetForceHttpsCommitUrl():
2879 # Allow forcing https commit URLs for some projects that don't allow
2880 # committing to http URLs (like Google Code).
2881 remote_url = cl.GetGitSvnRemoteUrl()
2882 if urlparse.urlparse(remote_url).scheme == 'http':
2883 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002884 cmd_args.append('--commit-url=%s' % remote_url)
2885 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002886 if 'Committed r' in output:
2887 revision = re.match(
2888 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2889 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002890 finally:
2891 # And then swap back to the original branch and clean up.
2892 RunGit(['checkout', '-q', cl.GetBranch()])
2893 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002894 if base_has_submodules:
2895 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002896
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002897 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002898 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002899 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002900
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002901 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002902 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002903 try:
2904 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2905 # We set pushed_to_pending to False, since it made it all the way to the
2906 # real ref.
2907 pushed_to_pending = False
2908 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002909 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002910
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002911 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002912 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002913 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002914 if not to_pending:
2915 if viewvc_url and revision:
2916 change_desc.append_footer(
2917 'Committed: %s%s' % (viewvc_url, revision))
2918 elif revision:
2919 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002920 print ('Closing issue '
2921 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002922 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002923 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002924 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002925 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002926 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002927 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002928 if options.bypass_hooks:
2929 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2930 else:
2931 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002932 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002933 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002934
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002935 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002936 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2937 print 'The commit is in the pending queue (%s).' % pending_ref
2938 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002939 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002940 'footer.' % branch)
2941
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002942 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2943 if os.path.isfile(hook):
2944 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002945
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002946 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002947
2948
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002949def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2950 print
2951 print 'Waiting for commit to be landed on %s...' % real_ref
2952 print '(If you are impatient, you may Ctrl-C once without harm)'
2953 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2954 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2955
2956 loop = 0
2957 while True:
2958 sys.stdout.write('fetching (%d)... \r' % loop)
2959 sys.stdout.flush()
2960 loop += 1
2961
2962 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2963 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2964 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2965 for commit in commits.splitlines():
2966 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2967 print 'Found commit on %s' % real_ref
2968 return commit
2969
2970 current_rev = to_rev
2971
2972
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002973def PushToGitPending(remote, pending_ref, upstream_ref):
2974 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2975
2976 Returns:
2977 (retcode of last operation, output log of last operation).
2978 """
2979 assert pending_ref.startswith('refs/'), pending_ref
2980 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2981 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2982 code = 0
2983 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002984 max_attempts = 3
2985 attempts_left = max_attempts
2986 while attempts_left:
2987 if attempts_left != max_attempts:
2988 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2989 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002990
2991 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002992 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002993 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002994 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002995 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002996 print 'Fetch failed with exit code %d.' % code
2997 if out.strip():
2998 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002999 continue
3000
3001 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003002 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003003 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003004 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003005 if code:
3006 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003007 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3008 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003009 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3010 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003011 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003012 return code, out
3013
3014 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003015 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003016 code, out = RunGitWithCode(
3017 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3018 if code == 0:
3019 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003020 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003021 return code, out
3022
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003023 print 'Push failed with exit code %d.' % code
3024 if out.strip():
3025 print out.strip()
3026 if IsFatalPushFailure(out):
3027 print (
3028 'Fatal push error. Make sure your .netrc credentials and git '
3029 'user.email are correct and you have push access to the repo.')
3030 return code, out
3031
3032 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003033 return code, out
3034
3035
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003036def IsFatalPushFailure(push_stdout):
3037 """True if retrying push won't help."""
3038 return '(prohibited by Gerrit)' in push_stdout
3039
3040
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003041@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003042def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003043 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003044 if not settings.GetIsGitSvn():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003045 if get_footer_svn_id():
3046 # If it looks like previous commits were mirrored with git-svn.
3047 message = """This repository appears to be a git-svn mirror, but no
3048upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3049 else:
3050 message = """This doesn't appear to be an SVN repository.
3051If your project has a true, writeable git repository, you probably want to run
3052'git cl land' instead.
3053If your project has a git mirror of an upstream SVN master, you probably need
3054to run 'git svn init'.
3055
3056Using the wrong command might cause your commit to appear to succeed, and the
3057review to be closed, without actually landing upstream. If you choose to
3058proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003059 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003060 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003061 return SendUpstream(parser, args, 'dcommit')
3062
3063
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003064@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003065def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003066 """Commits the current changelist via git."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003067 if settings.GetIsGitSvn() or get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003068 print('This appears to be an SVN repository.')
3069 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003070 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003071 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003072 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003073
3074
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003075def ParseIssueNum(arg):
3076 """Parses the issue number from args if present otherwise returns None."""
3077 if re.match(r'\d+', arg):
3078 return arg
3079 if arg.startswith('http'):
3080 return re.sub(r'.*/(\d+)/?', r'\1', arg)
3081 return None
3082
3083
3084@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003085def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003086 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003087 parser.add_option('-b', dest='newbranch',
3088 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003089 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003090 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003091 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3092 help='Change to the directory DIR immediately, '
3093 'before doing anything else.')
3094 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003095 help='failed patches spew .rej files rather than '
3096 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003097 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
3098 help="don't commit after patch applies")
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003099
3100 group = optparse.OptionGroup(parser,
3101 """Options for continuing work on the current issue uploaded
3102from a different clone (e.g. different machine). Must be used independently from
3103the other options. No issue number should be specified, and the branch must have
3104an issue number associated with it""")
3105 group.add_option('--reapply', action='store_true',
3106 dest='reapply',
3107 help="""Reset the branch and reapply the issue.
3108CAUTION: This will undo any local changes in this branch""")
3109
3110 group.add_option('--pull', action='store_true', dest='pull',
3111 help="Performs a pull before reapplying.")
3112 parser.add_option_group(group)
3113
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003114 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003115 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003116 auth_config = auth.extract_auth_config_from_options(options)
3117
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003118 issue_arg = None
3119 if options.reapply :
3120 if len(args) > 0:
3121 parser.error("--reapply implies no additional arguments.")
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003122
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003123 cl = Changelist()
3124 issue_arg = cl.GetIssue()
3125 upstream = cl.GetUpstreamBranch()
3126 if upstream == None:
3127 parser.error("No upstream branch specified. Cannot reset branch")
3128
3129 RunGit(['reset', '--hard', upstream])
3130 if options.pull:
3131 RunGit(['pull'])
3132 else:
3133 if len(args) != 1:
3134 parser.error("Must specify issue number")
3135
3136 issue_arg = ParseIssueNum(args[0])
3137
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003138 # The patch URL works because ParseIssueNum won't do any substitution
3139 # as the re.sub pattern fails to match and just returns it.
3140 if issue_arg == None:
3141 parser.print_help()
3142 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003143
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003144 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003145 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003146 return 1
3147
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003148 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00003149 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003150
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003151 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003152 if options.reapply:
3153 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003154 if options.force:
3155 RunGit(['branch', '-D', options.newbranch],
3156 stderr=subprocess2.PIPE, error_ok=True)
3157 RunGit(['checkout', '-b', options.newbranch,
3158 Changelist().GetUpstreamBranch()])
3159
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003160 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003161 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003162
3163
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003164def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00003165 # PatchIssue should never be called with a dirty tree. It is up to the
3166 # caller to check this, but just in case we assert here since the
3167 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003168 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003169
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003170 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003171 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00003172 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003173 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003174 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00003175 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003176 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00003177 # Assume it's a URL to the patch. Default to https.
3178 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003179 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003180 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003181 DieWithError('Must pass an issue ID or full URL for '
3182 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00003183 issue = int(match.group(2))
3184 cl = Changelist(issue=issue, auth_config=auth_config)
3185 cl.rietveld_server = match.group(1)
3186 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003187 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003188
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003189 # Switch up to the top-level directory, if necessary, in preparation for
3190 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003191 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003192 if top:
3193 os.chdir(top)
3194
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003195 # Git patches have a/ at the beginning of source paths. We strip that out
3196 # with a sed script rather than the -p flag to patch so we can feed either
3197 # Git or svn-style patches into the same apply command.
3198 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003199 try:
3200 patch_data = subprocess2.check_output(
3201 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3202 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003203 DieWithError('Git patch mungling failed.')
3204 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003205
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003206 # We use "git apply" to apply the patch instead of "patch" so that we can
3207 # pick up file adds.
3208 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003209 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003210 if directory:
3211 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003212 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003213 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003214 elif IsGitVersionAtLeast('1.7.12'):
3215 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003216 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003217 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003218 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003219 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003220 print 'Failed to apply the patch'
3221 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003222
3223 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003224 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003225 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3226 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003227 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3228 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003229 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003230 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003231 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003232 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003233 else:
3234 print "Patch applied to index."
3235 return 0
3236
3237
3238def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003239 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003240 # Provide a wrapper for git svn rebase to help avoid accidental
3241 # git svn dcommit.
3242 # It's the only command that doesn't use parser at all since we just defer
3243 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003244
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003245 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003246
3247
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003248def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003249 """Fetches the tree status and returns either 'open', 'closed',
3250 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003251 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003252 if url:
3253 status = urllib2.urlopen(url).read().lower()
3254 if status.find('closed') != -1 or status == '0':
3255 return 'closed'
3256 elif status.find('open') != -1 or status == '1':
3257 return 'open'
3258 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003259 return 'unset'
3260
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003261
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003262def GetTreeStatusReason():
3263 """Fetches the tree status from a json url and returns the message
3264 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003265 url = settings.GetTreeStatusUrl()
3266 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003267 connection = urllib2.urlopen(json_url)
3268 status = json.loads(connection.read())
3269 connection.close()
3270 return status['message']
3271
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003272
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003273def GetBuilderMaster(bot_list):
3274 """For a given builder, fetch the master from AE if available."""
3275 map_url = 'https://builders-map.appspot.com/'
3276 try:
3277 master_map = json.load(urllib2.urlopen(map_url))
3278 except urllib2.URLError as e:
3279 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3280 (map_url, e))
3281 except ValueError as e:
3282 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3283 if not master_map:
3284 return None, 'Failed to build master map.'
3285
3286 result_master = ''
3287 for bot in bot_list:
3288 builder = bot.split(':', 1)[0]
3289 master_list = master_map.get(builder, [])
3290 if not master_list:
3291 return None, ('No matching master for builder %s.' % builder)
3292 elif len(master_list) > 1:
3293 return None, ('The builder name %s exists in multiple masters %s.' %
3294 (builder, master_list))
3295 else:
3296 cur_master = master_list[0]
3297 if not result_master:
3298 result_master = cur_master
3299 elif result_master != cur_master:
3300 return None, 'The builders do not belong to the same master.'
3301 return result_master, None
3302
3303
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003304def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003305 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003306 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003307 status = GetTreeStatus()
3308 if 'unset' == status:
3309 print 'You must configure your tree status URL by running "git cl config".'
3310 return 2
3311
3312 print "The tree is %s" % status
3313 print
3314 print GetTreeStatusReason()
3315 if status != 'open':
3316 return 1
3317 return 0
3318
3319
maruel@chromium.org15192402012-09-06 12:38:29 +00003320def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003321 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003322 group = optparse.OptionGroup(parser, "Try job options")
3323 group.add_option(
3324 "-b", "--bot", action="append",
3325 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3326 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003327 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003328 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003329 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003330 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003331 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003332 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003333 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003334 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003335 "-r", "--revision",
3336 help="Revision to use for the try job; default: the "
3337 "revision will be determined by the try server; see "
3338 "its waterfall for more info")
3339 group.add_option(
3340 "-c", "--clobber", action="store_true", default=False,
3341 help="Force a clobber before building; e.g. don't do an "
3342 "incremental build")
3343 group.add_option(
3344 "--project",
3345 help="Override which project to use. Projects are defined "
3346 "server-side to define what default bot set to use")
3347 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003348 "-p", "--property", dest="properties", action="append", default=[],
3349 help="Specify generic properties in the form -p key1=value1 -p "
3350 "key2=value2 etc (buildbucket only). The value will be treated as "
3351 "json if decodable, or as string otherwise.")
3352 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003353 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003354 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003355 "--use-rietveld", action="store_true", default=False,
3356 help="Use Rietveld to trigger try jobs.")
3357 group.add_option(
3358 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3359 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003360 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003361 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003362 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003363 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003364
machenbach@chromium.org45453142015-09-15 08:45:22 +00003365 if options.use_rietveld and options.properties:
3366 parser.error('Properties can only be specified with buildbucket')
3367
3368 # Make sure that all properties are prop=value pairs.
3369 bad_params = [x for x in options.properties if '=' not in x]
3370 if bad_params:
3371 parser.error('Got properties with missing "=": %s' % bad_params)
3372
maruel@chromium.org15192402012-09-06 12:38:29 +00003373 if args:
3374 parser.error('Unknown arguments: %s' % args)
3375
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003376 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003377 if not cl.GetIssue():
3378 parser.error('Need to upload first')
3379
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003380 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003381 if props.get('closed'):
3382 parser.error('Cannot send tryjobs for a closed CL')
3383
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003384 if props.get('private'):
3385 parser.error('Cannot use trybots with private issue')
3386
maruel@chromium.org15192402012-09-06 12:38:29 +00003387 if not options.name:
3388 options.name = cl.GetBranch()
3389
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003390 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003391 options.master, err_msg = GetBuilderMaster(options.bot)
3392 if err_msg:
3393 parser.error('Tryserver master cannot be found because: %s\n'
3394 'Please manually specify the tryserver master'
3395 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003396
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003397 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003398 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003399 if not options.bot:
3400 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003401
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003402 # Get try masters from PRESUBMIT.py files.
3403 masters = presubmit_support.DoGetTryMasters(
3404 change,
3405 change.LocalPaths(),
3406 settings.GetRoot(),
3407 None,
3408 None,
3409 options.verbose,
3410 sys.stdout)
3411 if masters:
3412 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003413
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003414 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3415 options.bot = presubmit_support.DoGetTrySlaves(
3416 change,
3417 change.LocalPaths(),
3418 settings.GetRoot(),
3419 None,
3420 None,
3421 options.verbose,
3422 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003423
3424 if not options.bot:
3425 # Get try masters from cq.cfg if any.
3426 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3427 # location.
3428 cq_cfg = os.path.join(change.RepositoryRoot(),
3429 'infra', 'config', 'cq.cfg')
3430 if os.path.exists(cq_cfg):
3431 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00003432 cq_masters = commit_queue.get_master_builder_map(
3433 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003434 for master, builders in cq_masters.iteritems():
3435 for builder in builders:
3436 # Skip presubmit builders, because these will fail without LGTM.
3437 if 'presubmit' not in builder.lower():
3438 masters.setdefault(master, {})[builder] = ['defaulttests']
3439 if masters:
3440 return masters
3441
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003442 if not options.bot:
3443 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003444
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003445 builders_and_tests = {}
3446 # TODO(machenbach): The old style command-line options don't support
3447 # multiple try masters yet.
3448 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3449 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3450
3451 for bot in old_style:
3452 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003453 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003454 elif ',' in bot:
3455 parser.error('Specify one bot per --bot flag')
3456 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003457 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003458
3459 for bot, tests in new_style:
3460 builders_and_tests.setdefault(bot, []).extend(tests)
3461
3462 # Return a master map with one master to be backwards compatible. The
3463 # master name defaults to an empty string, which will cause the master
3464 # not to be set on rietveld (deprecated).
3465 return {options.master: builders_and_tests}
3466
3467 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003468
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003469 for builders in masters.itervalues():
3470 if any('triggered' in b for b in builders):
3471 print >> sys.stderr, (
3472 'ERROR You are trying to send a job to a triggered bot. This type of'
3473 ' bot requires an\ninitial job from a parent (usually a builder). '
3474 'Instead send your job to the parent.\n'
3475 'Bot list: %s' % builders)
3476 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003477
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003478 patchset = cl.GetMostRecentPatchset()
3479 if patchset and patchset != cl.GetPatchset():
3480 print(
3481 '\nWARNING Mismatch between local config and server. Did a previous '
3482 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3483 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003484 if options.luci:
3485 trigger_luci_job(cl, masters, options)
3486 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003487 try:
3488 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3489 except BuildbucketResponseException as ex:
3490 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003491 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003492 except Exception as e:
3493 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3494 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3495 e, stacktrace)
3496 return 1
3497 else:
3498 try:
3499 cl.RpcServer().trigger_distributed_try_jobs(
3500 cl.GetIssue(), patchset, options.name, options.clobber,
3501 options.revision, masters)
3502 except urllib2.HTTPError as e:
3503 if e.code == 404:
3504 print('404 from rietveld; '
3505 'did you mean to use "git try" instead of "git cl try"?')
3506 return 1
3507 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003508
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003509 for (master, builders) in sorted(masters.iteritems()):
3510 if master:
3511 print 'Master: %s' % master
3512 length = max(len(builder) for builder in builders)
3513 for builder in sorted(builders):
3514 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003515 return 0
3516
3517
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003518def CMDtry_results(parser, args):
3519 group = optparse.OptionGroup(parser, "Try job results options")
3520 group.add_option(
3521 "-p", "--patchset", type=int, help="patchset number if not current.")
3522 group.add_option(
3523 "--print-master", action='store_true', help="print master name as well")
3524 group.add_option(
3525 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3526 help="Host of buildbucket. The default host is %default.")
3527 parser.add_option_group(group)
3528 auth.add_auth_options(parser)
3529 options, args = parser.parse_args(args)
3530 if args:
3531 parser.error('Unrecognized args: %s' % ' '.join(args))
3532
3533 auth_config = auth.extract_auth_config_from_options(options)
3534 cl = Changelist(auth_config=auth_config)
3535 if not cl.GetIssue():
3536 parser.error('Need to upload first')
3537
3538 if not options.patchset:
3539 options.patchset = cl.GetMostRecentPatchset()
3540 if options.patchset and options.patchset != cl.GetPatchset():
3541 print(
3542 '\nWARNING Mismatch between local config and server. Did a previous '
3543 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3544 'Continuing using\npatchset %s.\n' % options.patchset)
3545 try:
3546 jobs = fetch_try_jobs(auth_config, cl, options)
3547 except BuildbucketResponseException as ex:
3548 print 'Buildbucket error: %s' % ex
3549 return 1
3550 except Exception as e:
3551 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3552 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
3553 e, stacktrace)
3554 return 1
3555 print_tryjobs(options, jobs)
3556 return 0
3557
3558
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003559@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003560def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003561 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003562 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003563 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003564 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003565
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003566 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003567 if args:
3568 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003569 branch = cl.GetBranch()
3570 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003571 cl = Changelist()
3572 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003573
3574 # Clear configured merge-base, if there is one.
3575 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003576 else:
3577 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003578 return 0
3579
3580
thestig@chromium.org00858c82013-12-02 23:08:03 +00003581def CMDweb(parser, args):
3582 """Opens the current CL in the web browser."""
3583 _, args = parser.parse_args(args)
3584 if args:
3585 parser.error('Unrecognized args: %s' % ' '.join(args))
3586
3587 issue_url = Changelist().GetIssueURL()
3588 if not issue_url:
3589 print >> sys.stderr, 'ERROR No issue to open'
3590 return 1
3591
3592 webbrowser.open(issue_url)
3593 return 0
3594
3595
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003596def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003597 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003598 auth.add_auth_options(parser)
3599 options, args = parser.parse_args(args)
3600 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003601 if args:
3602 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003603 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003604 props = cl.GetIssueProperties()
3605 if props.get('private'):
3606 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003607 cl.SetFlag('commit', '1')
3608 return 0
3609
3610
groby@chromium.org411034a2013-02-26 15:12:01 +00003611def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003612 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003613 auth.add_auth_options(parser)
3614 options, args = parser.parse_args(args)
3615 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003616 if args:
3617 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003618 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003619 # Ensure there actually is an issue to close.
3620 cl.GetDescription()
3621 cl.CloseIssue()
3622 return 0
3623
3624
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003625def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003626 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003627 auth.add_auth_options(parser)
3628 options, args = parser.parse_args(args)
3629 auth_config = auth.extract_auth_config_from_options(options)
3630 if args:
3631 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003632
3633 # Uncommitted (staged and unstaged) changes will be destroyed by
3634 # "git reset --hard" if there are merging conflicts in PatchIssue().
3635 # Staged changes would be committed along with the patch from last
3636 # upload, hence counted toward the "last upload" side in the final
3637 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003638 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003639 return 1
3640
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003641 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003642 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003643 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003644 if not issue:
3645 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003646 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003647 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003648
3649 # Create a new branch based on the merge-base
3650 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3651 try:
3652 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003653 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003654 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003655 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003656 return rtn
3657
wychen@chromium.org06928532015-02-03 02:11:29 +00003658 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003659 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003660 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003661 finally:
3662 RunGit(['checkout', '-q', branch])
3663 RunGit(['branch', '-D', TMP_BRANCH])
3664
3665 return 0
3666
3667
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003668def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003669 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003670 parser.add_option(
3671 '--no-color',
3672 action='store_true',
3673 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003674 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003675 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003676 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003677
3678 author = RunGit(['config', 'user.email']).strip() or None
3679
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003680 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003681
3682 if args:
3683 if len(args) > 1:
3684 parser.error('Unknown args')
3685 base_branch = args[0]
3686 else:
3687 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003688 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003689
3690 change = cl.GetChange(base_branch, None)
3691 return owners_finder.OwnersFinder(
3692 [f.LocalPath() for f in
3693 cl.GetChange(base_branch, None).AffectedFiles()],
3694 change.RepositoryRoot(), author,
3695 fopen=file, os_path=os.path, glob=glob.glob,
3696 disable_color=options.no_color).run()
3697
3698
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003699def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003700 """Generates a diff command."""
3701 # Generate diff for the current branch's changes.
3702 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3703 upstream_commit, '--' ]
3704
3705 if args:
3706 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003707 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003708 diff_cmd.append(arg)
3709 else:
3710 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003711
3712 return diff_cmd
3713
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003714def MatchingFileType(file_name, extensions):
3715 """Returns true if the file name ends with one of the given extensions."""
3716 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003717
enne@chromium.org555cfe42014-01-29 18:21:39 +00003718@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003719def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003720 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003721 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003722 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003723 parser.add_option('--full', action='store_true',
3724 help='Reformat the full content of all touched files')
3725 parser.add_option('--dry-run', action='store_true',
3726 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003727 parser.add_option('--python', action='store_true',
3728 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003729 parser.add_option('--diff', action='store_true',
3730 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003731 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003732
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003733 # git diff generates paths against the root of the repository. Change
3734 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003735 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003736 if rel_base_path:
3737 os.chdir(rel_base_path)
3738
digit@chromium.org29e47272013-05-17 17:01:46 +00003739 # Grab the merge-base commit, i.e. the upstream commit of the current
3740 # branch when it was created or the last time it was rebased. This is
3741 # to cover the case where the user may have called "git fetch origin",
3742 # moving the origin branch to a newer commit, but hasn't rebased yet.
3743 upstream_commit = None
3744 cl = Changelist()
3745 upstream_branch = cl.GetUpstreamBranch()
3746 if upstream_branch:
3747 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3748 upstream_commit = upstream_commit.strip()
3749
3750 if not upstream_commit:
3751 DieWithError('Could not find base commit for this branch. '
3752 'Are you in detached state?')
3753
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003754 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
3755 diff_output = RunGit(changed_files_cmd)
3756 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00003757 # Filter out files deleted by this CL
3758 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003759
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003760 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
3761 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
3762 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003763 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00003764
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003765 top_dir = os.path.normpath(
3766 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3767
3768 # Locate the clang-format binary in the checkout
3769 try:
3770 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3771 except clang_format.NotFoundError, e:
3772 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003773
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003774 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3775 # formatted. This is used to block during the presubmit.
3776 return_value = 0
3777
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003778 if clang_diff_files:
3779 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003780 cmd = [clang_format_tool]
3781 if not opts.dry_run and not opts.diff:
3782 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003783 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003784 if opts.diff:
3785 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003786 else:
3787 env = os.environ.copy()
3788 env['PATH'] = str(os.path.dirname(clang_format_tool))
3789 try:
3790 script = clang_format.FindClangFormatScriptInChromiumTree(
3791 'clang-format-diff.py')
3792 except clang_format.NotFoundError, e:
3793 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003794
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003795 cmd = [sys.executable, script, '-p0']
3796 if not opts.dry_run and not opts.diff:
3797 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003798
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003799 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
3800 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003801
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003802 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
3803 if opts.diff:
3804 sys.stdout.write(stdout)
3805 if opts.dry_run and len(stdout) > 0:
3806 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003807
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003808 # Similar code to above, but using yapf on .py files rather than clang-format
3809 # on C/C++ files
3810 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003811 yapf_tool = gclient_utils.FindExecutable('yapf')
3812 if yapf_tool is None:
3813 DieWithError('yapf not found in PATH')
3814
3815 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003816 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003817 cmd = [yapf_tool]
3818 if not opts.dry_run and not opts.diff:
3819 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003820 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003821 if opts.diff:
3822 sys.stdout.write(stdout)
3823 else:
3824 # TODO(sbc): yapf --lines mode still has some issues.
3825 # https://github.com/google/yapf/issues/154
3826 DieWithError('--python currently only works with --full')
3827
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003828 # Dart's formatter does not have the nice property of only operating on
3829 # modified chunks, so hard code full.
3830 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003831 try:
3832 command = [dart_format.FindDartFmtToolInChromiumTree()]
3833 if not opts.dry_run and not opts.diff:
3834 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003835 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003836
3837 stdout = RunCommand(command, cwd=top_dir, env=env)
3838 if opts.dry_run and stdout:
3839 return_value = 2
3840 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00003841 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
3842 'found in this checkout. Files in other languages are still ' +
3843 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003844
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003845 # Format GN build files. Always run on full build files for canonical form.
3846 if gn_diff_files:
3847 cmd = ['gn', 'format']
3848 if not opts.dry_run and not opts.diff:
3849 cmd.append('--in-place')
3850 for gn_diff_file in gn_diff_files:
3851 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
3852 if opts.diff:
3853 sys.stdout.write(stdout)
3854
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003855 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003856
3857
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003858@subcommand.usage('<codereview url or issue id>')
3859def CMDcheckout(parser, args):
3860 """Checks out a branch associated with a given Rietveld issue."""
3861 _, args = parser.parse_args(args)
3862
3863 if len(args) != 1:
3864 parser.print_help()
3865 return 1
3866
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003867 target_issue = ParseIssueNum(args[0])
3868 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003869 parser.print_help()
3870 return 1
3871
3872 key_and_issues = [x.split() for x in RunGit(
3873 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
3874 .splitlines()]
3875 branches = []
3876 for key, issue in key_and_issues:
3877 if issue == target_issue:
3878 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
3879
3880 if len(branches) == 0:
3881 print 'No branch found for issue %s.' % target_issue
3882 return 1
3883 if len(branches) == 1:
3884 RunGit(['checkout', branches[0]])
3885 else:
3886 print 'Multiple branches match issue %s:' % target_issue
3887 for i in range(len(branches)):
3888 print '%d: %s' % (i, branches[i])
3889 which = raw_input('Choose by index: ')
3890 try:
3891 RunGit(['checkout', branches[int(which)]])
3892 except (IndexError, ValueError):
3893 print 'Invalid selection, not checking out any branch.'
3894 return 1
3895
3896 return 0
3897
3898
maruel@chromium.org29404b52014-09-08 22:58:00 +00003899def CMDlol(parser, args):
3900 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003901 print zlib.decompress(base64.b64decode(
3902 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3903 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3904 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3905 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003906 return 0
3907
3908
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003909class OptionParser(optparse.OptionParser):
3910 """Creates the option parse and add --verbose support."""
3911 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003912 optparse.OptionParser.__init__(
3913 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003914 self.add_option(
3915 '-v', '--verbose', action='count', default=0,
3916 help='Use 2 times for more debugging info')
3917
3918 def parse_args(self, args=None, values=None):
3919 options, args = optparse.OptionParser.parse_args(self, args, values)
3920 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3921 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3922 return options, args
3923
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003924
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003925def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003926 if sys.hexversion < 0x02060000:
3927 print >> sys.stderr, (
3928 '\nYour python version %s is unsupported, please upgrade.\n' %
3929 sys.version.split(' ', 1)[0])
3930 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003931
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003932 # Reload settings.
3933 global settings
3934 settings = Settings()
3935
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003936 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003937 dispatcher = subcommand.CommandDispatcher(__name__)
3938 try:
3939 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003940 except auth.AuthenticationError as e:
3941 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003942 except urllib2.HTTPError, e:
3943 if e.code != 500:
3944 raise
3945 DieWithError(
3946 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3947 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003948 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003949
3950
3951if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003952 # These affect sys.stdout so do it outside of main() to simplify mocks in
3953 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003954 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003955 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003956 try:
3957 sys.exit(main(sys.argv[1:]))
3958 except KeyboardInterrupt:
3959 sys.stderr.write('interrupted\n')
3960 sys.exit(1)