blob: b0df10b94aef55da2eb08b12e5349f918eca2bb2 [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 )
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000361 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000362
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000363
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000364def fetch_try_jobs(auth_config, changelist, options):
365 """Fetches tryjobs from buildbucket.
366
367 Returns a map from build id to build info as json dictionary.
368 """
369 rietveld_url = settings.GetDefaultServerUrl()
370 rietveld_host = urlparse.urlparse(rietveld_url).hostname
371 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
372 if authenticator.has_cached_credentials():
373 http = authenticator.authorize(httplib2.Http())
374 else:
375 print ('Warning: Some results might be missing because %s' %
376 # Get the message on how to login.
377 auth.LoginRequiredError(rietveld_host).message)
378 http = httplib2.Http()
379
380 http.force_exception_to_status_code = True
381
382 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
383 hostname=rietveld_host,
384 issue=changelist.GetIssue(),
385 patch=options.patchset)
386 params = {'tag': 'buildset:%s' % buildset}
387
388 builds = {}
389 while True:
390 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
391 hostname=options.buildbucket_host,
392 params=urllib.urlencode(params))
393 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
394 for build in content.get('builds', []):
395 builds[build['id']] = build
396 if 'next_cursor' in content:
397 params['start_cursor'] = content['next_cursor']
398 else:
399 break
400 return builds
401
402
403def print_tryjobs(options, builds):
404 """Prints nicely result of fetch_try_jobs."""
405 if not builds:
406 print 'No tryjobs scheduled'
407 return
408
409 # Make a copy, because we'll be modifying builds dictionary.
410 builds = builds.copy()
411 builder_names_cache = {}
412
413 def get_builder(b):
414 try:
415 return builder_names_cache[b['id']]
416 except KeyError:
417 try:
418 parameters = json.loads(b['parameters_json'])
419 name = parameters['builder_name']
420 except (ValueError, KeyError) as error:
421 print 'WARNING: failed to get builder name for build %s: %s' % (
422 b['id'], error)
423 name = None
424 builder_names_cache[b['id']] = name
425 return name
426
427 def get_bucket(b):
428 bucket = b['bucket']
429 if bucket.startswith('master.'):
430 return bucket[len('master.'):]
431 return bucket
432
433 if options.print_master:
434 name_fmt = '%%-%ds %%-%ds' % (
435 max(len(str(get_bucket(b))) for b in builds.itervalues()),
436 max(len(str(get_builder(b))) for b in builds.itervalues()))
437 def get_name(b):
438 return name_fmt % (get_bucket(b), get_builder(b))
439 else:
440 name_fmt = '%%-%ds' % (
441 max(len(str(get_builder(b))) for b in builds.itervalues()))
442 def get_name(b):
443 return name_fmt % get_builder(b)
444
445 def sort_key(b):
446 return b['status'], b.get('result'), get_name(b), b.get('url')
447
448 def pop(title, f, color=None, **kwargs):
449 """Pop matching builds from `builds` dict and print them."""
450
451 if not sys.stdout.isatty() or color is None:
452 colorize = str
453 else:
454 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
455
456 result = []
457 for b in builds.values():
458 if all(b.get(k) == v for k, v in kwargs.iteritems()):
459 builds.pop(b['id'])
460 result.append(b)
461 if result:
462 print colorize(title)
463 for b in sorted(result, key=sort_key):
464 print ' ', colorize('\t'.join(map(str, f(b))))
465
466 total = len(builds)
467 pop(status='COMPLETED', result='SUCCESS',
468 title='Successes:', color=Fore.GREEN,
469 f=lambda b: (get_name(b), b.get('url')))
470 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
471 title='Infra Failures:', color=Fore.MAGENTA,
472 f=lambda b: (get_name(b), b.get('url')))
473 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
474 title='Failures:', color=Fore.RED,
475 f=lambda b: (get_name(b), b.get('url')))
476 pop(status='COMPLETED', result='CANCELED',
477 title='Canceled:', color=Fore.MAGENTA,
478 f=lambda b: (get_name(b),))
479 pop(status='COMPLETED', result='FAILURE',
480 failure_reason='INVALID_BUILD_DEFINITION',
481 title='Wrong master/builder name:', color=Fore.MAGENTA,
482 f=lambda b: (get_name(b),))
483 pop(status='COMPLETED', result='FAILURE',
484 title='Other failures:',
485 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
486 pop(status='COMPLETED',
487 title='Other finished:',
488 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
489 pop(status='STARTED',
490 title='Started:', color=Fore.YELLOW,
491 f=lambda b: (get_name(b), b.get('url')))
492 pop(status='SCHEDULED',
493 title='Scheduled:',
494 f=lambda b: (get_name(b), 'id=%s' % b['id']))
495 # The last section is just in case buildbucket API changes OR there is a bug.
496 pop(title='Other:',
497 f=lambda b: (get_name(b), 'id=%s' % b['id']))
498 assert len(builds) == 0
499 print 'Total: %d tryjobs' % total
500
501
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000502def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
503 """Return the corresponding git ref if |base_url| together with |glob_spec|
504 matches the full |url|.
505
506 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
507 """
508 fetch_suburl, as_ref = glob_spec.split(':')
509 if allow_wildcards:
510 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
511 if glob_match:
512 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
513 # "branches/{472,597,648}/src:refs/remotes/svn/*".
514 branch_re = re.escape(base_url)
515 if glob_match.group(1):
516 branch_re += '/' + re.escape(glob_match.group(1))
517 wildcard = glob_match.group(2)
518 if wildcard == '*':
519 branch_re += '([^/]*)'
520 else:
521 # Escape and replace surrounding braces with parentheses and commas
522 # with pipe symbols.
523 wildcard = re.escape(wildcard)
524 wildcard = re.sub('^\\\\{', '(', wildcard)
525 wildcard = re.sub('\\\\,', '|', wildcard)
526 wildcard = re.sub('\\\\}$', ')', wildcard)
527 branch_re += wildcard
528 if glob_match.group(3):
529 branch_re += re.escape(glob_match.group(3))
530 match = re.match(branch_re, url)
531 if match:
532 return re.sub('\*$', match.group(1), as_ref)
533
534 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
535 if fetch_suburl:
536 full_url = base_url + '/' + fetch_suburl
537 else:
538 full_url = base_url
539 if full_url == url:
540 return as_ref
541 return None
542
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000543
iannucci@chromium.org79540052012-10-19 23:15:26 +0000544def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000545 """Prints statistics about the change to the user."""
546 # --no-ext-diff is broken in some versions of Git, so try to work around
547 # this by overriding the environment (but there is still a problem if the
548 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000549 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000550 if 'GIT_EXTERNAL_DIFF' in env:
551 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000552
553 if find_copies:
554 similarity_options = ['--find-copies-harder', '-l100000',
555 '-C%s' % similarity]
556 else:
557 similarity_options = ['-M%s' % similarity]
558
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000559 try:
560 stdout = sys.stdout.fileno()
561 except AttributeError:
562 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000563 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000564 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000565 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000566 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000567
568
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000569class BuildbucketResponseException(Exception):
570 pass
571
572
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000573class Settings(object):
574 def __init__(self):
575 self.default_server = None
576 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000577 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000578 self.is_git_svn = None
579 self.svn_branch = None
580 self.tree_status_url = None
581 self.viewvc_url = None
582 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000583 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000584 self.squash_gerrit_uploads = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000585 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000586 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000587 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000588 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000589
590 def LazyUpdateIfNeeded(self):
591 """Updates the settings from a codereview.settings file, if available."""
592 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000593 # The only value that actually changes the behavior is
594 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000595 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000596 error_ok=True
597 ).strip().lower()
598
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000599 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000600 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000601 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000602 # set updated to True to avoid infinite calling loop
603 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000604 self.updated = True
605 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000606 self.updated = True
607
608 def GetDefaultServerUrl(self, error_ok=False):
609 if not self.default_server:
610 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000611 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000612 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000613 if error_ok:
614 return self.default_server
615 if not self.default_server:
616 error_message = ('Could not find settings file. You must configure '
617 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000618 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000619 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000620 return self.default_server
621
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000622 @staticmethod
623 def GetRelativeRoot():
624 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000625
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000626 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000627 if self.root is None:
628 self.root = os.path.abspath(self.GetRelativeRoot())
629 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000630
631 def GetIsGitSvn(self):
632 """Return true if this repo looks like it's using git-svn."""
633 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000634 if self.GetPendingRefPrefix():
635 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
636 self.is_git_svn = False
637 else:
638 # If you have any "svn-remote.*" config keys, we think you're using svn.
639 self.is_git_svn = RunGitWithCode(
640 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000641 return self.is_git_svn
642
643 def GetSVNBranch(self):
644 if self.svn_branch is None:
645 if not self.GetIsGitSvn():
646 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
647
648 # Try to figure out which remote branch we're based on.
649 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000650 # 1) iterate through our branch history and find the svn URL.
651 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000652
653 # regexp matching the git-svn line that contains the URL.
654 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
655
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000656 # We don't want to go through all of history, so read a line from the
657 # pipe at a time.
658 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000659 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000660 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
661 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000662 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000663 for line in proc.stdout:
664 match = git_svn_re.match(line)
665 if match:
666 url = match.group(1)
667 proc.stdout.close() # Cut pipe.
668 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000669
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000670 if url:
671 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
672 remotes = RunGit(['config', '--get-regexp',
673 r'^svn-remote\..*\.url']).splitlines()
674 for remote in remotes:
675 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000676 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000677 remote = match.group(1)
678 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000679 rewrite_root = RunGit(
680 ['config', 'svn-remote.%s.rewriteRoot' % remote],
681 error_ok=True).strip()
682 if rewrite_root:
683 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000684 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000685 ['config', 'svn-remote.%s.fetch' % remote],
686 error_ok=True).strip()
687 if fetch_spec:
688 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
689 if self.svn_branch:
690 break
691 branch_spec = RunGit(
692 ['config', 'svn-remote.%s.branches' % remote],
693 error_ok=True).strip()
694 if branch_spec:
695 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
696 if self.svn_branch:
697 break
698 tag_spec = RunGit(
699 ['config', 'svn-remote.%s.tags' % remote],
700 error_ok=True).strip()
701 if tag_spec:
702 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
703 if self.svn_branch:
704 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000705
706 if not self.svn_branch:
707 DieWithError('Can\'t guess svn branch -- try specifying it on the '
708 'command line')
709
710 return self.svn_branch
711
712 def GetTreeStatusUrl(self, error_ok=False):
713 if not self.tree_status_url:
714 error_message = ('You must configure your tree status URL by running '
715 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000716 self.tree_status_url = self._GetRietveldConfig(
717 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000718 return self.tree_status_url
719
720 def GetViewVCUrl(self):
721 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000722 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000723 return self.viewvc_url
724
rmistry@google.com90752582014-01-14 21:04:50 +0000725 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000726 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000727
rmistry@google.com78948ed2015-07-08 23:09:57 +0000728 def GetIsSkipDependencyUpload(self, branch_name):
729 """Returns true if specified branch should skip dep uploads."""
730 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
731 error_ok=True)
732
rmistry@google.com5626a922015-02-26 14:03:30 +0000733 def GetRunPostUploadHook(self):
734 run_post_upload_hook = self._GetRietveldConfig(
735 'run-post-upload-hook', error_ok=True)
736 return run_post_upload_hook == "True"
737
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000738 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000739 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000740
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000741 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000742 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000743
ukai@chromium.orge8077812012-02-03 03:41:46 +0000744 def GetIsGerrit(self):
745 """Return true if this repo is assosiated with gerrit code review system."""
746 if self.is_gerrit is None:
747 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
748 return self.is_gerrit
749
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000750 def GetSquashGerritUploads(self):
751 """Return true if uploads to Gerrit should be squashed by default."""
752 if self.squash_gerrit_uploads is None:
753 self.squash_gerrit_uploads = (
754 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
755 error_ok=True).strip() == 'true')
756 return self.squash_gerrit_uploads
757
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000758 def GetGitEditor(self):
759 """Return the editor specified in the git config, or None if none is."""
760 if self.git_editor is None:
761 self.git_editor = self._GetConfig('core.editor', error_ok=True)
762 return self.git_editor or None
763
thestig@chromium.org44202a22014-03-11 19:22:18 +0000764 def GetLintRegex(self):
765 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
766 DEFAULT_LINT_REGEX)
767
768 def GetLintIgnoreRegex(self):
769 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
770 DEFAULT_LINT_IGNORE_REGEX)
771
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000772 def GetProject(self):
773 if not self.project:
774 self.project = self._GetRietveldConfig('project', error_ok=True)
775 return self.project
776
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000777 def GetForceHttpsCommitUrl(self):
778 if not self.force_https_commit_url:
779 self.force_https_commit_url = self._GetRietveldConfig(
780 'force-https-commit-url', error_ok=True)
781 return self.force_https_commit_url
782
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000783 def GetPendingRefPrefix(self):
784 if not self.pending_ref_prefix:
785 self.pending_ref_prefix = self._GetRietveldConfig(
786 'pending-ref-prefix', error_ok=True)
787 return self.pending_ref_prefix
788
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000789 def _GetRietveldConfig(self, param, **kwargs):
790 return self._GetConfig('rietveld.' + param, **kwargs)
791
rmistry@google.com78948ed2015-07-08 23:09:57 +0000792 def _GetBranchConfig(self, branch_name, param, **kwargs):
793 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
794
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795 def _GetConfig(self, param, **kwargs):
796 self.LazyUpdateIfNeeded()
797 return RunGit(['config', param], **kwargs).strip()
798
799
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000800def ShortBranchName(branch):
801 """Convert a name like 'refs/heads/foo' to just 'foo'."""
802 return branch.replace('refs/heads/', '')
803
804
805class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000806 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000808 global settings
809 if not settings:
810 # Happens when git_cl.py is used as a utility library.
811 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000812 settings.GetDefaultServerUrl()
813 self.branchref = branchref
814 if self.branchref:
815 self.branch = ShortBranchName(self.branchref)
816 else:
817 self.branch = None
818 self.rietveld_server = None
819 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000820 self.lookedup_issue = False
821 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000822 self.has_description = False
823 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000824 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000825 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000826 self.cc = None
827 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000828 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000829 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000830 self._remote = None
831 self._rpc_server = None
832
833 @property
834 def auth_config(self):
835 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000836
837 def GetCCList(self):
838 """Return the users cc'd on this CL.
839
840 Return is a string suitable for passing to gcl with the --cc flag.
841 """
842 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000843 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000844 more_cc = ','.join(self.watchers)
845 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
846 return self.cc
847
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000848 def GetCCListWithoutDefault(self):
849 """Return the users cc'd on this CL excluding default ones."""
850 if self.cc is None:
851 self.cc = ','.join(self.watchers)
852 return self.cc
853
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000854 def SetWatchers(self, watchers):
855 """Set the list of email addresses that should be cc'd based on the changed
856 files in this CL.
857 """
858 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000859
860 def GetBranch(self):
861 """Returns the short branch name, e.g. 'master'."""
862 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000863 branchref = RunGit(['symbolic-ref', 'HEAD'],
864 stderr=subprocess2.VOID, error_ok=True).strip()
865 if not branchref:
866 return None
867 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000868 self.branch = ShortBranchName(self.branchref)
869 return self.branch
870
871 def GetBranchRef(self):
872 """Returns the full branch name, e.g. 'refs/heads/master'."""
873 self.GetBranch() # Poke the lazy loader.
874 return self.branchref
875
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000876 @staticmethod
877 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000878 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000879 e.g. 'origin', 'refs/heads/master'
880 """
881 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000882 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
883 error_ok=True).strip()
884 if upstream_branch:
885 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
886 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000887 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
888 error_ok=True).strip()
889 if upstream_branch:
890 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000891 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000892 # Fall back on trying a git-svn upstream branch.
893 if settings.GetIsGitSvn():
894 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000895 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000896 # Else, try to guess the origin remote.
897 remote_branches = RunGit(['branch', '-r']).split()
898 if 'origin/master' in remote_branches:
899 # Fall back on origin/master if it exits.
900 remote = 'origin'
901 upstream_branch = 'refs/heads/master'
902 elif 'origin/trunk' in remote_branches:
903 # Fall back on origin/trunk if it exists. Generally a shared
904 # git-svn clone
905 remote = 'origin'
906 upstream_branch = 'refs/heads/trunk'
907 else:
908 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000909Either pass complete "git diff"-style arguments, like
910 git cl upload origin/master
911or verify this branch is set up to track another (via the --track argument to
912"git checkout -b ...").""")
913
914 return remote, upstream_branch
915
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000916 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000917 upstream_branch = self.GetUpstreamBranch()
918 if not BranchExists(upstream_branch):
919 DieWithError('The upstream for the current branch (%s) does not exist '
920 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000921 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000922 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000923
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000924 def GetUpstreamBranch(self):
925 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000926 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000927 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000928 upstream_branch = upstream_branch.replace('refs/heads/',
929 'refs/remotes/%s/' % remote)
930 upstream_branch = upstream_branch.replace('refs/branch-heads/',
931 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000932 self.upstream_branch = upstream_branch
933 return self.upstream_branch
934
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000935 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000936 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000937 remote, branch = None, self.GetBranch()
938 seen_branches = set()
939 while branch not in seen_branches:
940 seen_branches.add(branch)
941 remote, branch = self.FetchUpstreamTuple(branch)
942 branch = ShortBranchName(branch)
943 if remote != '.' or branch.startswith('refs/remotes'):
944 break
945 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000946 remotes = RunGit(['remote'], error_ok=True).split()
947 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000948 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000949 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000950 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000951 logging.warning('Could not determine which remote this change is '
952 'associated with, so defaulting to "%s". This may '
953 'not be what you want. You may prevent this message '
954 'by running "git svn info" as documented here: %s',
955 self._remote,
956 GIT_INSTRUCTIONS_URL)
957 else:
958 logging.warn('Could not determine which remote this change is '
959 'associated with. You may prevent this message by '
960 'running "git svn info" as documented here: %s',
961 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000962 branch = 'HEAD'
963 if branch.startswith('refs/remotes'):
964 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000965 elif branch.startswith('refs/branch-heads/'):
966 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000967 else:
968 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000969 return self._remote
970
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000971 def GitSanityChecks(self, upstream_git_obj):
972 """Checks git repo status and ensures diff is from local commits."""
973
sbc@chromium.org79706062015-01-14 21:18:12 +0000974 if upstream_git_obj is None:
975 if self.GetBranch() is None:
976 print >> sys.stderr, (
dpranke@chromium.orgee87f582015-07-31 18:46:25 +0000977 'ERROR: unable to determine current branch (detached HEAD?)')
sbc@chromium.org79706062015-01-14 21:18:12 +0000978 else:
979 print >> sys.stderr, (
980 'ERROR: no upstream branch')
981 return False
982
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000983 # Verify the commit we're diffing against is in our current branch.
984 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
985 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
986 if upstream_sha != common_ancestor:
987 print >> sys.stderr, (
988 'ERROR: %s is not in the current branch. You may need to rebase '
989 'your tracking branch' % upstream_sha)
990 return False
991
992 # List the commits inside the diff, and verify they are all local.
993 commits_in_diff = RunGit(
994 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
995 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
996 remote_branch = remote_branch.strip()
997 if code != 0:
998 _, remote_branch = self.GetRemoteBranch()
999
1000 commits_in_remote = RunGit(
1001 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1002
1003 common_commits = set(commits_in_diff) & set(commits_in_remote)
1004 if common_commits:
1005 print >> sys.stderr, (
1006 'ERROR: Your diff contains %d commits already in %s.\n'
1007 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1008 'the diff. If you are using a custom git flow, you can override'
1009 ' the reference used for this check with "git config '
1010 'gitcl.remotebranch <git-ref>".' % (
1011 len(common_commits), remote_branch, upstream_git_obj))
1012 return False
1013 return True
1014
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001015 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001016 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001017
1018 Returns None if it is not set.
1019 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001020 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1021 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001022
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001023 def GetGitSvnRemoteUrl(self):
1024 """Return the configured git-svn remote URL parsed from git svn info.
1025
1026 Returns None if it is not set.
1027 """
1028 # URL is dependent on the current directory.
1029 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1030 if data:
1031 keys = dict(line.split(': ', 1) for line in data.splitlines()
1032 if ': ' in line)
1033 return keys.get('URL', None)
1034 return None
1035
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001036 def GetRemoteUrl(self):
1037 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1038
1039 Returns None if there is no remote.
1040 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001041 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001042 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1043
1044 # If URL is pointing to a local directory, it is probably a git cache.
1045 if os.path.isdir(url):
1046 url = RunGit(['config', 'remote.%s.url' % remote],
1047 error_ok=True,
1048 cwd=url).strip()
1049 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001050
1051 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001052 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001053 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001054 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001055 self.issue = int(issue) or None if issue else None
1056 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001057 return self.issue
1058
1059 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +00001060 if not self.rietveld_server:
1061 # If we're on a branch then get the server potentially associated
1062 # with that branch.
1063 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001064 rietveld_server_config = self._RietveldServer()
1065 if rietveld_server_config:
1066 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1067 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +00001068 if not self.rietveld_server:
1069 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001070 return self.rietveld_server
1071
1072 def GetIssueURL(self):
1073 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001074 if not self.GetIssue():
1075 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001076 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
1077
1078 def GetDescription(self, pretty=False):
1079 if not self.has_description:
1080 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +00001081 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +00001082 try:
1083 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +00001084 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +00001085 if e.code == 404:
1086 DieWithError(
1087 ('\nWhile fetching the description for issue %d, received a '
1088 '404 (not found)\n'
1089 'error. It is likely that you deleted this '
1090 'issue on the server. If this is the\n'
1091 'case, please run\n\n'
1092 ' git cl issue 0\n\n'
1093 'to clear the association with the deleted issue. Then run '
1094 'this command again.') % issue)
1095 else:
1096 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +00001097 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +00001098 except urllib2.URLError as e:
1099 print >> sys.stderr, (
1100 'Warning: Failed to retrieve CL description due to network '
1101 'failure.')
1102 self.description = ''
1103
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001104 self.has_description = True
1105 if pretty:
1106 wrapper = textwrap.TextWrapper()
1107 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1108 return wrapper.fill(self.description)
1109 return self.description
1110
1111 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001112 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001113 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001114 patchset = RunGit(['config', self._PatchsetSetting()],
1115 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001116 self.patchset = int(patchset) or None if patchset else None
1117 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001118 return self.patchset
1119
1120 def SetPatchset(self, patchset):
1121 """Set this branch's patchset. If patchset=0, clears the patchset."""
1122 if patchset:
1123 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001124 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001125 else:
1126 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001127 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001128 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001130 def GetMostRecentPatchset(self):
1131 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +00001132
1133 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001134 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001135 '/download/issue%s_%s.diff' % (issue, patchset))
1136
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001137 def GetIssueProperties(self):
1138 if self._props is None:
1139 issue = self.GetIssue()
1140 if not issue:
1141 self._props = {}
1142 else:
1143 self._props = self.RpcServer().get_issue_properties(issue, True)
1144 return self._props
1145
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001146 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001147 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001148
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001149 def AddComment(self, message):
1150 return self.RpcServer().add_comment(self.GetIssue(), message)
1151
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001152 def SetIssue(self, issue):
1153 """Set this branch's issue. If issue=0, clears the issue."""
1154 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001155 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156 RunGit(['config', self._IssueSetting(), str(issue)])
1157 if self.rietveld_server:
1158 RunGit(['config', self._RietveldServer(), self.rietveld_server])
1159 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +00001160 current_issue = self.GetIssue()
1161 if current_issue:
1162 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001163 self.issue = None
1164 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001166 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001167 if not self.GitSanityChecks(upstream_branch):
1168 DieWithError('\nGit sanity check failure')
1169
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001170 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001171 if not root:
1172 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001173 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001174
1175 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001176 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001177 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001178 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001179 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001180 except subprocess2.CalledProcessError:
1181 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001182 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001183 'This branch probably doesn\'t exist anymore. To reset the\n'
1184 'tracking branch, please run\n'
1185 ' git branch --set-upstream %s trunk\n'
1186 'replacing trunk with origin/master or the relevant branch') %
1187 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001188
maruel@chromium.org52424302012-08-29 15:14:30 +00001189 issue = self.GetIssue()
1190 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001191 if issue:
1192 description = self.GetDescription()
1193 else:
1194 # If the change was never uploaded, use the log messages of all commits
1195 # up to the branch point, as git cl upload will prefill the description
1196 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001197 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1198 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001199
1200 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001201 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001202 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001203 name,
1204 description,
1205 absroot,
1206 files,
1207 issue,
1208 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001209 author,
1210 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001211
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001212 def GetStatus(self):
1213 """Apply a rough heuristic to give a simple summary of an issue's review
1214 or CQ status, assuming adherence to a common workflow.
1215
1216 Returns None if no issue for this branch, or one of the following keywords:
1217 * 'error' - error from review tool (including deleted issues)
1218 * 'unsent' - not sent for review
1219 * 'waiting' - waiting for review
1220 * 'reply' - waiting for owner to reply to review
1221 * 'lgtm' - LGTM from at least one approved reviewer
1222 * 'commit' - in the commit queue
1223 * 'closed' - closed
1224 """
1225 if not self.GetIssue():
1226 return None
1227
1228 try:
1229 props = self.GetIssueProperties()
1230 except urllib2.HTTPError:
1231 return 'error'
1232
1233 if props.get('closed'):
1234 # Issue is closed.
1235 return 'closed'
1236 if props.get('commit'):
1237 # Issue is in the commit queue.
1238 return 'commit'
1239
1240 try:
1241 reviewers = self.GetApprovingReviewers()
1242 except urllib2.HTTPError:
1243 return 'error'
1244
1245 if reviewers:
1246 # Was LGTM'ed.
1247 return 'lgtm'
1248
1249 messages = props.get('messages') or []
1250
1251 if not messages:
1252 # No message was sent.
1253 return 'unsent'
1254 if messages[-1]['sender'] != props.get('owner_email'):
1255 # Non-LGTM reply from non-owner
1256 return 'reply'
1257 return 'waiting'
1258
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001259 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001260 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001261
1262 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001263 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001264 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001265 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001266 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001267 except presubmit_support.PresubmitFailure, e:
1268 DieWithError(
1269 ('%s\nMaybe your depot_tools is out of date?\n'
1270 'If all fails, contact maruel@') % e)
1271
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001272 def UpdateDescription(self, description):
1273 self.description = description
1274 return self.RpcServer().update_description(
1275 self.GetIssue(), self.description)
1276
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001277 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001278 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001279 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001280
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001281 def SetFlag(self, flag, value):
1282 """Patchset must match."""
1283 if not self.GetPatchset():
1284 DieWithError('The patchset needs to match. Send another patchset.')
1285 try:
1286 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001287 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001288 except urllib2.HTTPError, e:
1289 if e.code == 404:
1290 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1291 if e.code == 403:
1292 DieWithError(
1293 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1294 'match?') % (self.GetIssue(), self.GetPatchset()))
1295 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001296
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001297 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298 """Returns an upload.RpcServer() to access this review's rietveld instance.
1299 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001300 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001301 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001302 self.GetRietveldServer(),
1303 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001304 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001305
1306 def _IssueSetting(self):
1307 """Return the git setting that stores this change's issue."""
1308 return 'branch.%s.rietveldissue' % self.GetBranch()
1309
1310 def _PatchsetSetting(self):
1311 """Return the git setting that stores this change's most recent patchset."""
1312 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1313
1314 def _RietveldServer(self):
1315 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001316 branch = self.GetBranch()
1317 if branch:
1318 return 'branch.%s.rietveldserver' % branch
1319 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001320
1321
1322def GetCodereviewSettingsInteractively():
1323 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001324 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001325 server = settings.GetDefaultServerUrl(error_ok=True)
1326 prompt = 'Rietveld server (host[:port])'
1327 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001328 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001329 if not server and not newserver:
1330 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001331 if newserver:
1332 newserver = gclient_utils.UpgradeToHttps(newserver)
1333 if newserver != server:
1334 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001335
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001336 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337 prompt = caption
1338 if initial:
1339 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001340 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001341 if new_val == 'x':
1342 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001343 elif new_val:
1344 if is_url:
1345 new_val = gclient_utils.UpgradeToHttps(new_val)
1346 if new_val != initial:
1347 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001348
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001349 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001350 SetProperty(settings.GetDefaultPrivateFlag(),
1351 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001352 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001353 'tree-status-url', False)
1354 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001355 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001356 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1357 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001358
1359 # TODO: configure a default branch to diff against, rather than this
1360 # svn-based hackery.
1361
1362
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001363class ChangeDescription(object):
1364 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001365 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001366 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001367
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001368 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001369 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001370
agable@chromium.org42c20792013-09-12 17:34:49 +00001371 @property # www.logilab.org/ticket/89786
1372 def description(self): # pylint: disable=E0202
1373 return '\n'.join(self._description_lines)
1374
1375 def set_description(self, desc):
1376 if isinstance(desc, basestring):
1377 lines = desc.splitlines()
1378 else:
1379 lines = [line.rstrip() for line in desc]
1380 while lines and not lines[0]:
1381 lines.pop(0)
1382 while lines and not lines[-1]:
1383 lines.pop(-1)
1384 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001385
piman@chromium.org336f9122014-09-04 02:16:55 +00001386 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001387 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001388 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001389 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001390 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001391 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001392
agable@chromium.org42c20792013-09-12 17:34:49 +00001393 # Get the set of R= and TBR= lines and remove them from the desciption.
1394 regexp = re.compile(self.R_LINE)
1395 matches = [regexp.match(line) for line in self._description_lines]
1396 new_desc = [l for i, l in enumerate(self._description_lines)
1397 if not matches[i]]
1398 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001399
agable@chromium.org42c20792013-09-12 17:34:49 +00001400 # Construct new unified R= and TBR= lines.
1401 r_names = []
1402 tbr_names = []
1403 for match in matches:
1404 if not match:
1405 continue
1406 people = cleanup_list([match.group(2).strip()])
1407 if match.group(1) == 'TBR':
1408 tbr_names.extend(people)
1409 else:
1410 r_names.extend(people)
1411 for name in r_names:
1412 if name not in reviewers:
1413 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001414 if add_owners_tbr:
1415 owners_db = owners.Database(change.RepositoryRoot(),
1416 fopen=file, os_path=os.path, glob=glob.glob)
1417 all_reviewers = set(tbr_names + reviewers)
1418 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1419 all_reviewers)
1420 tbr_names.extend(owners_db.reviewers_for(missing_files,
1421 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001422 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1423 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1424
1425 # Put the new lines in the description where the old first R= line was.
1426 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1427 if 0 <= line_loc < len(self._description_lines):
1428 if new_tbr_line:
1429 self._description_lines.insert(line_loc, new_tbr_line)
1430 if new_r_line:
1431 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001432 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001433 if new_r_line:
1434 self.append_footer(new_r_line)
1435 if new_tbr_line:
1436 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001437
1438 def prompt(self):
1439 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001440 self.set_description([
1441 '# Enter a description of the change.',
1442 '# This will be displayed on the codereview site.',
1443 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001444 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001445 '--------------------',
1446 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001447
agable@chromium.org42c20792013-09-12 17:34:49 +00001448 regexp = re.compile(self.BUG_LINE)
1449 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001450 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001451 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001452 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001453 if not content:
1454 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001455 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001456
1457 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001458 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1459 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001460 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001461 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001462
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001463 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001464 if self._description_lines:
1465 # Add an empty line if either the last line or the new line isn't a tag.
1466 last_line = self._description_lines[-1]
1467 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1468 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1469 self._description_lines.append('')
1470 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001471
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001472 def get_reviewers(self):
1473 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001474 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1475 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001476 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001477
1478
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001479def get_approving_reviewers(props):
1480 """Retrieves the reviewers that approved a CL from the issue properties with
1481 messages.
1482
1483 Note that the list may contain reviewers that are not committer, thus are not
1484 considered by the CQ.
1485 """
1486 return sorted(
1487 set(
1488 message['sender']
1489 for message in props['messages']
1490 if message['approval'] and message['sender'] in props['reviewers']
1491 )
1492 )
1493
1494
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001495def FindCodereviewSettingsFile(filename='codereview.settings'):
1496 """Finds the given file starting in the cwd and going up.
1497
1498 Only looks up to the top of the repository unless an
1499 'inherit-review-settings-ok' file exists in the root of the repository.
1500 """
1501 inherit_ok_file = 'inherit-review-settings-ok'
1502 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001503 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001504 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1505 root = '/'
1506 while True:
1507 if filename in os.listdir(cwd):
1508 if os.path.isfile(os.path.join(cwd, filename)):
1509 return open(os.path.join(cwd, filename))
1510 if cwd == root:
1511 break
1512 cwd = os.path.dirname(cwd)
1513
1514
1515def LoadCodereviewSettingsFromFile(fileobj):
1516 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001517 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001518
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001519 def SetProperty(name, setting, unset_error_ok=False):
1520 fullname = 'rietveld.' + name
1521 if setting in keyvals:
1522 RunGit(['config', fullname, keyvals[setting]])
1523 else:
1524 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1525
1526 SetProperty('server', 'CODE_REVIEW_SERVER')
1527 # Only server setting is required. Other settings can be absent.
1528 # In that case, we ignore errors raised during option deletion attempt.
1529 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001530 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001531 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1532 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001533 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001534 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001535 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1536 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001537 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001538 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001539 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001540 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1541 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001542
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001543 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001544 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001545
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00001546 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
1547 RunGit(['config', 'gerrit.squash-uploads',
1548 keyvals['GERRIT_SQUASH_UPLOADS']])
1549
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001550 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1551 #should be of the form
1552 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1553 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1554 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1555 keyvals['ORIGIN_URL_CONFIG']])
1556
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001557
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001558def urlretrieve(source, destination):
1559 """urllib is broken for SSL connections via a proxy therefore we
1560 can't use urllib.urlretrieve()."""
1561 with open(destination, 'w') as f:
1562 f.write(urllib2.urlopen(source).read())
1563
1564
ukai@chromium.org712d6102013-11-27 00:52:58 +00001565def hasSheBang(fname):
1566 """Checks fname is a #! script."""
1567 with open(fname) as f:
1568 return f.read(2).startswith('#!')
1569
1570
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001571def DownloadHooks(force):
1572 """downloads hooks
1573
1574 Args:
1575 force: True to update hooks. False to install hooks if not present.
1576 """
1577 if not settings.GetIsGerrit():
1578 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001579 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001580 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1581 if not os.access(dst, os.X_OK):
1582 if os.path.exists(dst):
1583 if not force:
1584 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001585 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001586 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001587 if not hasSheBang(dst):
1588 DieWithError('Not a script: %s\n'
1589 'You need to download from\n%s\n'
1590 'into .git/hooks/commit-msg and '
1591 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001592 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1593 except Exception:
1594 if os.path.exists(dst):
1595 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001596 DieWithError('\nFailed to download hooks.\n'
1597 'You need to download from\n%s\n'
1598 'into .git/hooks/commit-msg and '
1599 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001600
1601
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001602@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001603def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001604 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001605
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001606 parser.add_option('--activate-update', action='store_true',
1607 help='activate auto-updating [rietveld] section in '
1608 '.git/config')
1609 parser.add_option('--deactivate-update', action='store_true',
1610 help='deactivate auto-updating [rietveld] section in '
1611 '.git/config')
1612 options, args = parser.parse_args(args)
1613
1614 if options.deactivate_update:
1615 RunGit(['config', 'rietveld.autoupdate', 'false'])
1616 return
1617
1618 if options.activate_update:
1619 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1620 return
1621
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001622 if len(args) == 0:
1623 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001624 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001625 return 0
1626
1627 url = args[0]
1628 if not url.endswith('codereview.settings'):
1629 url = os.path.join(url, 'codereview.settings')
1630
1631 # Load code review settings and download hooks (if available).
1632 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001633 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001634 return 0
1635
1636
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001637def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001638 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001639 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1640 branch = ShortBranchName(branchref)
1641 _, args = parser.parse_args(args)
1642 if not args:
1643 print("Current base-url:")
1644 return RunGit(['config', 'branch.%s.base-url' % branch],
1645 error_ok=False).strip()
1646 else:
1647 print("Setting base-url to %s" % args[0])
1648 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1649 error_ok=False).strip()
1650
1651
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001652def color_for_status(status):
1653 """Maps a Changelist status to color, for CMDstatus and other tools."""
1654 return {
1655 'unsent': Fore.RED,
1656 'waiting': Fore.BLUE,
1657 'reply': Fore.YELLOW,
1658 'lgtm': Fore.GREEN,
1659 'commit': Fore.MAGENTA,
1660 'closed': Fore.CYAN,
1661 'error': Fore.WHITE,
1662 }.get(status, Fore.WHITE)
1663
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001664def fetch_cl_status(branch, auth_config=None):
1665 """Fetches information for an issue and returns (branch, issue, status)."""
1666 cl = Changelist(branchref=branch, auth_config=auth_config)
1667 url = cl.GetIssueURL()
1668 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001669
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001670 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001671 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001672 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001673
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001674 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001675
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001676def get_cl_statuses(
1677 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001678 """Returns a blocking iterable of (branch, issue, color) for given branches.
1679
1680 If fine_grained is true, this will fetch CL statuses from the server.
1681 Otherwise, simply indicate if there's a matching url for the given branches.
1682
1683 If max_processes is specified, it is used as the maximum number of processes
1684 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1685 spawned.
1686 """
1687 # Silence upload.py otherwise it becomes unwieldly.
1688 upload.verbosity = 0
1689
1690 if fine_grained:
1691 # Process one branch synchronously to work through authentication, then
1692 # spawn processes to process all the other branches in parallel.
1693 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001694 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1695 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001696
1697 branches_to_fetch = branches[1:]
1698 pool = ThreadPool(
1699 min(max_processes, len(branches_to_fetch))
1700 if max_processes is not None
1701 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001702 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001703 yield x
1704 else:
1705 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1706 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001707 cl = Changelist(branchref=b, auth_config=auth_config)
1708 url = cl.GetIssueURL()
1709 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001710
rmistry@google.com2dd99862015-06-22 12:22:18 +00001711
1712def upload_branch_deps(cl, args):
1713 """Uploads CLs of local branches that are dependents of the current branch.
1714
1715 If the local branch dependency tree looks like:
1716 test1 -> test2.1 -> test3.1
1717 -> test3.2
1718 -> test2.2 -> test3.3
1719
1720 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1721 run on the dependent branches in this order:
1722 test2.1, test3.1, test3.2, test2.2, test3.3
1723
1724 Note: This function does not rebase your local dependent branches. Use it when
1725 you make a change to the parent branch that will not conflict with its
1726 dependent branches, and you would like their dependencies updated in
1727 Rietveld.
1728 """
1729 if git_common.is_dirty_git_tree('upload-branch-deps'):
1730 return 1
1731
1732 root_branch = cl.GetBranch()
1733 if root_branch is None:
1734 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1735 'Get on a branch!')
1736 if not cl.GetIssue() or not cl.GetPatchset():
1737 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1738 'patchset dependencies without an uploaded CL.')
1739
1740 branches = RunGit(['for-each-ref',
1741 '--format=%(refname:short) %(upstream:short)',
1742 'refs/heads'])
1743 if not branches:
1744 print('No local branches found.')
1745 return 0
1746
1747 # Create a dictionary of all local branches to the branches that are dependent
1748 # on it.
1749 tracked_to_dependents = collections.defaultdict(list)
1750 for b in branches.splitlines():
1751 tokens = b.split()
1752 if len(tokens) == 2:
1753 branch_name, tracked = tokens
1754 tracked_to_dependents[tracked].append(branch_name)
1755
1756 print
1757 print 'The dependent local branches of %s are:' % root_branch
1758 dependents = []
1759 def traverse_dependents_preorder(branch, padding=''):
1760 dependents_to_process = tracked_to_dependents.get(branch, [])
1761 padding += ' '
1762 for dependent in dependents_to_process:
1763 print '%s%s' % (padding, dependent)
1764 dependents.append(dependent)
1765 traverse_dependents_preorder(dependent, padding)
1766 traverse_dependents_preorder(root_branch)
1767 print
1768
1769 if not dependents:
1770 print 'There are no dependent local branches for %s' % root_branch
1771 return 0
1772
1773 print ('This command will checkout all dependent branches and run '
1774 '"git cl upload".')
1775 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1776
andybons@chromium.org962f9462016-02-03 20:00:42 +00001777 # Add a default patchset title to all upload calls in Rietveld.
1778 if not settings.GetIsGerrit():
1779 args.extend(['-t', 'Updated patchset dependency'])
1780
rmistry@google.com2dd99862015-06-22 12:22:18 +00001781 # Record all dependents that failed to upload.
1782 failures = {}
1783 # Go through all dependents, checkout the branch and upload.
1784 try:
1785 for dependent_branch in dependents:
1786 print
1787 print '--------------------------------------'
1788 print 'Running "git cl upload" from %s:' % dependent_branch
1789 RunGit(['checkout', '-q', dependent_branch])
1790 print
1791 try:
1792 if CMDupload(OptionParser(), args) != 0:
1793 print 'Upload failed for %s!' % dependent_branch
1794 failures[dependent_branch] = 1
1795 except: # pylint: disable=W0702
1796 failures[dependent_branch] = 1
1797 print
1798 finally:
1799 # Swap back to the original root branch.
1800 RunGit(['checkout', '-q', root_branch])
1801
1802 print
1803 print 'Upload complete for dependent branches!'
1804 for dependent_branch in dependents:
1805 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1806 print ' %s : %s' % (dependent_branch, upload_status)
1807 print
1808
1809 return 0
1810
1811
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001812def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001813 """Show status of changelists.
1814
1815 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001816 - Red not sent for review or broken
1817 - Blue waiting for review
1818 - Yellow waiting for you to reply to review
1819 - Green LGTM'ed
1820 - Magenta in the commit queue
1821 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001822
1823 Also see 'git cl comments'.
1824 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001825 parser.add_option('--field',
1826 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001827 parser.add_option('-f', '--fast', action='store_true',
1828 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001829 parser.add_option(
1830 '-j', '--maxjobs', action='store', type=int,
1831 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001832
1833 auth.add_auth_options(parser)
1834 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001835 if args:
1836 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001837 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001838
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001839 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001840 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001841 if options.field.startswith('desc'):
1842 print cl.GetDescription()
1843 elif options.field == 'id':
1844 issueid = cl.GetIssue()
1845 if issueid:
1846 print issueid
1847 elif options.field == 'patch':
1848 patchset = cl.GetPatchset()
1849 if patchset:
1850 print patchset
1851 elif options.field == 'url':
1852 url = cl.GetIssueURL()
1853 if url:
1854 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001855 return 0
1856
1857 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1858 if not branches:
1859 print('No local branch found.')
1860 return 0
1861
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001862 changes = (
1863 Changelist(branchref=b, auth_config=auth_config)
1864 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001865 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001866 alignment = max(5, max(len(b) for b in branches))
1867 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001868 output = get_cl_statuses(branches,
1869 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001870 max_processes=options.maxjobs,
1871 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001872
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001873 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001874 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001875 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001876 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001877 b, i, status = output.next()
1878 branch_statuses[b] = (i, status)
1879 issue_url, status = branch_statuses.pop(branch)
1880 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001881 reset = Fore.RESET
1882 if not sys.stdout.isatty():
1883 color = ''
1884 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001885 status_str = '(%s)' % status if status else ''
1886 print ' %*s : %s%s %s%s' % (
1887 alignment, ShortBranchName(branch), color, issue_url, status_str,
1888 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001889
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001890 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001891 print
1892 print 'Current branch:',
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001893 print cl.GetBranch()
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00001894 if not cl.GetIssue():
1895 print 'No issue assigned.'
1896 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001897 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001898 if not options.fast:
1899 print 'Issue description:'
1900 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001901 return 0
1902
1903
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001904def colorize_CMDstatus_doc():
1905 """To be called once in main() to add colors to git cl status help."""
1906 colors = [i for i in dir(Fore) if i[0].isupper()]
1907
1908 def colorize_line(line):
1909 for color in colors:
1910 if color in line.upper():
1911 # Extract whitespaces first and the leading '-'.
1912 indent = len(line) - len(line.lstrip(' ')) + 1
1913 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1914 return line
1915
1916 lines = CMDstatus.__doc__.splitlines()
1917 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1918
1919
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001920@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001921def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001922 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001923
1924 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001925 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001926 parser.add_option('-r', '--reverse', action='store_true',
1927 help='Lookup the branch(es) for the specified issues. If '
1928 'no issues are specified, all branches with mapped '
1929 'issues will be listed.')
1930 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001931
dnj@chromium.org406c4402015-03-03 17:22:28 +00001932 if options.reverse:
1933 branches = RunGit(['for-each-ref', 'refs/heads',
1934 '--format=%(refname:short)']).splitlines()
1935
1936 # Reverse issue lookup.
1937 issue_branch_map = {}
1938 for branch in branches:
1939 cl = Changelist(branchref=branch)
1940 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1941 if not args:
1942 args = sorted(issue_branch_map.iterkeys())
1943 for issue in args:
1944 if not issue:
1945 continue
1946 print 'Branch for issue number %s: %s' % (
1947 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1948 else:
1949 cl = Changelist()
1950 if len(args) > 0:
1951 try:
1952 issue = int(args[0])
1953 except ValueError:
1954 DieWithError('Pass a number to set the issue or none to list it.\n'
1955 'Maybe you want to run git cl status?')
1956 cl.SetIssue(issue)
1957 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001958 return 0
1959
1960
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001961def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001962 """Shows or posts review comments for any changelist."""
1963 parser.add_option('-a', '--add-comment', dest='comment',
1964 help='comment to add to an issue')
1965 parser.add_option('-i', dest='issue',
1966 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00001967 parser.add_option('-j', '--json-file',
1968 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001969 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001970 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001971 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001972
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001973 issue = None
1974 if options.issue:
1975 try:
1976 issue = int(options.issue)
1977 except ValueError:
1978 DieWithError('A review issue id is expected to be a number')
1979
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001980 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001981
1982 if options.comment:
1983 cl.AddComment(options.comment)
1984 return 0
1985
1986 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00001987 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001988 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00001989 summary.append({
1990 'date': message['date'],
1991 'lgtm': False,
1992 'message': message['text'],
1993 'not_lgtm': False,
1994 'sender': message['sender'],
1995 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001996 if message['disapproval']:
1997 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00001998 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001999 elif message['approval']:
2000 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00002001 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00002002 elif message['sender'] == data['owner_email']:
2003 color = Fore.MAGENTA
2004 else:
2005 color = Fore.BLUE
2006 print '\n%s%s %s%s' % (
2007 color, message['date'].split('.', 1)[0], message['sender'],
2008 Fore.RESET)
2009 if message['text'].strip():
2010 print '\n'.join(' ' + l for l in message['text'].splitlines())
smut@google.comc85ac942015-09-15 16:34:43 +00002011 if options.json_file:
2012 with open(options.json_file, 'wb') as f:
2013 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00002014 return 0
2015
2016
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002017def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002018 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00002019 parser.add_option('-d', '--display', action='store_true',
2020 help='Display the description instead of opening an editor')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002021 auth.add_auth_options(parser)
2022 options, _ = parser.parse_args(args)
2023 auth_config = auth.extract_auth_config_from_options(options)
2024 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002025 if not cl.GetIssue():
2026 DieWithError('This branch has no associated changelist.')
2027 description = ChangeDescription(cl.GetDescription())
smut@google.com34fb6b12015-07-13 20:03:26 +00002028 if options.display:
2029 print description.description
2030 return 0
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002031 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00002032 if cl.GetDescription() != description.description:
2033 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00002034 return 0
2035
2036
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002037def CreateDescriptionFromLog(args):
2038 """Pulls out the commit log to use as a base for the CL description."""
2039 log_args = []
2040 if len(args) == 1 and not args[0].endswith('.'):
2041 log_args = [args[0] + '..']
2042 elif len(args) == 1 and args[0].endswith('...'):
2043 log_args = [args[0][:-1]]
2044 elif len(args) == 2:
2045 log_args = [args[0] + '..' + args[1]]
2046 else:
2047 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00002048 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002049
2050
thestig@chromium.org44202a22014-03-11 19:22:18 +00002051def CMDlint(parser, args):
2052 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002053 parser.add_option('--filter', action='append', metavar='-x,+y',
2054 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002055 auth.add_auth_options(parser)
2056 options, args = parser.parse_args(args)
2057 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002058
2059 # Access to a protected member _XX of a client class
2060 # pylint: disable=W0212
2061 try:
2062 import cpplint
2063 import cpplint_chromium
2064 except ImportError:
2065 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2066 return 1
2067
2068 # Change the current working directory before calling lint so that it
2069 # shows the correct base.
2070 previous_cwd = os.getcwd()
2071 os.chdir(settings.GetRoot())
2072 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002073 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002074 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2075 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00002076 if not files:
2077 print "Cannot lint an empty CL"
2078 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00002079
2080 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00002081 command = args + files
2082 if options.filter:
2083 command = ['--filter=' + ','.join(options.filter)] + command
2084 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00002085
2086 white_regex = re.compile(settings.GetLintRegex())
2087 black_regex = re.compile(settings.GetLintIgnoreRegex())
2088 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2089 for filename in filenames:
2090 if white_regex.match(filename):
2091 if black_regex.match(filename):
2092 print "Ignoring file %s" % filename
2093 else:
2094 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2095 extra_check_functions)
2096 else:
2097 print "Skipping file %s" % filename
2098 finally:
2099 os.chdir(previous_cwd)
2100 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2101 if cpplint._cpplint_state.error_count != 0:
2102 return 1
2103 return 0
2104
2105
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002106def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002107 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002108 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002109 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00002110 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00002111 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002112 auth.add_auth_options(parser)
2113 options, args = parser.parse_args(args)
2114 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002115
sbc@chromium.org71437c02015-04-09 19:29:40 +00002116 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00002117 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002118 return 1
2119
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002120 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002121 if args:
2122 base_branch = args[0]
2123 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002124 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002125 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002126
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002127 cl.RunHook(
2128 committing=not options.upload,
2129 may_prompt=False,
2130 verbose=options.verbose,
2131 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00002132 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002133
2134
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00002135def AddChangeIdToCommitMessage(options, args):
2136 """Re-commits using the current message, assumes the commit hook is in
2137 place.
2138 """
2139 log_desc = options.message or CreateDescriptionFromLog(args)
2140 git_command = ['commit', '--amend', '-m', log_desc]
2141 RunGit(git_command)
2142 new_log_desc = CreateDescriptionFromLog(args)
2143 if CHANGE_ID in new_log_desc:
2144 print 'git-cl: Added Change-Id to commit message.'
2145 else:
2146 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2147
2148
piman@chromium.org336f9122014-09-04 02:16:55 +00002149def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002150 """upload the current branch to gerrit."""
2151 # We assume the remote called "origin" is the one we want.
2152 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002153 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00002154
2155 remote, remote_branch = cl.GetRemoteBranch()
2156 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2157 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002158
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002159 change_desc = ChangeDescription(
2160 options.message or CreateDescriptionFromLog(args))
2161 if not change_desc.description:
andybons@chromium.org962f9462016-02-03 20:00:42 +00002162 print "\nDescription is empty. Aborting..."
2163 return 1
2164
2165 if options.title:
2166 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002167 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002168
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002169 if options.squash:
2170 # Try to get the message from a previous upload.
2171 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
bauerb@chromium.org13502e02016-02-18 10:18:29 +00002172 message = RunGitSilent(['show', '--format=%B', '-s', shadow_branch])
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002173 if not message:
2174 if not options.force:
2175 change_desc.prompt()
2176
2177 if CHANGE_ID not in change_desc.description:
2178 # Run the commit-msg hook without modifying the head commit by writing
2179 # the commit message to a temporary file and running the hook over it,
2180 # then reading the file back in.
2181 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
2182 'commit-msg')
2183 file_handle, msg_file = tempfile.mkstemp(text=True,
2184 prefix='commit_msg')
tandrii@chromium.orga83663a2016-01-14 16:01:00 +00002185 logging.debug("%s %s", file_handle, msg_file)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002186 try:
2187 try:
tandrii@chromium.orga83663a2016-01-14 16:01:00 +00002188 try:
2189 fileobj = os.fdopen(file_handle, 'w')
2190 except OSError:
2191 # if fdopen fails, file_handle remains open.
2192 # See https://docs.python.org/2/library/os.html#os.fdopen.
2193 os.close(file_handle)
2194 raise
2195 with fileobj:
2196 # This will close the file_handle.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002197 fileobj.write(change_desc.description)
tandrii@chromium.orga83663a2016-01-14 16:01:00 +00002198 logging.debug("%s %s finish editing", file_handle, msg_file)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002199 finally:
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002200 RunCommand([commit_msg_hook, msg_file])
2201 change_desc.set_description(gclient_utils.FileRead(msg_file))
2202 finally:
2203 os.remove(msg_file)
2204
2205 if not change_desc.description:
2206 print "Description is empty; aborting."
2207 return 1
2208
2209 message = change_desc.description
2210
2211 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2212 if remote is '.':
2213 # If our upstream branch is local, we base our squashed commit on its
2214 # squashed version.
2215 parent = ('refs/heads/git_cl_uploads/' +
2216 scm.GIT.ShortBranchName(upstream_branch))
2217
2218 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2219 # will create additional CLs when uploading.
2220 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2221 RunGitSilent(['rev-parse', parent + ':'])):
2222 print 'Upload upstream branch ' + upstream_branch + ' first.'
2223 return 1
2224 else:
2225 parent = cl.GetCommonAncestorWithUpstream()
2226
2227 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2228 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2229 '-m', message]).strip()
2230 else:
2231 if CHANGE_ID not in change_desc.description:
2232 AddChangeIdToCommitMessage(options, args)
2233 ref_to_push = 'HEAD'
2234 parent = '%s/%s' % (gerrit_remote, branch)
2235
2236 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2237 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002238 if len(commits) > 1:
2239 print('WARNING: This will upload %d commits. Run the following command '
2240 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002241 print('git log %s..%s' % (parent, ref_to_push))
2242 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002243 'commit.')
2244 ask_for_data('About to upload; enter to confirm.')
2245
piman@chromium.org336f9122014-09-04 02:16:55 +00002246 if options.reviewers or options.tbr_owners:
2247 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002248
ukai@chromium.orge8077812012-02-03 03:41:46 +00002249 receive_options = []
2250 cc = cl.GetCCList().split(',')
2251 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002252 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002253 cc = filter(None, cc)
2254 if cc:
2255 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002256 if change_desc.get_reviewers():
2257 receive_options.extend(
2258 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002259
ukai@chromium.orge8077812012-02-03 03:41:46 +00002260 git_command = ['push']
2261 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002262 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002263 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002264 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002265 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002266
2267 if options.squash:
2268 head = RunGit(['rev-parse', 'HEAD']).strip()
2269 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2270
ukai@chromium.orge8077812012-02-03 03:41:46 +00002271 # TODO(ukai): parse Change-Id: and set issue number?
2272 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002273
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002274
wittman@chromium.org455dc922015-01-26 20:15:50 +00002275def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2276 """Computes the remote branch ref to use for the CL.
2277
2278 Args:
2279 remote (str): The git remote for the CL.
2280 remote_branch (str): The git remote branch for the CL.
2281 target_branch (str): The target branch specified by the user.
2282 pending_prefix (str): The pending prefix from the settings.
2283 """
2284 if not (remote and remote_branch):
2285 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002286
wittman@chromium.org455dc922015-01-26 20:15:50 +00002287 if target_branch:
2288 # Cannonicalize branch references to the equivalent local full symbolic
2289 # refs, which are then translated into the remote full symbolic refs
2290 # below.
2291 if '/' not in target_branch:
2292 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2293 else:
2294 prefix_replacements = (
2295 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2296 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2297 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2298 )
2299 match = None
2300 for regex, replacement in prefix_replacements:
2301 match = re.search(regex, target_branch)
2302 if match:
2303 remote_branch = target_branch.replace(match.group(0), replacement)
2304 break
2305 if not match:
2306 # This is a branch path but not one we recognize; use as-is.
2307 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002308 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2309 # Handle the refs that need to land in different refs.
2310 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002311
wittman@chromium.org455dc922015-01-26 20:15:50 +00002312 # Create the true path to the remote branch.
2313 # Does the following translation:
2314 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2315 # * refs/remotes/origin/master -> refs/heads/master
2316 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2317 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2318 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2319 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2320 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2321 'refs/heads/')
2322 elif remote_branch.startswith('refs/remotes/branch-heads'):
2323 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2324 # If a pending prefix exists then replace refs/ with it.
2325 if pending_prefix:
2326 remote_branch = remote_branch.replace('refs/', pending_prefix)
2327 return remote_branch
2328
2329
piman@chromium.org336f9122014-09-04 02:16:55 +00002330def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002331 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002332 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2333 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002334 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002335 if options.emulate_svn_auto_props:
2336 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002337
2338 change_desc = None
2339
pgervais@chromium.org91141372014-01-09 23:27:20 +00002340 if options.email is not None:
2341 upload_args.extend(['--email', options.email])
2342
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002343 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002344 if options.title:
2345 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002346 if options.message:
2347 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002348 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002349 print ("This branch is associated with issue %s. "
2350 "Adding patch to that issue." % cl.GetIssue())
2351 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002352 if options.title:
2353 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002354 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002355 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002356 if options.reviewers or options.tbr_owners:
2357 change_desc.update_reviewers(options.reviewers,
2358 options.tbr_owners,
2359 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002360 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002361 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002362
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002363 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002364 print "Description is empty; aborting."
2365 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002366
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002367 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002368 if change_desc.get_reviewers():
2369 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002370 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002371 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002372 DieWithError("Must specify reviewers to send email.")
2373 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002374
2375 # We check this before applying rietveld.private assuming that in
2376 # rietveld.cc only addresses which we can send private CLs to are listed
2377 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2378 # --private is specified explicitly on the command line.
2379 if options.private:
2380 logging.warn('rietveld.cc is ignored since private flag is specified. '
2381 'You need to review and add them manually if necessary.')
2382 cc = cl.GetCCListWithoutDefault()
2383 else:
2384 cc = cl.GetCCList()
2385 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002386 if cc:
2387 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002388
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002389 if options.private or settings.GetDefaultPrivateFlag() == "True":
2390 upload_args.append('--private')
2391
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002392 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002393 if not options.find_copies:
2394 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002395
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002396 # Include the upstream repo's URL in the change -- this is useful for
2397 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002398 remote_url = cl.GetGitBaseUrlFromConfig()
2399 if not remote_url:
2400 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002401 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002402 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002403 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2404 remote_url = (cl.GetRemoteUrl() + '@'
2405 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002406 if remote_url:
2407 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002408 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002409 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2410 settings.GetPendingRefPrefix())
2411 if target_ref:
2412 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002413
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002414 # Look for dependent patchsets. See crbug.com/480453 for more details.
2415 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2416 upstream_branch = ShortBranchName(upstream_branch)
2417 if remote is '.':
2418 # A local branch is being tracked.
2419 local_branch = ShortBranchName(upstream_branch)
rmistry@google.com78948ed2015-07-08 23:09:57 +00002420 if settings.GetIsSkipDependencyUpload(local_branch):
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002421 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002422 print ('Skipping dependency patchset upload because git config '
2423 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002424 print
rmistry@google.com78948ed2015-07-08 23:09:57 +00002425 else:
2426 auth_config = auth.extract_auth_config_from_options(options)
2427 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2428 branch_cl_issue_url = branch_cl.GetIssueURL()
2429 branch_cl_issue = branch_cl.GetIssue()
2430 branch_cl_patchset = branch_cl.GetPatchset()
2431 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2432 upload_args.extend(
2433 ['--depends_on_patchset', '%s:%s' % (
2434 branch_cl_issue, branch_cl_patchset)])
2435 print
2436 print ('The current branch (%s) is tracking a local branch (%s) with '
2437 'an associated CL.') % (cl.GetBranch(), local_branch)
2438 print 'Adding %s/#ps%s as a dependency patchset.' % (
2439 branch_cl_issue_url, branch_cl_patchset)
2440 print
rmistry@google.comd91b7e32015-06-23 11:24:07 +00002441
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002442 project = settings.GetProject()
2443 if project:
2444 upload_args.extend(['--project', project])
2445
rmistry@google.comef966222015-04-07 11:15:01 +00002446 if options.cq_dry_run:
2447 upload_args.extend(['--cq_dry_run'])
2448
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002449 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002450 upload_args = ['upload'] + upload_args + args
2451 logging.info('upload.RealMain(%s)', upload_args)
2452 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002453 issue = int(issue)
2454 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002455 except KeyboardInterrupt:
2456 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002457 except:
2458 # If we got an exception after the user typed a description for their
2459 # change, back up the description before re-raising.
2460 if change_desc:
2461 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2462 print '\nGot exception while uploading -- saving description to %s\n' \
2463 % backup_path
2464 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002465 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002466 backup_file.close()
2467 raise
2468
2469 if not cl.GetIssue():
2470 cl.SetIssue(issue)
2471 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002472
2473 if options.use_commit_queue:
2474 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002475 return 0
2476
2477
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002478def cleanup_list(l):
2479 """Fixes a list so that comma separated items are put as individual items.
2480
2481 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2482 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2483 """
2484 items = sum((i.split(',') for i in l), [])
2485 stripped_items = (i.strip() for i in items)
2486 return sorted(filter(None, stripped_items))
2487
2488
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002489@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002490def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00002491 """Uploads the current changelist to codereview.
2492
2493 Can skip dependency patchset uploads for a branch by running:
2494 git config branch.branch_name.skip-deps-uploads True
2495 To unset run:
2496 git config --unset branch.branch_name.skip-deps-uploads
2497 Can also set the above globally by using the --global flag.
2498 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00002499 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2500 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002501 parser.add_option('--bypass-watchlists', action='store_true',
2502 dest='bypass_watchlists',
2503 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002504 parser.add_option('-f', action='store_true', dest='force',
2505 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002506 parser.add_option('-m', dest='message', help='message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00002507 parser.add_option('-t', dest='title',
2508 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002509 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002510 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002511 help='reviewer email addresses')
2512 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002513 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002514 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002515 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002516 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002517 parser.add_option('--emulate_svn_auto_props',
2518 '--emulate-svn-auto-props',
2519 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002520 dest="emulate_svn_auto_props",
2521 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002522 parser.add_option('-c', '--use-commit-queue', action='store_true',
2523 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002524 parser.add_option('--private', action='store_true',
2525 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002526 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002527 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002528 metavar='TARGET',
2529 help='Apply CL to remote ref TARGET. ' +
2530 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002531 parser.add_option('--squash', action='store_true',
2532 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002533 parser.add_option('--no-squash', action='store_true',
2534 help='Don\'t squash multiple commits into one ' +
2535 '(Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002536 parser.add_option('--email', default=None,
2537 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002538 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2539 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00002540 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2541 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00002542 help='Send the patchset to do a CQ dry run right after '
2543 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002544 parser.add_option('--dependencies', action='store_true',
2545 help='Uploads CLs of all the local branches that depend on '
2546 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002547
rmistry@google.com2dd99862015-06-22 12:22:18 +00002548 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002549 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002550 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002551 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002552 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002553
sbc@chromium.org71437c02015-04-09 19:29:40 +00002554 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002555 return 1
2556
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002557 options.reviewers = cleanup_list(options.reviewers)
2558 options.cc = cleanup_list(options.cc)
2559
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002560 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002561 if args:
2562 # TODO(ukai): is it ok for gerrit case?
2563 base_branch = args[0]
2564 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002565 if cl.GetBranch() is None:
2566 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2567
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002568 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002569 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002570 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002571
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002572 # Make sure authenticated to Rietveld before running expensive hooks. It is
2573 # a fast, best efforts check. Rietveld still can reject the authentication
2574 # during the actual upload.
2575 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2576 authenticator = auth.get_authenticator_for_host(
2577 cl.GetRietveldServer(), auth_config)
2578 if not authenticator.has_cached_credentials():
2579 raise auth.LoginRequiredError(cl.GetRietveldServer())
2580
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002581 # Apply watchlists on upload.
2582 change = cl.GetChange(base_branch, None)
2583 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2584 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002585 if not options.bypass_watchlists:
2586 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002587
ukai@chromium.orge8077812012-02-03 03:41:46 +00002588 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002589 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002590 # Set the reviewer list now so that presubmit checks can access it.
2591 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002592 change_description.update_reviewers(options.reviewers,
2593 options.tbr_owners,
2594 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002595 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002596 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002597 may_prompt=not options.force,
2598 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002599 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002600 if not hook_results.should_continue():
2601 return 1
2602 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002603 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002604
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002605 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002606 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002607 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002608 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002609 print ('The last upload made from this repository was patchset #%d but '
2610 'the most recent patchset on the server is #%d.'
2611 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002612 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2613 'from another machine or branch the patch you\'re uploading now '
2614 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002615 ask_for_data('About to upload; enter to confirm.')
2616
iannucci@chromium.org79540052012-10-19 23:15:26 +00002617 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002618 if settings.GetIsGerrit():
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00002619 if options.squash and options.no_squash:
2620 DieWithError('Can only use one of --squash or --no-squash')
2621
2622 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2623 not options.no_squash)
2624
tandrii@chromium.org1e67bb72016-02-11 12:15:49 +00002625 ret = GerritUpload(options, args, cl, change)
2626 else:
2627 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002628 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002629 git_set_branch_value('last-upload-hash',
2630 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002631 # Run post upload hooks, if specified.
2632 if settings.GetRunPostUploadHook():
2633 presubmit_support.DoPostUploadExecuter(
2634 change,
2635 cl,
2636 settings.GetRoot(),
2637 options.verbose,
2638 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002639
rmistry@google.com2dd99862015-06-22 12:22:18 +00002640 # Upload all dependencies if specified.
2641 if options.dependencies:
2642 print
2643 print '--dependencies has been specified.'
2644 print 'All dependent local branches will be re-uploaded.'
2645 print
2646 # Remove the dependencies flag from args so that we do not end up in a
2647 # loop.
2648 orig_args.remove('--dependencies')
2649 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002650 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002651
2652
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002653def IsSubmoduleMergeCommit(ref):
2654 # When submodules are added to the repo, we expect there to be a single
2655 # non-git-svn merge commit at remote HEAD with a signature comment.
2656 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002657 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002658 return RunGit(cmd) != ''
2659
2660
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002661def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002662 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002663
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002664 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002665 Updates changelog with metadata (e.g. pointer to review).
2666 Pushes/dcommits the code upstream.
2667 Updates review and closes.
2668 """
2669 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2670 help='bypass upload presubmit hook')
2671 parser.add_option('-m', dest='message',
2672 help="override review description")
2673 parser.add_option('-f', action='store_true', dest='force',
2674 help="force yes to questions (don't prompt)")
2675 parser.add_option('-c', dest='contributor',
2676 help="external contributor for patch (appended to " +
2677 "description and used as author for git). Should be " +
2678 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002679 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002680 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002681 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002682 auth_config = auth.extract_auth_config_from_options(options)
2683
2684 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002685
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002686 current = cl.GetBranch()
2687 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2688 if not settings.GetIsGitSvn() and remote == '.':
2689 print
2690 print 'Attempting to push branch %r into another local branch!' % current
2691 print
2692 print 'Either reparent this branch on top of origin/master:'
2693 print ' git reparent-branch --root'
2694 print
2695 print 'OR run `git rebase-update` if you think the parent branch is already'
2696 print 'committed.'
2697 print
2698 print ' Current parent: %r' % upstream_branch
2699 return 1
2700
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002701 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002702 # Default to merging against our best guess of the upstream branch.
2703 args = [cl.GetUpstreamBranch()]
2704
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002705 if options.contributor:
2706 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2707 print "Please provide contibutor as 'First Last <email@example.com>'"
2708 return 1
2709
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002710 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002711 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002712
sbc@chromium.org71437c02015-04-09 19:29:40 +00002713 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002714 return 1
2715
2716 # This rev-list syntax means "show all commits not in my branch that
2717 # are in base_branch".
2718 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2719 base_branch]).splitlines()
2720 if upstream_commits:
2721 print ('Base branch "%s" has %d commits '
2722 'not in this branch.' % (base_branch, len(upstream_commits)))
2723 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2724 return 1
2725
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002726 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002727 svn_head = None
2728 if cmd == 'dcommit' or base_has_submodules:
2729 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2730 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002731
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002732 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002733 # If the base_head is a submodule merge commit, the first parent of the
2734 # base_head should be a git-svn commit, which is what we're interested in.
2735 base_svn_head = base_branch
2736 if base_has_submodules:
2737 base_svn_head += '^1'
2738
2739 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002740 if extra_commits:
2741 print ('This branch has %d additional commits not upstreamed yet.'
2742 % len(extra_commits.splitlines()))
2743 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2744 'before attempting to %s.' % (base_branch, cmd))
2745 return 1
2746
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002747 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002748 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002749 author = None
2750 if options.contributor:
2751 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002752 hook_results = cl.RunHook(
2753 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002754 may_prompt=not options.force,
2755 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002756 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002757 if not hook_results.should_continue():
2758 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002759
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002760 # Check the tree status if the tree status URL is set.
2761 status = GetTreeStatus()
2762 if 'closed' == status:
2763 print('The tree is closed. Please wait for it to reopen. Use '
2764 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2765 return 1
2766 elif 'unknown' == status:
2767 print('Unable to determine tree status. Please verify manually and '
2768 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2769 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002770
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002771 change_desc = ChangeDescription(options.message)
2772 if not change_desc.description and cl.GetIssue():
2773 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002774
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002775 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002776 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002777 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002778 else:
2779 print 'No description set.'
2780 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2781 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002782
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002783 # Keep a separate copy for the commit message, because the commit message
2784 # contains the link to the Rietveld issue, while the Rietveld message contains
2785 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002786 # Keep a separate copy for the commit message.
2787 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002788 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002789
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002790 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002791 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002792 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00002793 # after it. Add a period on a new line to circumvent this. Also add a space
2794 # before the period to make sure that Gitiles continues to correctly resolve
2795 # the URL.
2796 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002797 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002798 commit_desc.append_footer('Patch from %s.' % options.contributor)
2799
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002800 print('Description:')
2801 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002802
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002803 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002804 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002805 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002806
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002807 # We want to squash all this branch's commits into one commit with the proper
2808 # description. We do this by doing a "reset --soft" to the base branch (which
2809 # keeps the working copy the same), then dcommitting that. If origin/master
2810 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2811 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002812 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002813 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2814 # Delete the branches if they exist.
2815 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2816 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2817 result = RunGitWithCode(showref_cmd)
2818 if result[0] == 0:
2819 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002820
2821 # We might be in a directory that's present in this branch but not in the
2822 # trunk. Move up to the top of the tree so that git commands that expect a
2823 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002824 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002825 if rel_base_path:
2826 os.chdir(rel_base_path)
2827
2828 # Stuff our change into the merge branch.
2829 # We wrap in a try...finally block so if anything goes wrong,
2830 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002831 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002832 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002833 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002834 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002835 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002836 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002837 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002838 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002839 RunGit(
2840 [
2841 'commit', '--author', options.contributor,
2842 '-m', commit_desc.description,
2843 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002844 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002845 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002846 if base_has_submodules:
2847 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2848 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2849 RunGit(['checkout', CHERRY_PICK_BRANCH])
2850 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002851 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002852 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002853 pending_prefix = settings.GetPendingRefPrefix()
2854 if not pending_prefix or branch.startswith(pending_prefix):
2855 # If not using refs/pending/heads/* at all, or target ref is already set
2856 # to pending, then push to the target ref directly.
2857 retcode, output = RunGitWithCode(
2858 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002859 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002860 else:
2861 # Cherry-pick the change on top of pending ref and then push it.
2862 assert branch.startswith('refs/'), branch
2863 assert pending_prefix[-1] == '/', pending_prefix
2864 pending_ref = pending_prefix + branch[len('refs/'):]
2865 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002866 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002867 if retcode == 0:
2868 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002869 else:
2870 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002871 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002872 'svn', 'dcommit',
2873 '-C%s' % options.similarity,
2874 '--no-rebase', '--rmdir',
2875 ]
2876 if settings.GetForceHttpsCommitUrl():
2877 # Allow forcing https commit URLs for some projects that don't allow
2878 # committing to http URLs (like Google Code).
2879 remote_url = cl.GetGitSvnRemoteUrl()
2880 if urlparse.urlparse(remote_url).scheme == 'http':
2881 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002882 cmd_args.append('--commit-url=%s' % remote_url)
2883 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002884 if 'Committed r' in output:
2885 revision = re.match(
2886 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2887 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002888 finally:
2889 # And then swap back to the original branch and clean up.
2890 RunGit(['checkout', '-q', cl.GetBranch()])
2891 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002892 if base_has_submodules:
2893 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002894
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002895 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002896 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002897 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002898
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002899 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002900 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002901 try:
2902 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2903 # We set pushed_to_pending to False, since it made it all the way to the
2904 # real ref.
2905 pushed_to_pending = False
2906 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002907 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002908
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002909 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002910 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002911 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002912 if not to_pending:
2913 if viewvc_url and revision:
2914 change_desc.append_footer(
2915 'Committed: %s%s' % (viewvc_url, revision))
2916 elif revision:
2917 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002918 print ('Closing issue '
2919 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002920 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002921 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002922 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002923 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002924 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002925 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002926 if options.bypass_hooks:
2927 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2928 else:
2929 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002930 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002931 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002932
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002933 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002934 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2935 print 'The commit is in the pending queue (%s).' % pending_ref
2936 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002937 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002938 'footer.' % branch)
2939
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002940 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2941 if os.path.isfile(hook):
2942 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002943
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002944 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002945
2946
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002947def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2948 print
2949 print 'Waiting for commit to be landed on %s...' % real_ref
2950 print '(If you are impatient, you may Ctrl-C once without harm)'
2951 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2952 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2953
2954 loop = 0
2955 while True:
2956 sys.stdout.write('fetching (%d)... \r' % loop)
2957 sys.stdout.flush()
2958 loop += 1
2959
2960 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2961 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2962 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2963 for commit in commits.splitlines():
2964 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2965 print 'Found commit on %s' % real_ref
2966 return commit
2967
2968 current_rev = to_rev
2969
2970
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002971def PushToGitPending(remote, pending_ref, upstream_ref):
2972 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2973
2974 Returns:
2975 (retcode of last operation, output log of last operation).
2976 """
2977 assert pending_ref.startswith('refs/'), pending_ref
2978 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2979 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2980 code = 0
2981 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002982 max_attempts = 3
2983 attempts_left = max_attempts
2984 while attempts_left:
2985 if attempts_left != max_attempts:
2986 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2987 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002988
2989 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002990 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002991 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002992 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002993 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002994 print 'Fetch failed with exit code %d.' % code
2995 if out.strip():
2996 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002997 continue
2998
2999 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003000 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003001 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003002 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003003 if code:
3004 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003005 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3006 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003007 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3008 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003009 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003010 return code, out
3011
3012 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003013 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003014 code, out = RunGitWithCode(
3015 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3016 if code == 0:
3017 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00003018 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003019 return code, out
3020
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003021 print 'Push failed with exit code %d.' % code
3022 if out.strip():
3023 print out.strip()
3024 if IsFatalPushFailure(out):
3025 print (
3026 'Fatal push error. Make sure your .netrc credentials and git '
3027 'user.email are correct and you have push access to the repo.')
3028 return code, out
3029
3030 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003031 return code, out
3032
3033
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00003034def IsFatalPushFailure(push_stdout):
3035 """True if retrying push won't help."""
3036 return '(prohibited by Gerrit)' in push_stdout
3037
3038
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003039@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003040def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003041 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003042 if not settings.GetIsGitSvn():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003043 if get_footer_svn_id():
3044 # If it looks like previous commits were mirrored with git-svn.
3045 message = """This repository appears to be a git-svn mirror, but no
3046upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3047 else:
3048 message = """This doesn't appear to be an SVN repository.
3049If your project has a true, writeable git repository, you probably want to run
3050'git cl land' instead.
3051If your project has a git mirror of an upstream SVN master, you probably need
3052to run 'git svn init'.
3053
3054Using the wrong command might cause your commit to appear to succeed, and the
3055review to be closed, without actually landing upstream. If you choose to
3056proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00003057 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00003058 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003059 return SendUpstream(parser, args, 'dcommit')
3060
3061
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003062@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00003063def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003064 """Commits the current changelist via git."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003065 if settings.GetIsGitSvn() or get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003066 print('This appears to be an SVN repository.')
3067 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00003068 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00003069 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003070 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003071
3072
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003073def ParseIssueNum(arg):
3074 """Parses the issue number from args if present otherwise returns None."""
3075 if re.match(r'\d+', arg):
3076 return arg
3077 if arg.startswith('http'):
3078 return re.sub(r'.*/(\d+)/?', r'\1', arg)
3079 return None
3080
3081
3082@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003083def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00003084 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003085 parser.add_option('-b', dest='newbranch',
3086 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003087 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003088 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003089 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3090 help='Change to the directory DIR immediately, '
3091 'before doing anything else.')
3092 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003093 help='failed patches spew .rej files rather than '
3094 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003095 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
3096 help="don't commit after patch applies")
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003097
3098 group = optparse.OptionGroup(parser,
3099 """Options for continuing work on the current issue uploaded
3100from a different clone (e.g. different machine). Must be used independently from
3101the other options. No issue number should be specified, and the branch must have
3102an issue number associated with it""")
3103 group.add_option('--reapply', action='store_true',
3104 dest='reapply',
3105 help="""Reset the branch and reapply the issue.
3106CAUTION: This will undo any local changes in this branch""")
3107
3108 group.add_option('--pull', action='store_true', dest='pull',
3109 help="Performs a pull before reapplying.")
3110 parser.add_option_group(group)
3111
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003112 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003113 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003114 auth_config = auth.extract_auth_config_from_options(options)
3115
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003116 issue_arg = None
3117 if options.reapply :
3118 if len(args) > 0:
3119 parser.error("--reapply implies no additional arguments.")
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003120
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003121 cl = Changelist()
3122 issue_arg = cl.GetIssue()
3123 upstream = cl.GetUpstreamBranch()
3124 if upstream == None:
3125 parser.error("No upstream branch specified. Cannot reset branch")
3126
3127 RunGit(['reset', '--hard', upstream])
3128 if options.pull:
3129 RunGit(['pull'])
3130 else:
3131 if len(args) != 1:
3132 parser.error("Must specify issue number")
3133
3134 issue_arg = ParseIssueNum(args[0])
3135
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003136 # The patch URL works because ParseIssueNum won't do any substitution
3137 # as the re.sub pattern fails to match and just returns it.
3138 if issue_arg == None:
3139 parser.print_help()
3140 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003141
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003142 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003143 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003144 return 1
3145
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003146 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00003147 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003148
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003149 if options.newbranch:
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00003150 if options.reapply:
3151 parser.error("--reapply excludes any option other than --pull")
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003152 if options.force:
3153 RunGit(['branch', '-D', options.newbranch],
3154 stderr=subprocess2.PIPE, error_ok=True)
3155 RunGit(['checkout', '-b', options.newbranch,
3156 Changelist().GetUpstreamBranch()])
3157
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003158 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003159 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003160
3161
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003162def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00003163 # PatchIssue should never be called with a dirty tree. It is up to the
3164 # caller to check this, but just in case we assert here since the
3165 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003166 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003167
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003168 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003169 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00003170 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003171 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003172 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00003173 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003174 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00003175 # Assume it's a URL to the patch. Default to https.
3176 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00003177 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003178 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003179 DieWithError('Must pass an issue ID or full URL for '
3180 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00003181 issue = int(match.group(2))
3182 cl = Changelist(issue=issue, auth_config=auth_config)
3183 cl.rietveld_server = match.group(1)
3184 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00003185 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003186
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003187 # Switch up to the top-level directory, if necessary, in preparation for
3188 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003189 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003190 if top:
3191 os.chdir(top)
3192
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003193 # Git patches have a/ at the beginning of source paths. We strip that out
3194 # with a sed script rather than the -p flag to patch so we can feed either
3195 # Git or svn-style patches into the same apply command.
3196 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003197 try:
3198 patch_data = subprocess2.check_output(
3199 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3200 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003201 DieWithError('Git patch mungling failed.')
3202 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003203
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003204 # We use "git apply" to apply the patch instead of "patch" so that we can
3205 # pick up file adds.
3206 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003207 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00003208 if directory:
3209 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003210 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003211 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00003212 elif IsGitVersionAtLeast('1.7.12'):
3213 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003214 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003215 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003216 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00003217 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003218 print 'Failed to apply the patch'
3219 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003220
3221 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003222 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00003223 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3224 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00003225 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3226 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003227 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003228 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00003229 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00003230 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003231 else:
3232 print "Patch applied to index."
3233 return 0
3234
3235
3236def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003237 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003238 # Provide a wrapper for git svn rebase to help avoid accidental
3239 # git svn dcommit.
3240 # It's the only command that doesn't use parser at all since we just defer
3241 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00003242
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003243 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003244
3245
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003246def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003247 """Fetches the tree status and returns either 'open', 'closed',
3248 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00003249 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003250 if url:
3251 status = urllib2.urlopen(url).read().lower()
3252 if status.find('closed') != -1 or status == '0':
3253 return 'closed'
3254 elif status.find('open') != -1 or status == '1':
3255 return 'open'
3256 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003257 return 'unset'
3258
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003259
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003260def GetTreeStatusReason():
3261 """Fetches the tree status from a json url and returns the message
3262 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00003263 url = settings.GetTreeStatusUrl()
3264 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003265 connection = urllib2.urlopen(json_url)
3266 status = json.loads(connection.read())
3267 connection.close()
3268 return status['message']
3269
dpranke@chromium.org970c5222011-03-12 00:32:24 +00003270
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003271def GetBuilderMaster(bot_list):
3272 """For a given builder, fetch the master from AE if available."""
3273 map_url = 'https://builders-map.appspot.com/'
3274 try:
3275 master_map = json.load(urllib2.urlopen(map_url))
3276 except urllib2.URLError as e:
3277 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3278 (map_url, e))
3279 except ValueError as e:
3280 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3281 if not master_map:
3282 return None, 'Failed to build master map.'
3283
3284 result_master = ''
3285 for bot in bot_list:
3286 builder = bot.split(':', 1)[0]
3287 master_list = master_map.get(builder, [])
3288 if not master_list:
3289 return None, ('No matching master for builder %s.' % builder)
3290 elif len(master_list) > 1:
3291 return None, ('The builder name %s exists in multiple masters %s.' %
3292 (builder, master_list))
3293 else:
3294 cur_master = master_list[0]
3295 if not result_master:
3296 result_master = cur_master
3297 elif result_master != cur_master:
3298 return None, 'The builders do not belong to the same master.'
3299 return result_master, None
3300
3301
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003302def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003303 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003304 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003305 status = GetTreeStatus()
3306 if 'unset' == status:
3307 print 'You must configure your tree status URL by running "git cl config".'
3308 return 2
3309
3310 print "The tree is %s" % status
3311 print
3312 print GetTreeStatusReason()
3313 if status != 'open':
3314 return 1
3315 return 0
3316
3317
maruel@chromium.org15192402012-09-06 12:38:29 +00003318def CMDtry(parser, args):
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003319 """Triggers a try job through BuildBucket."""
maruel@chromium.org15192402012-09-06 12:38:29 +00003320 group = optparse.OptionGroup(parser, "Try job options")
3321 group.add_option(
3322 "-b", "--bot", action="append",
3323 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3324 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003325 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003326 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003327 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003328 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003329 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003330 help=("Specify a try master where to run the tries."))
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003331 group.add_option( "--luci", action='store_true')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003332 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003333 "-r", "--revision",
3334 help="Revision to use for the try job; default: the "
3335 "revision will be determined by the try server; see "
3336 "its waterfall for more info")
3337 group.add_option(
3338 "-c", "--clobber", action="store_true", default=False,
3339 help="Force a clobber before building; e.g. don't do an "
3340 "incremental build")
3341 group.add_option(
3342 "--project",
3343 help="Override which project to use. Projects are defined "
3344 "server-side to define what default bot set to use")
3345 group.add_option(
machenbach@chromium.org45453142015-09-15 08:45:22 +00003346 "-p", "--property", dest="properties", action="append", default=[],
3347 help="Specify generic properties in the form -p key1=value1 -p "
3348 "key2=value2 etc (buildbucket only). The value will be treated as "
3349 "json if decodable, or as string otherwise.")
3350 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003351 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003352 group.add_option(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00003353 "--use-rietveld", action="store_true", default=False,
3354 help="Use Rietveld to trigger try jobs.")
3355 group.add_option(
3356 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3357 help="Host of buildbucket. The default host is %default.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003358 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003359 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003360 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003361 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003362
machenbach@chromium.org45453142015-09-15 08:45:22 +00003363 if options.use_rietveld and options.properties:
3364 parser.error('Properties can only be specified with buildbucket')
3365
3366 # Make sure that all properties are prop=value pairs.
3367 bad_params = [x for x in options.properties if '=' not in x]
3368 if bad_params:
3369 parser.error('Got properties with missing "=": %s' % bad_params)
3370
maruel@chromium.org15192402012-09-06 12:38:29 +00003371 if args:
3372 parser.error('Unknown arguments: %s' % args)
3373
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003374 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003375 if not cl.GetIssue():
3376 parser.error('Need to upload first')
3377
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003378 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003379 if props.get('closed'):
3380 parser.error('Cannot send tryjobs for a closed CL')
3381
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003382 if props.get('private'):
3383 parser.error('Cannot use trybots with private issue')
3384
maruel@chromium.org15192402012-09-06 12:38:29 +00003385 if not options.name:
3386 options.name = cl.GetBranch()
3387
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003388 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003389 options.master, err_msg = GetBuilderMaster(options.bot)
3390 if err_msg:
3391 parser.error('Tryserver master cannot be found because: %s\n'
3392 'Please manually specify the tryserver master'
3393 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003394
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003395 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003396 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003397 if not options.bot:
3398 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003399
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003400 # Get try masters from PRESUBMIT.py files.
3401 masters = presubmit_support.DoGetTryMasters(
3402 change,
3403 change.LocalPaths(),
3404 settings.GetRoot(),
3405 None,
3406 None,
3407 options.verbose,
3408 sys.stdout)
3409 if masters:
3410 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003411
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003412 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3413 options.bot = presubmit_support.DoGetTrySlaves(
3414 change,
3415 change.LocalPaths(),
3416 settings.GetRoot(),
3417 None,
3418 None,
3419 options.verbose,
3420 sys.stdout)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003421
3422 if not options.bot:
3423 # Get try masters from cq.cfg if any.
3424 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3425 # location.
3426 cq_cfg = os.path.join(change.RepositoryRoot(),
3427 'infra', 'config', 'cq.cfg')
3428 if os.path.exists(cq_cfg):
3429 masters = {}
machenbach@chromium.org59994802016-01-14 10:10:33 +00003430 cq_masters = commit_queue.get_master_builder_map(
3431 cq_cfg, include_experimental=False, include_triggered=False)
tandrii@chromium.org71184c02016-01-13 15:18:44 +00003432 for master, builders in cq_masters.iteritems():
3433 for builder in builders:
3434 # Skip presubmit builders, because these will fail without LGTM.
3435 if 'presubmit' not in builder.lower():
3436 masters.setdefault(master, {})[builder] = ['defaulttests']
3437 if masters:
3438 return masters
3439
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003440 if not options.bot:
3441 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003442
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003443 builders_and_tests = {}
3444 # TODO(machenbach): The old style command-line options don't support
3445 # multiple try masters yet.
3446 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3447 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3448
3449 for bot in old_style:
3450 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003451 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003452 elif ',' in bot:
3453 parser.error('Specify one bot per --bot flag')
3454 else:
tandrii@chromium.org3764fa22015-10-21 16:40:40 +00003455 builders_and_tests.setdefault(bot, [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003456
3457 for bot, tests in new_style:
3458 builders_and_tests.setdefault(bot, []).extend(tests)
3459
3460 # Return a master map with one master to be backwards compatible. The
3461 # master name defaults to an empty string, which will cause the master
3462 # not to be set on rietveld (deprecated).
3463 return {options.master: builders_and_tests}
3464
3465 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003466
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003467 for builders in masters.itervalues():
3468 if any('triggered' in b for b in builders):
3469 print >> sys.stderr, (
3470 'ERROR You are trying to send a job to a triggered bot. This type of'
3471 ' bot requires an\ninitial job from a parent (usually a builder). '
3472 'Instead send your job to the parent.\n'
3473 'Bot list: %s' % builders)
3474 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003475
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003476 patchset = cl.GetMostRecentPatchset()
3477 if patchset and patchset != cl.GetPatchset():
3478 print(
3479 '\nWARNING Mismatch between local config and server. Did a previous '
3480 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3481 'Continuing using\npatchset %s.\n' % patchset)
hinoka@chromium.orgfeb9e2a2015-09-25 19:11:09 +00003482 if options.luci:
3483 trigger_luci_job(cl, masters, options)
3484 elif not options.use_rietveld:
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003485 try:
3486 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3487 except BuildbucketResponseException as ex:
3488 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003489 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003490 except Exception as e:
3491 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3492 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3493 e, stacktrace)
3494 return 1
3495 else:
3496 try:
3497 cl.RpcServer().trigger_distributed_try_jobs(
3498 cl.GetIssue(), patchset, options.name, options.clobber,
3499 options.revision, masters)
3500 except urllib2.HTTPError as e:
3501 if e.code == 404:
3502 print('404 from rietveld; '
3503 'did you mean to use "git try" instead of "git cl try"?')
3504 return 1
3505 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003506
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003507 for (master, builders) in sorted(masters.iteritems()):
3508 if master:
3509 print 'Master: %s' % master
3510 length = max(len(builder) for builder in builders)
3511 for builder in sorted(builders):
3512 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003513 return 0
3514
3515
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00003516def CMDtry_results(parser, args):
3517 group = optparse.OptionGroup(parser, "Try job results options")
3518 group.add_option(
3519 "-p", "--patchset", type=int, help="patchset number if not current.")
3520 group.add_option(
3521 "--print-master", action='store_true', help="print master name as well")
3522 group.add_option(
3523 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3524 help="Host of buildbucket. The default host is %default.")
3525 parser.add_option_group(group)
3526 auth.add_auth_options(parser)
3527 options, args = parser.parse_args(args)
3528 if args:
3529 parser.error('Unrecognized args: %s' % ' '.join(args))
3530
3531 auth_config = auth.extract_auth_config_from_options(options)
3532 cl = Changelist(auth_config=auth_config)
3533 if not cl.GetIssue():
3534 parser.error('Need to upload first')
3535
3536 if not options.patchset:
3537 options.patchset = cl.GetMostRecentPatchset()
3538 if options.patchset and options.patchset != cl.GetPatchset():
3539 print(
3540 '\nWARNING Mismatch between local config and server. Did a previous '
3541 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3542 'Continuing using\npatchset %s.\n' % options.patchset)
3543 try:
3544 jobs = fetch_try_jobs(auth_config, cl, options)
3545 except BuildbucketResponseException as ex:
3546 print 'Buildbucket error: %s' % ex
3547 return 1
3548 except Exception as e:
3549 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3550 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
3551 e, stacktrace)
3552 return 1
3553 print_tryjobs(options, jobs)
3554 return 0
3555
3556
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003557@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003558def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003559 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003560 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003561 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003562 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003563
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003564 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003565 if args:
3566 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003567 branch = cl.GetBranch()
3568 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003569 cl = Changelist()
3570 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003571
3572 # Clear configured merge-base, if there is one.
3573 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003574 else:
3575 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003576 return 0
3577
3578
thestig@chromium.org00858c82013-12-02 23:08:03 +00003579def CMDweb(parser, args):
3580 """Opens the current CL in the web browser."""
3581 _, args = parser.parse_args(args)
3582 if args:
3583 parser.error('Unrecognized args: %s' % ' '.join(args))
3584
3585 issue_url = Changelist().GetIssueURL()
3586 if not issue_url:
3587 print >> sys.stderr, 'ERROR No issue to open'
3588 return 1
3589
3590 webbrowser.open(issue_url)
3591 return 0
3592
3593
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003594def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003595 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003596 auth.add_auth_options(parser)
3597 options, args = parser.parse_args(args)
3598 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003599 if args:
3600 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003601 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003602 props = cl.GetIssueProperties()
3603 if props.get('private'):
3604 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003605 cl.SetFlag('commit', '1')
3606 return 0
3607
3608
groby@chromium.org411034a2013-02-26 15:12:01 +00003609def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003610 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003611 auth.add_auth_options(parser)
3612 options, args = parser.parse_args(args)
3613 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003614 if args:
3615 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003616 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003617 # Ensure there actually is an issue to close.
3618 cl.GetDescription()
3619 cl.CloseIssue()
3620 return 0
3621
3622
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003623def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003624 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003625 auth.add_auth_options(parser)
3626 options, args = parser.parse_args(args)
3627 auth_config = auth.extract_auth_config_from_options(options)
3628 if args:
3629 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003630
3631 # Uncommitted (staged and unstaged) changes will be destroyed by
3632 # "git reset --hard" if there are merging conflicts in PatchIssue().
3633 # Staged changes would be committed along with the patch from last
3634 # upload, hence counted toward the "last upload" side in the final
3635 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003636 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003637 return 1
3638
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003639 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003640 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003641 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003642 if not issue:
3643 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003644 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003645 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003646
3647 # Create a new branch based on the merge-base
3648 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3649 try:
3650 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003651 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003652 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003653 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003654 return rtn
3655
wychen@chromium.org06928532015-02-03 02:11:29 +00003656 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003657 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003658 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003659 finally:
3660 RunGit(['checkout', '-q', branch])
3661 RunGit(['branch', '-D', TMP_BRANCH])
3662
3663 return 0
3664
3665
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003666def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003667 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003668 parser.add_option(
3669 '--no-color',
3670 action='store_true',
3671 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003672 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003673 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003674 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003675
3676 author = RunGit(['config', 'user.email']).strip() or None
3677
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003678 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003679
3680 if args:
3681 if len(args) > 1:
3682 parser.error('Unknown args')
3683 base_branch = args[0]
3684 else:
3685 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003686 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003687
3688 change = cl.GetChange(base_branch, None)
3689 return owners_finder.OwnersFinder(
3690 [f.LocalPath() for f in
3691 cl.GetChange(base_branch, None).AffectedFiles()],
3692 change.RepositoryRoot(), author,
3693 fopen=file, os_path=os.path, glob=glob.glob,
3694 disable_color=options.no_color).run()
3695
3696
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003697def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003698 """Generates a diff command."""
3699 # Generate diff for the current branch's changes.
3700 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3701 upstream_commit, '--' ]
3702
3703 if args:
3704 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003705 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003706 diff_cmd.append(arg)
3707 else:
3708 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003709
3710 return diff_cmd
3711
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003712def MatchingFileType(file_name, extensions):
3713 """Returns true if the file name ends with one of the given extensions."""
3714 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003715
enne@chromium.org555cfe42014-01-29 18:21:39 +00003716@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003717def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003718 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003719 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003720 GN_EXTS = ['.gn', '.gni']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003721 parser.add_option('--full', action='store_true',
3722 help='Reformat the full content of all touched files')
3723 parser.add_option('--dry-run', action='store_true',
3724 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003725 parser.add_option('--python', action='store_true',
3726 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003727 parser.add_option('--diff', action='store_true',
3728 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003729 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003730
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003731 # git diff generates paths against the root of the repository. Change
3732 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003733 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003734 if rel_base_path:
3735 os.chdir(rel_base_path)
3736
digit@chromium.org29e47272013-05-17 17:01:46 +00003737 # Grab the merge-base commit, i.e. the upstream commit of the current
3738 # branch when it was created or the last time it was rebased. This is
3739 # to cover the case where the user may have called "git fetch origin",
3740 # moving the origin branch to a newer commit, but hasn't rebased yet.
3741 upstream_commit = None
3742 cl = Changelist()
3743 upstream_branch = cl.GetUpstreamBranch()
3744 if upstream_branch:
3745 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3746 upstream_commit = upstream_commit.strip()
3747
3748 if not upstream_commit:
3749 DieWithError('Could not find base commit for this branch. '
3750 'Are you in detached state?')
3751
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003752 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
3753 diff_output = RunGit(changed_files_cmd)
3754 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00003755 # Filter out files deleted by this CL
3756 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003757
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003758 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
3759 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
3760 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003761 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00003762
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003763 top_dir = os.path.normpath(
3764 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3765
3766 # Locate the clang-format binary in the checkout
3767 try:
3768 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3769 except clang_format.NotFoundError, e:
3770 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003771
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003772 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3773 # formatted. This is used to block during the presubmit.
3774 return_value = 0
3775
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003776 if clang_diff_files:
3777 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003778 cmd = [clang_format_tool]
3779 if not opts.dry_run and not opts.diff:
3780 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003781 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003782 if opts.diff:
3783 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003784 else:
3785 env = os.environ.copy()
3786 env['PATH'] = str(os.path.dirname(clang_format_tool))
3787 try:
3788 script = clang_format.FindClangFormatScriptInChromiumTree(
3789 'clang-format-diff.py')
3790 except clang_format.NotFoundError, e:
3791 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003792
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003793 cmd = [sys.executable, script, '-p0']
3794 if not opts.dry_run and not opts.diff:
3795 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003796
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003797 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
3798 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003799
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00003800 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
3801 if opts.diff:
3802 sys.stdout.write(stdout)
3803 if opts.dry_run and len(stdout) > 0:
3804 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003805
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003806 # Similar code to above, but using yapf on .py files rather than clang-format
3807 # on C/C++ files
3808 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003809 yapf_tool = gclient_utils.FindExecutable('yapf')
3810 if yapf_tool is None:
3811 DieWithError('yapf not found in PATH')
3812
3813 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003814 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003815 cmd = [yapf_tool]
3816 if not opts.dry_run and not opts.diff:
3817 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003818 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003819 if opts.diff:
3820 sys.stdout.write(stdout)
3821 else:
3822 # TODO(sbc): yapf --lines mode still has some issues.
3823 # https://github.com/google/yapf/issues/154
3824 DieWithError('--python currently only works with --full')
3825
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003826 # Dart's formatter does not have the nice property of only operating on
3827 # modified chunks, so hard code full.
3828 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003829 try:
3830 command = [dart_format.FindDartFmtToolInChromiumTree()]
3831 if not opts.dry_run and not opts.diff:
3832 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00003833 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003834
3835 stdout = RunCommand(command, cwd=top_dir, env=env)
3836 if opts.dry_run and stdout:
3837 return_value = 2
3838 except dart_format.NotFoundError as e:
erikcorry@chromium.org3e445022015-12-17 09:07:26 +00003839 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
3840 'found in this checkout. Files in other languages are still ' +
3841 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003842
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00003843 # Format GN build files. Always run on full build files for canonical form.
3844 if gn_diff_files:
3845 cmd = ['gn', 'format']
3846 if not opts.dry_run and not opts.diff:
3847 cmd.append('--in-place')
3848 for gn_diff_file in gn_diff_files:
3849 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
3850 if opts.diff:
3851 sys.stdout.write(stdout)
3852
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003853 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003854
3855
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003856@subcommand.usage('<codereview url or issue id>')
3857def CMDcheckout(parser, args):
3858 """Checks out a branch associated with a given Rietveld issue."""
3859 _, args = parser.parse_args(args)
3860
3861 if len(args) != 1:
3862 parser.print_help()
3863 return 1
3864
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00003865 target_issue = ParseIssueNum(args[0])
3866 if target_issue == None:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00003867 parser.print_help()
3868 return 1
3869
3870 key_and_issues = [x.split() for x in RunGit(
3871 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
3872 .splitlines()]
3873 branches = []
3874 for key, issue in key_and_issues:
3875 if issue == target_issue:
3876 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
3877
3878 if len(branches) == 0:
3879 print 'No branch found for issue %s.' % target_issue
3880 return 1
3881 if len(branches) == 1:
3882 RunGit(['checkout', branches[0]])
3883 else:
3884 print 'Multiple branches match issue %s:' % target_issue
3885 for i in range(len(branches)):
3886 print '%d: %s' % (i, branches[i])
3887 which = raw_input('Choose by index: ')
3888 try:
3889 RunGit(['checkout', branches[int(which)]])
3890 except (IndexError, ValueError):
3891 print 'Invalid selection, not checking out any branch.'
3892 return 1
3893
3894 return 0
3895
3896
maruel@chromium.org29404b52014-09-08 22:58:00 +00003897def CMDlol(parser, args):
3898 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003899 print zlib.decompress(base64.b64decode(
3900 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3901 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3902 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3903 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003904 return 0
3905
3906
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003907class OptionParser(optparse.OptionParser):
3908 """Creates the option parse and add --verbose support."""
3909 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003910 optparse.OptionParser.__init__(
3911 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003912 self.add_option(
3913 '-v', '--verbose', action='count', default=0,
3914 help='Use 2 times for more debugging info')
3915
3916 def parse_args(self, args=None, values=None):
3917 options, args = optparse.OptionParser.parse_args(self, args, values)
3918 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3919 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3920 return options, args
3921
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003922
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003923def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003924 if sys.hexversion < 0x02060000:
3925 print >> sys.stderr, (
3926 '\nYour python version %s is unsupported, please upgrade.\n' %
3927 sys.version.split(' ', 1)[0])
3928 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003929
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003930 # Reload settings.
3931 global settings
3932 settings = Settings()
3933
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003934 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003935 dispatcher = subcommand.CommandDispatcher(__name__)
3936 try:
3937 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003938 except auth.AuthenticationError as e:
3939 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003940 except urllib2.HTTPError, e:
3941 if e.code != 500:
3942 raise
3943 DieWithError(
3944 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3945 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003946 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003947
3948
3949if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003950 # These affect sys.stdout so do it outside of main() to simplify mocks in
3951 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003952 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003953 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003954 try:
3955 sys.exit(main(sys.argv[1:]))
3956 except KeyboardInterrupt:
3957 sys.stderr.write('interrupted\n')
3958 sys.exit(1)