blob: ef974d50f33f07faf2a5e7cb1049cc5813649d4c [file] [log] [blame]
[email protected]405b87e2015-11-12 18:08:341#!/usr/bin/env python
[email protected]183df1a2012-01-04 19:44:552# Copyright (c) 2012 The Chromium Authors. All rights reserved.
[email protected]725f1c32011-04-01 20:24:543# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
[email protected]cc51cd02010-12-23 00:48:396# Copyright (C) 2008 Evan Martin <[email protected]>
7
[email protected]725f1c32011-04-01 20:24:548"""A git-command for integrating reviews on Rietveld."""
9
[email protected]6a0b07c2013-07-10 01:29:1910from distutils.version import LooseVersion
[email protected]ffde55c2015-03-12 00:44:1711from multiprocessing.pool import ThreadPool
[email protected]3421c992014-11-02 02:20:3212import base64
[email protected]2dd99862015-06-22 12:22:1813import collections
[email protected]faf3fdf2013-09-20 02:11:4814import glob
[email protected]6ebaf782015-05-12 19:17:5415import httplib
[email protected]4f6852c2012-04-20 20:39:2016import json
[email protected]cc51cd02010-12-23 00:48:3917import logging
18import optparse
19import os
[email protected]1033efd2013-07-23 23:25:0920import Queue
[email protected]cc51cd02010-12-23 00:48:3921import re
[email protected]78c4b982012-02-14 02:20:2622import stat
[email protected]cc51cd02010-12-23 00:48:3923import sys
[email protected]27386dd2015-02-16 10:45:3924import tempfile
[email protected]cc51cd02010-12-23 00:48:3925import textwrap
[email protected]6ebaf782015-05-12 19:17:5426import time
27import traceback
[email protected]b015fac2016-02-26 14:52:0128import urllib
[email protected]cc51cd02010-12-23 00:48:3929import urllib2
[email protected]967c0a82013-06-17 22:52:2430import urlparse
[email protected]b015fac2016-02-26 14:52:0131import uuid
[email protected]00858c82013-12-02 23:08:0332import webbrowser
[email protected]3421c992014-11-02 02:20:3233import zlib
[email protected]cc51cd02010-12-23 00:48:3934
35try:
[email protected]c98c0c52011-04-06 13:39:4336 import readline # pylint: disable=F0401,W0611
[email protected]cc51cd02010-12-23 00:48:3937except ImportError:
38 pass
39
[email protected]2e23ce32013-05-07 12:42:2840from third_party import colorama
[email protected]6ebaf782015-05-12 19:17:5441from third_party import httplib2
[email protected]2a74d372011-03-29 19:05:5042from third_party import upload
[email protected]cf6a5d22015-04-09 22:02:0043import auth
[email protected]feb9e2a2015-09-25 19:11:0944from luci_hacks import trigger_luci_job as luci_trigger
[email protected]3ac1c4e2014-01-16 02:44:4245import clang_format
[email protected]71184c02016-01-13 15:18:4446import commit_queue
[email protected]e0a7c5d2015-02-23 20:30:0847import dart_format
[email protected]6f09cd92011-04-01 16:38:1248import fix_encoding
[email protected]0e0436a2011-10-25 13:32:4149import gclient_utils
[email protected]9e849272014-04-04 00:31:5550import git_common
[email protected]f0e41522015-06-10 19:52:0151from git_footers import get_footer_svn_id
[email protected]336f9122014-09-04 02:16:5552import owners
[email protected]9e849272014-04-04 00:31:5553import owners_finder
[email protected]2a74d372011-03-29 19:05:5054import presubmit_support
[email protected]cab38e92011-04-09 00:30:5155import rietveld
[email protected]2a74d372011-03-29 19:05:5056import scm
[email protected]0633fb42013-08-16 20:06:1457import subcommand
[email protected]32f9f5e2011-09-14 13:41:4758import subprocess2
[email protected]2a74d372011-03-29 19:05:5059import watchlists
60
[email protected]0633fb42013-08-16 20:06:1461__version__ = '1.0'
[email protected]2a74d372011-03-29 19:05:5062
[email protected]eb5edbc2012-01-16 17:03:2863DEFAULT_SERVER = 'https://codereview.appspot.com'
[email protected]0ba7f962011-01-11 22:13:5864POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
[email protected]cc51cd02010-12-23 00:48:3965DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
[email protected]d6617f32013-11-19 00:34:5466GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
[email protected]aebe87f2012-10-22 20:34:2167CHANGE_ID = 'Change-Id:'
[email protected]c68112d2015-03-03 12:48:0668REFS_THAT_ALIAS_TO_OTHER_REFS = {
69 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
70 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
71}
[email protected]cc51cd02010-12-23 00:48:3972
[email protected]44202a22014-03-11 19:22:1873# Valid extensions for files we want to lint.
74DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
75DEFAULT_LINT_IGNORE_REGEX = r"$^"
76
[email protected]2e23ce32013-05-07 12:42:2877# Shortcut since it quickly becomes redundant.
78Fore = colorama.Fore
[email protected]90541732011-04-01 17:54:1879
[email protected]ddd59412011-11-30 14:20:3880# Initialized in main()
81settings = None
82
83
[email protected]cc51cd02010-12-23 00:48:3984def DieWithError(message):
[email protected]970c5222011-03-12 00:32:2485 print >> sys.stderr, message
[email protected]cc51cd02010-12-23 00:48:3986 sys.exit(1)
87
88
[email protected]8b0553c2014-02-11 00:33:3789def 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
[email protected]566a02a2014-08-22 01:34:1395
[email protected]32f9f5e2011-09-14 13:41:4796def RunCommand(args, error_ok=False, error_message=None, **kwargs):
[email protected]cc51cd02010-12-23 00:48:3997 try:
[email protected]373af802012-05-25 21:07:3398 return subprocess2.check_output(args, shell=False, **kwargs)
[email protected]78936cb2013-04-11 00:17:5299 except subprocess2.CalledProcessError as e:
100 logging.debug('Failed running %s', args)
[email protected]32f9f5e2011-09-14 13:41:47101 if not error_ok:
[email protected]cc51cd02010-12-23 00:48:39102 DieWithError(
[email protected]32f9f5e2011-09-14 13:41:47103 'Command "%s" failed.\n%s' % (
104 ' '.join(args), error_message or e.stdout or ''))
105 return e.stdout
[email protected]cc51cd02010-12-23 00:48:39106
107
108def RunGit(args, **kwargs):
[email protected]32f9f5e2011-09-14 13:41:47109 """Returns stdout."""
[email protected]82b91cd2013-07-09 06:33:41110 return RunCommand(['git'] + args, **kwargs)
[email protected]cc51cd02010-12-23 00:48:39111
112
[email protected]3b7e15c2014-01-21 17:44:47113def RunGitWithCode(args, suppress_stderr=False):
[email protected]32f9f5e2011-09-14 13:41:47114 """Returns return code and stdout."""
[email protected]9bb85e22012-06-13 20:28:23115 try:
[email protected]3b7e15c2014-01-21 17:44:47116 if suppress_stderr:
117 stderr = subprocess2.VOID
118 else:
119 stderr = sys.stderr
[email protected]82b91cd2013-07-09 06:33:41120 out, code = subprocess2.communicate(['git'] + args,
[email protected]8b0553c2014-02-11 00:33:37121 env=GetNoGitPagerEnv(),
[email protected]3b7e15c2014-01-21 17:44:47122 stdout=subprocess2.PIPE,
123 stderr=stderr)
[email protected]9bb85e22012-06-13 20:28:23124 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, ''
[email protected]cc51cd02010-12-23 00:48:39129
130
[email protected]27386dd2015-02-16 10:45:39131def RunGitSilent(args):
132 """Returns stdout, suppresses stderr and ingores the return code."""
133 return RunGitWithCode(args, suppress_stderr=True)[1]
134
135
[email protected]6a0b07c2013-07-10 01:29:19136def IsGitVersionAtLeast(min_version):
[email protected]cc56ee42013-07-10 22:16:29137 prefix = 'git version '
[email protected]6a0b07c2013-07-10 01:29:19138 version = RunGit(['--version']).strip()
[email protected]cc56ee42013-07-10 22:16:29139 return (version.startswith(prefix) and
140 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
[email protected]6a0b07c2013-07-10 01:29:19141
142
[email protected]8ba38ff2015-06-11 21:41:25143def 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
[email protected]90541732011-04-01 17:54:18150def ask_for_data(prompt):
151 try:
152 return raw_input(prompt)
153 except KeyboardInterrupt:
154 # Hide the exception.
155 sys.exit(1)
156
157
[email protected]79540052012-10-19 23:15:26158def git_set_branch_value(key, value):
159 branch = Changelist().GetBranch()
[email protected]caa16552013-03-18 20:45:05160 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)])
[email protected]79540052012-10-19 23:15:26168
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
[email protected]53937ba2012-10-02 18:20:43182def add_git_similarity(parser):
183 parser.add_option(
[email protected]79540052012-10-19 23:15:26184 '--similarity', metavar='SIM', type='int', action='store',
[email protected]53937ba2012-10-02 18:20:43185 help='Sets the percentage that a pair of files need to match in order to'
186 ' be considered copies (default 50)')
[email protected]79540052012-10-19 23:15:26187 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.')
[email protected]53937ba2012-10-02 18:20:43193
194 old_parser_args = parser.parse_args
195 def Parse(args):
196 options, args = old_parser_args(args)
197
[email protected]53937ba2012-10-02 18:20:43198 if options.similarity is None:
[email protected]79540052012-10-19 23:15:26199 options.similarity = git_get_branch_default('git-cl-similarity', 50)
[email protected]53937ba2012-10-02 18:20:43200 else:
[email protected]79540052012-10-19 23:15:26201 print('Note: Saving similarity of %d%% in git config.'
202 % options.similarity)
203 git_set_branch_value('git-cl-similarity', options.similarity)
[email protected]53937ba2012-10-02 18:20:43204
[email protected]79540052012-10-19 23:15:26205 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))
[email protected]53937ba2012-10-02 18:20:43212
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
[email protected]45453142015-09-15 08:45:22220def _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
[email protected]6ebaf782015-05-12 19:17:54230def _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
[email protected]b015fac2016-02-26 14:52:01244def _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
[email protected]feb9e2a2015-09-25 19:11:09278def 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()):
[email protected]3764fa22015-10-21 16:40:40284 # 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):
[email protected]feb9e2a2015-09-25 19:11:09287 luci_trigger.trigger(
288 builder, 'HEAD', issue, patchset, issue_props['project'])
289
290
[email protected]45453142015-09-15 08:45:22291def trigger_try_jobs(auth_config, changelist, options, masters, category):
[email protected]6ebaf782015-05-12 19:17:54292 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()
[email protected]45453142015-09-15 08:45:22300 properties = _get_properties_from_options(options)
[email protected]6ebaf782015-05-12 19:17:54301
302 buildbucket_put_url = (
303 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
[email protected]db375572015-08-17 19:22:23304 hostname=options.buildbucket_host))
[email protected]6ebaf782015-05-12 19:17:54305 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,
[email protected]d2217312015-09-21 15:51:21320 'changes': [{
321 'author': {'email': issue_props['owner_email']},
322 'revision': options.revision,
323 }],
[email protected]6ebaf782015-05-12 19:17:54324 '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,
[email protected]6ebaf782015-05-12 19:17:54332 'rietveld': rietveld_url,
[email protected]6ebaf782015-05-12 19:17:54333 },
334 }
[email protected]3764fa22015-10-21 16:40:40335 if tests:
336 parameters['properties']['testfilter'] = tests
[email protected]45453142015-09-15 08:45:22337 if properties:
338 parameters['properties'].update(properties)
[email protected]6ebaf782015-05-12 19:17:54339 if options.clobber:
340 parameters['properties']['clobber'] = True
341 batch_req_body['builds'].append(
342 {
343 'bucket': bucket,
344 'parameters_json': json.dumps(parameters),
[email protected]b015fac2016-02-26 14:52:01345 'client_operation_id': str(uuid.uuid4()),
[email protected]6ebaf782015-05-12 19:17:54346 'tags': ['builder:%s' % builder,
347 'buildset:%s' % buildset,
348 'master:%s' % master,
349 'user_agent:git_cl_try']
350 }
351 )
352
[email protected]b015fac2016-02-26 14:52:01353 _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 )
[email protected]35c61452016-02-26 15:24:57361 print_text.append('To see results here, run: git cl try-results')
362 print_text.append('To see results in browser, run: git cl web')
[email protected]6ebaf782015-05-12 19:17:54363 print '\n'.join(print_text)
[email protected]44424542015-06-02 18:35:29364
[email protected]6ebaf782015-05-12 19:17:54365
[email protected]b015fac2016-02-26 14:52:01366def fetch_try_jobs(auth_config, changelist, options):
367 """Fetches tryjobs from buildbucket.
368
369 Returns a map from build id to build info as json dictionary.
370 """
371 rietveld_url = settings.GetDefaultServerUrl()
372 rietveld_host = urlparse.urlparse(rietveld_url).hostname
373 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
374 if authenticator.has_cached_credentials():
375 http = authenticator.authorize(httplib2.Http())
376 else:
377 print ('Warning: Some results might be missing because %s' %
378 # Get the message on how to login.
379 auth.LoginRequiredError(rietveld_host).message)
380 http = httplib2.Http()
381
382 http.force_exception_to_status_code = True
383
384 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
385 hostname=rietveld_host,
386 issue=changelist.GetIssue(),
387 patch=options.patchset)
388 params = {'tag': 'buildset:%s' % buildset}
389
390 builds = {}
391 while True:
392 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
393 hostname=options.buildbucket_host,
394 params=urllib.urlencode(params))
395 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
396 for build in content.get('builds', []):
397 builds[build['id']] = build
398 if 'next_cursor' in content:
399 params['start_cursor'] = content['next_cursor']
400 else:
401 break
402 return builds
403
404
405def print_tryjobs(options, builds):
406 """Prints nicely result of fetch_try_jobs."""
407 if not builds:
408 print 'No tryjobs scheduled'
409 return
410
411 # Make a copy, because we'll be modifying builds dictionary.
412 builds = builds.copy()
413 builder_names_cache = {}
414
415 def get_builder(b):
416 try:
417 return builder_names_cache[b['id']]
418 except KeyError:
419 try:
420 parameters = json.loads(b['parameters_json'])
421 name = parameters['builder_name']
422 except (ValueError, KeyError) as error:
423 print 'WARNING: failed to get builder name for build %s: %s' % (
424 b['id'], error)
425 name = None
426 builder_names_cache[b['id']] = name
427 return name
428
429 def get_bucket(b):
430 bucket = b['bucket']
431 if bucket.startswith('master.'):
432 return bucket[len('master.'):]
433 return bucket
434
435 if options.print_master:
436 name_fmt = '%%-%ds %%-%ds' % (
437 max(len(str(get_bucket(b))) for b in builds.itervalues()),
438 max(len(str(get_builder(b))) for b in builds.itervalues()))
439 def get_name(b):
440 return name_fmt % (get_bucket(b), get_builder(b))
441 else:
442 name_fmt = '%%-%ds' % (
443 max(len(str(get_builder(b))) for b in builds.itervalues()))
444 def get_name(b):
445 return name_fmt % get_builder(b)
446
447 def sort_key(b):
448 return b['status'], b.get('result'), get_name(b), b.get('url')
449
450 def pop(title, f, color=None, **kwargs):
451 """Pop matching builds from `builds` dict and print them."""
452
453 if not sys.stdout.isatty() or color is None:
454 colorize = str
455 else:
456 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
457
458 result = []
459 for b in builds.values():
460 if all(b.get(k) == v for k, v in kwargs.iteritems()):
461 builds.pop(b['id'])
462 result.append(b)
463 if result:
464 print colorize(title)
465 for b in sorted(result, key=sort_key):
466 print ' ', colorize('\t'.join(map(str, f(b))))
467
468 total = len(builds)
469 pop(status='COMPLETED', result='SUCCESS',
470 title='Successes:', color=Fore.GREEN,
471 f=lambda b: (get_name(b), b.get('url')))
472 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
473 title='Infra Failures:', color=Fore.MAGENTA,
474 f=lambda b: (get_name(b), b.get('url')))
475 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
476 title='Failures:', color=Fore.RED,
477 f=lambda b: (get_name(b), b.get('url')))
478 pop(status='COMPLETED', result='CANCELED',
479 title='Canceled:', color=Fore.MAGENTA,
480 f=lambda b: (get_name(b),))
481 pop(status='COMPLETED', result='FAILURE',
482 failure_reason='INVALID_BUILD_DEFINITION',
483 title='Wrong master/builder name:', color=Fore.MAGENTA,
484 f=lambda b: (get_name(b),))
485 pop(status='COMPLETED', result='FAILURE',
486 title='Other failures:',
487 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
488 pop(status='COMPLETED',
489 title='Other finished:',
490 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
491 pop(status='STARTED',
492 title='Started:', color=Fore.YELLOW,
493 f=lambda b: (get_name(b), b.get('url')))
494 pop(status='SCHEDULED',
495 title='Scheduled:',
496 f=lambda b: (get_name(b), 'id=%s' % b['id']))
497 # The last section is just in case buildbucket API changes OR there is a bug.
498 pop(title='Other:',
499 f=lambda b: (get_name(b), 'id=%s' % b['id']))
500 assert len(builds) == 0
501 print 'Total: %d tryjobs' % total
502
503
[email protected]866276c2011-03-18 20:09:31504def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
505 """Return the corresponding git ref if |base_url| together with |glob_spec|
506 matches the full |url|.
507
508 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
509 """
510 fetch_suburl, as_ref = glob_spec.split(':')
511 if allow_wildcards:
512 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
513 if glob_match:
514 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
515 # "branches/{472,597,648}/src:refs/remotes/svn/*".
516 branch_re = re.escape(base_url)
517 if glob_match.group(1):
518 branch_re += '/' + re.escape(glob_match.group(1))
519 wildcard = glob_match.group(2)
520 if wildcard == '*':
521 branch_re += '([^/]*)'
522 else:
523 # Escape and replace surrounding braces with parentheses and commas
524 # with pipe symbols.
525 wildcard = re.escape(wildcard)
526 wildcard = re.sub('^\\\\{', '(', wildcard)
527 wildcard = re.sub('\\\\,', '|', wildcard)
528 wildcard = re.sub('\\\\}$', ')', wildcard)
529 branch_re += wildcard
530 if glob_match.group(3):
531 branch_re += re.escape(glob_match.group(3))
532 match = re.match(branch_re, url)
533 if match:
534 return re.sub('\*$', match.group(1), as_ref)
535
536 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
537 if fetch_suburl:
538 full_url = base_url + '/' + fetch_suburl
539 else:
540 full_url = base_url
541 if full_url == url:
542 return as_ref
543 return None
544
[email protected]32f9f5e2011-09-14 13:41:47545
[email protected]79540052012-10-19 23:15:26546def print_stats(similarity, find_copies, args):
[email protected]49e3d802012-07-18 23:54:45547 """Prints statistics about the change to the user."""
548 # --no-ext-diff is broken in some versions of Git, so try to work around
549 # this by overriding the environment (but there is still a problem if the
550 # git config key "diff.external" is used).
[email protected]8b0553c2014-02-11 00:33:37551 env = GetNoGitPagerEnv()
[email protected]49e3d802012-07-18 23:54:45552 if 'GIT_EXTERNAL_DIFF' in env:
553 del env['GIT_EXTERNAL_DIFF']
[email protected]79540052012-10-19 23:15:26554
555 if find_copies:
556 similarity_options = ['--find-copies-harder', '-l100000',
557 '-C%s' % similarity]
558 else:
559 similarity_options = ['-M%s' % similarity]
560
[email protected]d057f9a2014-05-29 21:09:36561 try:
562 stdout = sys.stdout.fileno()
563 except AttributeError:
564 stdout = None
[email protected]49e3d802012-07-18 23:54:45565 return subprocess2.call(
[email protected]82b91cd2013-07-09 06:33:41566 ['git',
[email protected]f267b0e2013-05-02 09:11:43567 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
[email protected]d057f9a2014-05-29 21:09:36568 stdout=stdout, env=env)
[email protected]49e3d802012-07-18 23:54:45569
570
[email protected]6ebaf782015-05-12 19:17:54571class BuildbucketResponseException(Exception):
572 pass
573
574
[email protected]cc51cd02010-12-23 00:48:39575class Settings(object):
576 def __init__(self):
577 self.default_server = None
578 self.cc = None
[email protected]7a54e812014-02-11 19:57:22579 self.root = None
[email protected]cc51cd02010-12-23 00:48:39580 self.is_git_svn = None
581 self.svn_branch = None
582 self.tree_status_url = None
583 self.viewvc_url = None
584 self.updated = False
[email protected]e8077812012-02-03 03:41:46585 self.is_gerrit = None
[email protected]54b400c2016-01-14 10:08:25586 self.squash_gerrit_uploads = None
[email protected]615a2622013-05-03 13:20:14587 self.git_editor = None
[email protected]152cf832014-06-11 21:37:49588 self.project = None
[email protected]6abc6522014-12-02 07:34:49589 self.force_https_commit_url = None
[email protected]566a02a2014-08-22 01:34:13590 self.pending_ref_prefix = None
[email protected]cc51cd02010-12-23 00:48:39591
592 def LazyUpdateIfNeeded(self):
593 """Updates the settings from a codereview.settings file, if available."""
594 if not self.updated:
[email protected]87884cc2014-01-03 22:23:41595 # The only value that actually changes the behavior is
596 # autoupdate = "false". Everything else means "true".
[email protected]3ac1c4e2014-01-16 02:44:42597 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
[email protected]87884cc2014-01-03 22:23:41598 error_ok=True
599 ).strip().lower()
600
[email protected]cc51cd02010-12-23 00:48:39601 cr_settings_file = FindCodereviewSettingsFile()
[email protected]87884cc2014-01-03 22:23:41602 if autoupdate != 'false' and cr_settings_file:
[email protected]cc51cd02010-12-23 00:48:39603 LoadCodereviewSettingsFromFile(cr_settings_file)
[email protected]87884cc2014-01-03 22:23:41604 # set updated to True to avoid infinite calling loop
605 # through DownloadHooks
[email protected]78c4b982012-02-14 02:20:26606 self.updated = True
607 DownloadHooks(False)
[email protected]cc51cd02010-12-23 00:48:39608 self.updated = True
609
610 def GetDefaultServerUrl(self, error_ok=False):
611 if not self.default_server:
612 self.LazyUpdateIfNeeded()
[email protected]eb5edbc2012-01-16 17:03:28613 self.default_server = gclient_utils.UpgradeToHttps(
[email protected]8b0553c2014-02-11 00:33:37614 self._GetRietveldConfig('server', error_ok=True))
[email protected]cc51cd02010-12-23 00:48:39615 if error_ok:
616 return self.default_server
617 if not self.default_server:
618 error_message = ('Could not find settings file. You must configure '
619 'your review setup by running "git cl config".')
[email protected]eb5edbc2012-01-16 17:03:28620 self.default_server = gclient_utils.UpgradeToHttps(
[email protected]8b0553c2014-02-11 00:33:37621 self._GetRietveldConfig('server', error_message=error_message))
[email protected]cc51cd02010-12-23 00:48:39622 return self.default_server
623
[email protected]7a54e812014-02-11 19:57:22624 @staticmethod
625 def GetRelativeRoot():
626 return RunGit(['rev-parse', '--show-cdup']).strip()
[email protected]8b0553c2014-02-11 00:33:37627
[email protected]cc51cd02010-12-23 00:48:39628 def GetRoot(self):
[email protected]7a54e812014-02-11 19:57:22629 if self.root is None:
630 self.root = os.path.abspath(self.GetRelativeRoot())
631 return self.root
[email protected]cc51cd02010-12-23 00:48:39632
633 def GetIsGitSvn(self):
634 """Return true if this repo looks like it's using git-svn."""
635 if self.is_git_svn is None:
[email protected]566a02a2014-08-22 01:34:13636 if self.GetPendingRefPrefix():
637 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
638 self.is_git_svn = False
639 else:
640 # If you have any "svn-remote.*" config keys, we think you're using svn.
641 self.is_git_svn = RunGitWithCode(
642 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
[email protected]cc51cd02010-12-23 00:48:39643 return self.is_git_svn
644
645 def GetSVNBranch(self):
646 if self.svn_branch is None:
647 if not self.GetIsGitSvn():
648 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
649
650 # Try to figure out which remote branch we're based on.
651 # Strategy:
[email protected]ade368c2011-03-01 08:57:50652 # 1) iterate through our branch history and find the svn URL.
653 # 2) find the svn-remote that fetches from the URL.
[email protected]cc51cd02010-12-23 00:48:39654
655 # regexp matching the git-svn line that contains the URL.
656 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
657
[email protected]ade368c2011-03-01 08:57:50658 # We don't want to go through all of history, so read a line from the
659 # pipe at a time.
660 # The -100 is an arbitrary limit so we don't search forever.
[email protected]82b91cd2013-07-09 06:33:41661 cmd = ['git', 'log', '-100', '--pretty=medium']
[email protected]8b0553c2014-02-11 00:33:37662 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
663 env=GetNoGitPagerEnv())
[email protected]740f9d72011-06-10 18:33:10664 url = None
[email protected]ade368c2011-03-01 08:57:50665 for line in proc.stdout:
666 match = git_svn_re.match(line)
667 if match:
668 url = match.group(1)
669 proc.stdout.close() # Cut pipe.
670 break
[email protected]cc51cd02010-12-23 00:48:39671
[email protected]ade368c2011-03-01 08:57:50672 if url:
673 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
674 remotes = RunGit(['config', '--get-regexp',
675 r'^svn-remote\..*\.url']).splitlines()
676 for remote in remotes:
677 match = svn_remote_re.match(remote)
[email protected]cc51cd02010-12-23 00:48:39678 if match:
[email protected]ade368c2011-03-01 08:57:50679 remote = match.group(1)
680 base_url = match.group(2)
[email protected]4ac25532013-12-16 22:07:02681 rewrite_root = RunGit(
682 ['config', 'svn-remote.%s.rewriteRoot' % remote],
683 error_ok=True).strip()
684 if rewrite_root:
685 base_url = rewrite_root
[email protected]ade368c2011-03-01 08:57:50686 fetch_spec = RunGit(
[email protected]866276c2011-03-18 20:09:31687 ['config', 'svn-remote.%s.fetch' % remote],
688 error_ok=True).strip()
689 if fetch_spec:
690 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
691 if self.svn_branch:
692 break
693 branch_spec = RunGit(
694 ['config', 'svn-remote.%s.branches' % remote],
695 error_ok=True).strip()
696 if branch_spec:
697 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
698 if self.svn_branch:
699 break
700 tag_spec = RunGit(
701 ['config', 'svn-remote.%s.tags' % remote],
702 error_ok=True).strip()
703 if tag_spec:
704 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
705 if self.svn_branch:
706 break
[email protected]cc51cd02010-12-23 00:48:39707
708 if not self.svn_branch:
709 DieWithError('Can\'t guess svn branch -- try specifying it on the '
710 'command line')
711
712 return self.svn_branch
713
714 def GetTreeStatusUrl(self, error_ok=False):
715 if not self.tree_status_url:
716 error_message = ('You must configure your tree status URL by running '
717 '"git cl config".')
[email protected]8b0553c2014-02-11 00:33:37718 self.tree_status_url = self._GetRietveldConfig(
719 'tree-status-url', error_ok=error_ok, error_message=error_message)
[email protected]cc51cd02010-12-23 00:48:39720 return self.tree_status_url
721
722 def GetViewVCUrl(self):
723 if not self.viewvc_url:
[email protected]8b0553c2014-02-11 00:33:37724 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
[email protected]cc51cd02010-12-23 00:48:39725 return self.viewvc_url
726
[email protected]90752582014-01-14 21:04:50727 def GetBugPrefix(self):
[email protected]8b0553c2014-02-11 00:33:37728 return self._GetRietveldConfig('bug-prefix', error_ok=True)
[email protected]90752582014-01-14 21:04:50729
[email protected]78948ed2015-07-08 23:09:57730 def GetIsSkipDependencyUpload(self, branch_name):
731 """Returns true if specified branch should skip dep uploads."""
732 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
733 error_ok=True)
734
[email protected]5626a922015-02-26 14:03:30735 def GetRunPostUploadHook(self):
736 run_post_upload_hook = self._GetRietveldConfig(
737 'run-post-upload-hook', error_ok=True)
738 return run_post_upload_hook == "True"
739
[email protected]ae6df352011-04-06 17:40:39740 def GetDefaultCCList(self):
[email protected]8b0553c2014-02-11 00:33:37741 return self._GetRietveldConfig('cc', error_ok=True)
[email protected]ae6df352011-04-06 17:40:39742
[email protected]c1737d02013-05-29 14:17:28743 def GetDefaultPrivateFlag(self):
[email protected]8b0553c2014-02-11 00:33:37744 return self._GetRietveldConfig('private', error_ok=True)
[email protected]c1737d02013-05-29 14:17:28745
[email protected]e8077812012-02-03 03:41:46746 def GetIsGerrit(self):
747 """Return true if this repo is assosiated with gerrit code review system."""
748 if self.is_gerrit is None:
749 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
750 return self.is_gerrit
751
[email protected]54b400c2016-01-14 10:08:25752 def GetSquashGerritUploads(self):
753 """Return true if uploads to Gerrit should be squashed by default."""
754 if self.squash_gerrit_uploads is None:
755 self.squash_gerrit_uploads = (
756 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
757 error_ok=True).strip() == 'true')
758 return self.squash_gerrit_uploads
759
[email protected]615a2622013-05-03 13:20:14760 def GetGitEditor(self):
761 """Return the editor specified in the git config, or None if none is."""
762 if self.git_editor is None:
763 self.git_editor = self._GetConfig('core.editor', error_ok=True)
764 return self.git_editor or None
765
[email protected]44202a22014-03-11 19:22:18766 def GetLintRegex(self):
767 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
768 DEFAULT_LINT_REGEX)
769
770 def GetLintIgnoreRegex(self):
771 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
772 DEFAULT_LINT_IGNORE_REGEX)
773
[email protected]152cf832014-06-11 21:37:49774 def GetProject(self):
775 if not self.project:
776 self.project = self._GetRietveldConfig('project', error_ok=True)
777 return self.project
778
[email protected]6abc6522014-12-02 07:34:49779 def GetForceHttpsCommitUrl(self):
780 if not self.force_https_commit_url:
781 self.force_https_commit_url = self._GetRietveldConfig(
782 'force-https-commit-url', error_ok=True)
783 return self.force_https_commit_url
784
[email protected]566a02a2014-08-22 01:34:13785 def GetPendingRefPrefix(self):
786 if not self.pending_ref_prefix:
787 self.pending_ref_prefix = self._GetRietveldConfig(
788 'pending-ref-prefix', error_ok=True)
789 return self.pending_ref_prefix
790
[email protected]8b0553c2014-02-11 00:33:37791 def _GetRietveldConfig(self, param, **kwargs):
792 return self._GetConfig('rietveld.' + param, **kwargs)
793
[email protected]78948ed2015-07-08 23:09:57794 def _GetBranchConfig(self, branch_name, param, **kwargs):
795 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
796
[email protected]cc51cd02010-12-23 00:48:39797 def _GetConfig(self, param, **kwargs):
798 self.LazyUpdateIfNeeded()
799 return RunGit(['config', param], **kwargs).strip()
800
801
[email protected]cc51cd02010-12-23 00:48:39802def ShortBranchName(branch):
803 """Convert a name like 'refs/heads/foo' to just 'foo'."""
804 return branch.replace('refs/heads/', '')
805
806
807class Changelist(object):
[email protected]cf6a5d22015-04-09 22:02:00808 def __init__(self, branchref=None, issue=None, auth_config=None):
[email protected]cc51cd02010-12-23 00:48:39809 # Poke settings so we get the "configure your server" message if necessary.
[email protected]379d07a2011-11-30 14:58:10810 global settings
811 if not settings:
812 # Happens when git_cl.py is used as a utility library.
813 settings = Settings()
[email protected]cc51cd02010-12-23 00:48:39814 settings.GetDefaultServerUrl()
815 self.branchref = branchref
816 if self.branchref:
817 self.branch = ShortBranchName(self.branchref)
818 else:
819 self.branch = None
820 self.rietveld_server = None
821 self.upstream_branch = None
[email protected]1033efd2013-07-23 23:25:09822 self.lookedup_issue = False
823 self.issue = issue or None
[email protected]cc51cd02010-12-23 00:48:39824 self.has_description = False
825 self.description = None
[email protected]1033efd2013-07-23 23:25:09826 self.lookedup_patchset = False
[email protected]cc51cd02010-12-23 00:48:39827 self.patchset = None
[email protected]ae6df352011-04-06 17:40:39828 self.cc = None
829 self.watchers = ()
[email protected]cf6a5d22015-04-09 22:02:00830 self._auth_config = auth_config
[email protected]1033efd2013-07-23 23:25:09831 self._props = None
[email protected]cf6a5d22015-04-09 22:02:00832 self._remote = None
833 self._rpc_server = None
834
835 @property
836 def auth_config(self):
837 return self._auth_config
[email protected]ae6df352011-04-06 17:40:39838
839 def GetCCList(self):
840 """Return the users cc'd on this CL.
841
842 Return is a string suitable for passing to gcl with the --cc flag.
843 """
844 if self.cc is None:
[email protected]99918ab2013-09-30 06:17:28845 base_cc = settings.GetDefaultCCList()
[email protected]ae6df352011-04-06 17:40:39846 more_cc = ','.join(self.watchers)
847 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
848 return self.cc
849
[email protected]99918ab2013-09-30 06:17:28850 def GetCCListWithoutDefault(self):
851 """Return the users cc'd on this CL excluding default ones."""
852 if self.cc is None:
853 self.cc = ','.join(self.watchers)
854 return self.cc
855
[email protected]ae6df352011-04-06 17:40:39856 def SetWatchers(self, watchers):
857 """Set the list of email addresses that should be cc'd based on the changed
858 files in this CL.
859 """
860 self.watchers = watchers
[email protected]cc51cd02010-12-23 00:48:39861
862 def GetBranch(self):
863 """Returns the short branch name, e.g. 'master'."""
864 if not self.branch:
[email protected]d62c61f2014-10-20 22:33:21865 branchref = RunGit(['symbolic-ref', 'HEAD'],
866 stderr=subprocess2.VOID, error_ok=True).strip()
867 if not branchref:
868 return None
869 self.branchref = branchref
[email protected]cc51cd02010-12-23 00:48:39870 self.branch = ShortBranchName(self.branchref)
871 return self.branch
872
873 def GetBranchRef(self):
874 """Returns the full branch name, e.g. 'refs/heads/master'."""
875 self.GetBranch() # Poke the lazy loader.
876 return self.branchref
877
[email protected]0f58fa82012-11-05 01:45:20878 @staticmethod
879 def FetchUpstreamTuple(branch):
[email protected]d6617f32013-11-19 00:34:54880 """Returns a tuple containing remote and remote ref,
[email protected]cc51cd02010-12-23 00:48:39881 e.g. 'origin', 'refs/heads/master'
882 """
883 remote = '.'
[email protected]cc51cd02010-12-23 00:48:39884 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
885 error_ok=True).strip()
886 if upstream_branch:
887 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
888 else:
[email protected]ade368c2011-03-01 08:57:50889 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
890 error_ok=True).strip()
891 if upstream_branch:
892 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
[email protected]cc51cd02010-12-23 00:48:39893 else:
[email protected]ade368c2011-03-01 08:57:50894 # Fall back on trying a git-svn upstream branch.
895 if settings.GetIsGitSvn():
896 upstream_branch = settings.GetSVNBranch()
[email protected]cc51cd02010-12-23 00:48:39897 else:
[email protected]ade368c2011-03-01 08:57:50898 # Else, try to guess the origin remote.
899 remote_branches = RunGit(['branch', '-r']).split()
900 if 'origin/master' in remote_branches:
901 # Fall back on origin/master if it exits.
902 remote = 'origin'
903 upstream_branch = 'refs/heads/master'
904 elif 'origin/trunk' in remote_branches:
905 # Fall back on origin/trunk if it exists. Generally a shared
906 # git-svn clone
907 remote = 'origin'
908 upstream_branch = 'refs/heads/trunk'
909 else:
910 DieWithError("""Unable to determine default branch to diff against.
[email protected]cc51cd02010-12-23 00:48:39911Either pass complete "git diff"-style arguments, like
912 git cl upload origin/master
913or verify this branch is set up to track another (via the --track argument to
914"git checkout -b ...").""")
915
916 return remote, upstream_branch
917
[email protected]8b0553c2014-02-11 00:33:37918 def GetCommonAncestorWithUpstream(self):
[email protected]8ba38ff2015-06-11 21:41:25919 upstream_branch = self.GetUpstreamBranch()
920 if not BranchExists(upstream_branch):
921 DieWithError('The upstream for the current branch (%s) does not exist '
922 'anymore.\nPlease fix it and try again.' % self.GetBranch())
[email protected]9e849272014-04-04 00:31:55923 return git_common.get_or_create_merge_base(self.GetBranch(),
[email protected]8ba38ff2015-06-11 21:41:25924 upstream_branch)
[email protected]8b0553c2014-02-11 00:33:37925
[email protected]cc51cd02010-12-23 00:48:39926 def GetUpstreamBranch(self):
927 if self.upstream_branch is None:
[email protected]0f58fa82012-11-05 01:45:20928 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
[email protected]cc51cd02010-12-23 00:48:39929 if remote is not '.':
[email protected]e7585452014-08-24 01:41:11930 upstream_branch = upstream_branch.replace('refs/heads/',
931 'refs/remotes/%s/' % remote)
932 upstream_branch = upstream_branch.replace('refs/branch-heads/',
933 'refs/remotes/branch-heads/')
[email protected]cc51cd02010-12-23 00:48:39934 self.upstream_branch = upstream_branch
935 return self.upstream_branch
936
[email protected]0f58fa82012-11-05 01:45:20937 def GetRemoteBranch(self):
[email protected]a2cbbbb2012-03-22 20:40:40938 if not self._remote:
[email protected]0f58fa82012-11-05 01:45:20939 remote, branch = None, self.GetBranch()
940 seen_branches = set()
941 while branch not in seen_branches:
942 seen_branches.add(branch)
943 remote, branch = self.FetchUpstreamTuple(branch)
944 branch = ShortBranchName(branch)
945 if remote != '.' or branch.startswith('refs/remotes'):
946 break
947 else:
[email protected]a2cbbbb2012-03-22 20:40:40948 remotes = RunGit(['remote'], error_ok=True).split()
949 if len(remotes) == 1:
[email protected]0f58fa82012-11-05 01:45:20950 remote, = remotes
[email protected]a2cbbbb2012-03-22 20:40:40951 elif 'origin' in remotes:
[email protected]0f58fa82012-11-05 01:45:20952 remote = 'origin'
[email protected]a2cbbbb2012-03-22 20:40:40953 logging.warning('Could not determine which remote this change is '
954 'associated with, so defaulting to "%s". This may '
955 'not be what you want. You may prevent this message '
956 'by running "git svn info" as documented here: %s',
957 self._remote,
958 GIT_INSTRUCTIONS_URL)
959 else:
960 logging.warn('Could not determine which remote this change is '
961 'associated with. You may prevent this message by '
962 'running "git svn info" as documented here: %s',
963 GIT_INSTRUCTIONS_URL)
[email protected]0f58fa82012-11-05 01:45:20964 branch = 'HEAD'
965 if branch.startswith('refs/remotes'):
966 self._remote = (remote, branch)
[email protected]e7585452014-08-24 01:41:11967 elif branch.startswith('refs/branch-heads/'):
968 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
[email protected]0f58fa82012-11-05 01:45:20969 else:
970 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
[email protected]a2cbbbb2012-03-22 20:40:40971 return self._remote
972
[email protected]0f58fa82012-11-05 01:45:20973 def GitSanityChecks(self, upstream_git_obj):
974 """Checks git repo status and ensures diff is from local commits."""
975
[email protected]79706062015-01-14 21:18:12976 if upstream_git_obj is None:
977 if self.GetBranch() is None:
978 print >> sys.stderr, (
[email protected]ee87f582015-07-31 18:46:25979 'ERROR: unable to determine current branch (detached HEAD?)')
[email protected]79706062015-01-14 21:18:12980 else:
981 print >> sys.stderr, (
982 'ERROR: no upstream branch')
983 return False
984
[email protected]0f58fa82012-11-05 01:45:20985 # Verify the commit we're diffing against is in our current branch.
986 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
987 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
988 if upstream_sha != common_ancestor:
989 print >> sys.stderr, (
990 'ERROR: %s is not in the current branch. You may need to rebase '
991 'your tracking branch' % upstream_sha)
992 return False
993
994 # List the commits inside the diff, and verify they are all local.
995 commits_in_diff = RunGit(
996 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
997 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
998 remote_branch = remote_branch.strip()
999 if code != 0:
1000 _, remote_branch = self.GetRemoteBranch()
1001
1002 commits_in_remote = RunGit(
1003 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1004
1005 common_commits = set(commits_in_diff) & set(commits_in_remote)
1006 if common_commits:
1007 print >> sys.stderr, (
1008 'ERROR: Your diff contains %d commits already in %s.\n'
1009 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1010 'the diff. If you are using a custom git flow, you can override'
1011 ' the reference used for this check with "git config '
1012 'gitcl.remotebranch <git-ref>".' % (
1013 len(common_commits), remote_branch, upstream_git_obj))
1014 return False
1015 return True
1016
[email protected]6b0051e2012-04-03 15:45:081017 def GetGitBaseUrlFromConfig(self):
[email protected]a656e702014-05-15 20:43:051018 """Return the configured base URL from branch.<branchname>.baseurl.
[email protected]6b0051e2012-04-03 15:45:081019
1020 Returns None if it is not set.
1021 """
[email protected]a656e702014-05-15 20:43:051022 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
1023 error_ok=True).strip()
[email protected]a2cbbbb2012-03-22 20:40:401024
[email protected]6abc6522014-12-02 07:34:491025 def GetGitSvnRemoteUrl(self):
1026 """Return the configured git-svn remote URL parsed from git svn info.
1027
1028 Returns None if it is not set.
1029 """
1030 # URL is dependent on the current directory.
1031 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1032 if data:
1033 keys = dict(line.split(': ', 1) for line in data.splitlines()
1034 if ': ' in line)
1035 return keys.get('URL', None)
1036 return None
1037
[email protected]cc51cd02010-12-23 00:48:391038 def GetRemoteUrl(self):
1039 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1040
1041 Returns None if there is no remote.
1042 """
[email protected]0f58fa82012-11-05 01:45:201043 remote, _ = self.GetRemoteBranch()
[email protected]2a13d4f2014-06-13 00:06:371044 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1045
1046 # If URL is pointing to a local directory, it is probably a git cache.
1047 if os.path.isdir(url):
1048 url = RunGit(['config', 'remote.%s.url' % remote],
1049 error_ok=True,
1050 cwd=url).strip()
1051 return url
[email protected]cc51cd02010-12-23 00:48:391052
1053 def GetIssue(self):
[email protected]52424302012-08-29 15:14:301054 """Returns the issue number as a int or None if not set."""
[email protected]1033efd2013-07-23 23:25:091055 if self.issue is None and not self.lookedup_issue:
[email protected]cc51cd02010-12-23 00:48:391056 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
[email protected]1033efd2013-07-23 23:25:091057 self.issue = int(issue) or None if issue else None
1058 self.lookedup_issue = True
[email protected]cc51cd02010-12-23 00:48:391059 return self.issue
1060
1061 def GetRietveldServer(self):
[email protected]0af9b702012-02-11 00:42:161062 if not self.rietveld_server:
1063 # If we're on a branch then get the server potentially associated
1064 # with that branch.
1065 if self.GetIssue():
[email protected]d62c61f2014-10-20 22:33:211066 rietveld_server_config = self._RietveldServer()
1067 if rietveld_server_config:
1068 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
1069 ['config', rietveld_server_config], error_ok=True).strip())
[email protected]0af9b702012-02-11 00:42:161070 if not self.rietveld_server:
1071 self.rietveld_server = settings.GetDefaultServerUrl()
[email protected]cc51cd02010-12-23 00:48:391072 return self.rietveld_server
1073
1074 def GetIssueURL(self):
1075 """Get the URL for a particular issue."""
[email protected]015fd3d2013-06-18 19:02:501076 if not self.GetIssue():
1077 return None
[email protected]cc51cd02010-12-23 00:48:391078 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
1079
1080 def GetDescription(self, pretty=False):
1081 if not self.has_description:
1082 if self.GetIssue():
[email protected]52424302012-08-29 15:14:301083 issue = self.GetIssue()
[email protected]183df1a2012-01-04 19:44:551084 try:
1085 self.description = self.RpcServer().get_description(issue).strip()
[email protected]85616e02014-07-28 15:37:551086 except urllib2.HTTPError as e:
[email protected]183df1a2012-01-04 19:44:551087 if e.code == 404:
1088 DieWithError(
1089 ('\nWhile fetching the description for issue %d, received a '
1090 '404 (not found)\n'
1091 'error. It is likely that you deleted this '
1092 'issue on the server. If this is the\n'
1093 'case, please run\n\n'
1094 ' git cl issue 0\n\n'
1095 'to clear the association with the deleted issue. Then run '
1096 'this command again.') % issue)
1097 else:
1098 DieWithError(
[email protected]daee1d32013-12-18 11:55:031099 '\nFailed to fetch issue description. HTTP error %d' % e.code)
[email protected]85616e02014-07-28 15:37:551100 except urllib2.URLError as e:
1101 print >> sys.stderr, (
1102 'Warning: Failed to retrieve CL description due to network '
1103 'failure.')
1104 self.description = ''
1105
[email protected]cc51cd02010-12-23 00:48:391106 self.has_description = True
1107 if pretty:
1108 wrapper = textwrap.TextWrapper()
1109 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1110 return wrapper.fill(self.description)
1111 return self.description
1112
1113 def GetPatchset(self):
[email protected]52424302012-08-29 15:14:301114 """Returns the patchset number as a int or None if not set."""
[email protected]1033efd2013-07-23 23:25:091115 if self.patchset is None and not self.lookedup_patchset:
[email protected]cc51cd02010-12-23 00:48:391116 patchset = RunGit(['config', self._PatchsetSetting()],
1117 error_ok=True).strip()
[email protected]1033efd2013-07-23 23:25:091118 self.patchset = int(patchset) or None if patchset else None
1119 self.lookedup_patchset = True
[email protected]cc51cd02010-12-23 00:48:391120 return self.patchset
1121
1122 def SetPatchset(self, patchset):
1123 """Set this branch's patchset. If patchset=0, clears the patchset."""
1124 if patchset:
1125 RunGit(['config', self._PatchsetSetting(), str(patchset)])
[email protected]1033efd2013-07-23 23:25:091126 self.patchset = patchset
[email protected]cc51cd02010-12-23 00:48:391127 else:
1128 RunGit(['config', '--unset', self._PatchsetSetting()],
[email protected]32f9f5e2011-09-14 13:41:471129 stderr=subprocess2.PIPE, error_ok=True)
[email protected]1033efd2013-07-23 23:25:091130 self.patchset = None
[email protected]cc51cd02010-12-23 00:48:391131
[email protected]1033efd2013-07-23 23:25:091132 def GetMostRecentPatchset(self):
1133 return self.GetIssueProperties()['patchsets'][-1]
[email protected]0281f522012-09-14 13:37:591134
1135 def GetPatchSetDiff(self, issue, patchset):
[email protected]27bb3872011-05-30 20:33:191136 return self.RpcServer().get(
[email protected]e77ebbf2011-03-29 20:35:381137 '/download/issue%s_%s.diff' % (issue, patchset))
1138
[email protected]1033efd2013-07-23 23:25:091139 def GetIssueProperties(self):
1140 if self._props is None:
1141 issue = self.GetIssue()
1142 if not issue:
1143 self._props = {}
1144 else:
1145 self._props = self.RpcServer().get_issue_properties(issue, True)
1146 return self._props
1147
[email protected]cf087782013-07-23 13:08:481148 def GetApprovingReviewers(self):
[email protected]1033efd2013-07-23 23:25:091149 return get_approving_reviewers(self.GetIssueProperties())
[email protected]e52678e2013-04-26 18:34:441150
[email protected]e4efd512014-11-05 09:05:291151 def AddComment(self, message):
1152 return self.RpcServer().add_comment(self.GetIssue(), message)
1153
[email protected]cc51cd02010-12-23 00:48:391154 def SetIssue(self, issue):
1155 """Set this branch's issue. If issue=0, clears the issue."""
1156 if issue:
[email protected]1033efd2013-07-23 23:25:091157 self.issue = issue
[email protected]cc51cd02010-12-23 00:48:391158 RunGit(['config', self._IssueSetting(), str(issue)])
1159 if self.rietveld_server:
1160 RunGit(['config', self._RietveldServer(), self.rietveld_server])
1161 else:
[email protected]d79d4b82013-10-23 20:09:081162 current_issue = self.GetIssue()
1163 if current_issue:
1164 RunGit(['config', '--unset', self._IssueSetting()])
[email protected]1033efd2013-07-23 23:25:091165 self.issue = None
1166 self.SetPatchset(None)
[email protected]cc51cd02010-12-23 00:48:391167
[email protected]15169952011-09-27 14:30:531168 def GetChange(self, upstream_branch, author):
[email protected]0f58fa82012-11-05 01:45:201169 if not self.GitSanityChecks(upstream_branch):
1170 DieWithError('\nGit sanity check failure')
1171
[email protected]8b0553c2014-02-11 00:33:371172 root = settings.GetRelativeRoot()
[email protected]f267b0e2013-05-02 09:11:431173 if not root:
1174 root = '.'
[email protected]512f1ef2011-04-20 15:17:571175 absroot = os.path.abspath(root)
[email protected]6fb99c62011-04-18 15:57:281176
1177 # We use the sha1 of HEAD as a name of this change.
[email protected]8b0553c2014-02-11 00:33:371178 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
[email protected]512f1ef2011-04-20 15:17:571179 # Need to pass a relative path for msysgit.
[email protected]2b38e9c2011-10-19 00:04:351180 try:
[email protected]80a9ef12011-12-13 20:44:101181 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
[email protected]2b38e9c2011-10-19 00:04:351182 except subprocess2.CalledProcessError:
1183 DieWithError(
[email protected]d6617f32013-11-19 00:34:541184 ('\nFailed to diff against upstream branch %s\n\n'
[email protected]2b38e9c2011-10-19 00:04:351185 'This branch probably doesn\'t exist anymore. To reset the\n'
1186 'tracking branch, please run\n'
1187 ' git branch --set-upstream %s trunk\n'
1188 'replacing trunk with origin/master or the relevant branch') %
1189 (upstream_branch, self.GetBranch()))
[email protected]6fb99c62011-04-18 15:57:281190
[email protected]52424302012-08-29 15:14:301191 issue = self.GetIssue()
1192 patchset = self.GetPatchset()
[email protected]6fb99c62011-04-18 15:57:281193 if issue:
1194 description = self.GetDescription()
1195 else:
1196 # If the change was never uploaded, use the log messages of all commits
1197 # up to the branch point, as git cl upload will prefill the description
1198 # with these log messages.
[email protected]8b0553c2014-02-11 00:33:371199 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1200 description = RunGitWithCode(args)[1].strip()
[email protected]03b3bdc2011-06-14 13:04:121201
1202 if not author:
[email protected]13f623c2011-07-22 16:02:231203 author = RunGit(['config', 'user.email']).strip() or None
[email protected]15169952011-09-27 14:30:531204 return presubmit_support.GitChange(
[email protected]6fb99c62011-04-18 15:57:281205 name,
1206 description,
1207 absroot,
1208 files,
1209 issue,
1210 patchset,
[email protected]ea84ef12014-04-30 19:55:121211 author,
1212 upstream=upstream_branch)
[email protected]6fb99c62011-04-18 15:57:281213
[email protected]b99fbd92014-09-11 17:29:281214 def GetStatus(self):
1215 """Apply a rough heuristic to give a simple summary of an issue's review
1216 or CQ status, assuming adherence to a common workflow.
1217
1218 Returns None if no issue for this branch, or one of the following keywords:
1219 * 'error' - error from review tool (including deleted issues)
1220 * 'unsent' - not sent for review
1221 * 'waiting' - waiting for review
1222 * 'reply' - waiting for owner to reply to review
1223 * 'lgtm' - LGTM from at least one approved reviewer
1224 * 'commit' - in the commit queue
1225 * 'closed' - closed
1226 """
1227 if not self.GetIssue():
1228 return None
1229
1230 try:
1231 props = self.GetIssueProperties()
1232 except urllib2.HTTPError:
1233 return 'error'
1234
1235 if props.get('closed'):
1236 # Issue is closed.
1237 return 'closed'
[email protected]b4f6a222016-03-03 01:11:041238 if props.get('commit') and not props.get('cq_dry_run', False):
[email protected]b99fbd92014-09-11 17:29:281239 # Issue is in the commit queue.
1240 return 'commit'
1241
1242 try:
1243 reviewers = self.GetApprovingReviewers()
1244 except urllib2.HTTPError:
1245 return 'error'
1246
1247 if reviewers:
1248 # Was LGTM'ed.
1249 return 'lgtm'
1250
1251 messages = props.get('messages') or []
1252
1253 if not messages:
1254 # No message was sent.
1255 return 'unsent'
1256 if messages[-1]['sender'] != props.get('owner_email'):
1257 # Non-LGTM reply from non-owner
1258 return 'reply'
1259 return 'waiting'
1260
[email protected]051ad0e2013-03-04 21:57:341261 def RunHook(self, committing, may_prompt, verbose, change):
[email protected]15169952011-09-27 14:30:531262 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
[email protected]6fb99c62011-04-18 15:57:281263
1264 try:
[email protected]b0a63912012-01-17 18:10:161265 return presubmit_support.DoPresubmitChecks(change, committing,
[email protected]6fb99c62011-04-18 15:57:281266 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
[email protected]cc73ad62011-07-06 17:39:261267 default_presubmit=None, may_prompt=may_prompt,
[email protected]239f4112011-06-03 20:08:231268 rietveld_obj=self.RpcServer())
[email protected]6fb99c62011-04-18 15:57:281269 except presubmit_support.PresubmitFailure, e:
1270 DieWithError(
1271 ('%s\nMaybe your depot_tools is out of date?\n'
1272 'If all fails, contact maruel@') % e)
1273
[email protected]b021b322013-04-08 17:57:291274 def UpdateDescription(self, description):
1275 self.description = description
1276 return self.RpcServer().update_description(
1277 self.GetIssue(), self.description)
1278
[email protected]cc51cd02010-12-23 00:48:391279 def CloseIssue(self):
[email protected]607bb1b2011-06-01 23:43:111280 """Updates the description and closes the issue."""
[email protected]b021b322013-04-08 17:57:291281 return self.RpcServer().close_issue(self.GetIssue())
[email protected]cc51cd02010-12-23 00:48:391282
[email protected]27bb3872011-05-30 20:33:191283 def SetFlag(self, flag, value):
1284 """Patchset must match."""
1285 if not self.GetPatchset():
1286 DieWithError('The patchset needs to match. Send another patchset.')
1287 try:
1288 return self.RpcServer().set_flag(
[email protected]52424302012-08-29 15:14:301289 self.GetIssue(), self.GetPatchset(), flag, value)
[email protected]27bb3872011-05-30 20:33:191290 except urllib2.HTTPError, e:
1291 if e.code == 404:
1292 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1293 if e.code == 403:
1294 DieWithError(
1295 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1296 'match?') % (self.GetIssue(), self.GetPatchset()))
1297 raise
[email protected]cc51cd02010-12-23 00:48:391298
[email protected]cab38e92011-04-09 00:30:511299 def RpcServer(self):
[email protected]cc51cd02010-12-23 00:48:391300 """Returns an upload.RpcServer() to access this review's rietveld instance.
1301 """
[email protected]e77ebbf2011-03-29 20:35:381302 if not self._rpc_server:
[email protected]4bac4b52012-11-27 20:33:521303 self._rpc_server = rietveld.CachingRietveld(
[email protected]cf6a5d22015-04-09 22:02:001304 self.GetRietveldServer(),
1305 self._auth_config or auth.make_auth_config())
[email protected]e77ebbf2011-03-29 20:35:381306 return self._rpc_server
[email protected]cc51cd02010-12-23 00:48:391307
1308 def _IssueSetting(self):
1309 """Return the git setting that stores this change's issue."""
1310 return 'branch.%s.rietveldissue' % self.GetBranch()
1311
1312 def _PatchsetSetting(self):
1313 """Return the git setting that stores this change's most recent patchset."""
1314 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1315
1316 def _RietveldServer(self):
1317 """Returns the git setting that stores this change's rietveld server."""
[email protected]d62c61f2014-10-20 22:33:211318 branch = self.GetBranch()
1319 if branch:
1320 return 'branch.%s.rietveldserver' % branch
1321 return None
[email protected]cc51cd02010-12-23 00:48:391322
1323
1324def GetCodereviewSettingsInteractively():
1325 """Prompt the user for settings."""
[email protected]e8077812012-02-03 03:41:461326 # TODO(ukai): ask code review system is rietveld or gerrit?
[email protected]cc51cd02010-12-23 00:48:391327 server = settings.GetDefaultServerUrl(error_ok=True)
1328 prompt = 'Rietveld server (host[:port])'
1329 prompt += ' [%s]' % (server or DEFAULT_SERVER)
[email protected]90541732011-04-01 17:54:181330 newserver = ask_for_data(prompt + ':')
[email protected]cc51cd02010-12-23 00:48:391331 if not server and not newserver:
1332 newserver = DEFAULT_SERVER
[email protected]eb5edbc2012-01-16 17:03:281333 if newserver:
1334 newserver = gclient_utils.UpgradeToHttps(newserver)
1335 if newserver != server:
1336 RunGit(['config', 'rietveld.server', newserver])
[email protected]cc51cd02010-12-23 00:48:391337
[email protected]eb5edbc2012-01-16 17:03:281338 def SetProperty(initial, caption, name, is_url):
[email protected]cc51cd02010-12-23 00:48:391339 prompt = caption
1340 if initial:
1341 prompt += ' ("x" to clear) [%s]' % initial
[email protected]90541732011-04-01 17:54:181342 new_val = ask_for_data(prompt + ':')
[email protected]cc51cd02010-12-23 00:48:391343 if new_val == 'x':
1344 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
[email protected]eb5edbc2012-01-16 17:03:281345 elif new_val:
1346 if is_url:
1347 new_val = gclient_utils.UpgradeToHttps(new_val)
1348 if new_val != initial:
1349 RunGit(['config', 'rietveld.' + name, new_val])
[email protected]cc51cd02010-12-23 00:48:391350
[email protected]eb5edbc2012-01-16 17:03:281351 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
[email protected]c1737d02013-05-29 14:17:281352 SetProperty(settings.GetDefaultPrivateFlag(),
1353 'Private flag (rietveld only)', 'private', False)
[email protected]cc51cd02010-12-23 00:48:391354 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
[email protected]eb5edbc2012-01-16 17:03:281355 'tree-status-url', False)
1356 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
[email protected]90752582014-01-14 21:04:501357 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
[email protected]5626a922015-02-26 14:03:301358 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1359 'run-post-upload-hook', False)
[email protected]cc51cd02010-12-23 00:48:391360
1361 # TODO: configure a default branch to diff against, rather than this
1362 # svn-based hackery.
1363
1364
[email protected]20254fc2011-03-22 18:28:591365class ChangeDescription(object):
1366 """Contains a parsed form of the change description."""
[email protected]c6f60e82013-04-19 17:01:571367 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
[email protected]42c20792013-09-12 17:34:491368 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
[email protected]20254fc2011-03-22 18:28:591369
[email protected]78936cb2013-04-11 00:17:521370 def __init__(self, description):
[email protected]42c20792013-09-12 17:34:491371 self._description_lines = (description or '').strip().splitlines()
[email protected]78936cb2013-04-11 00:17:521372
[email protected]42c20792013-09-12 17:34:491373 @property # www.logilab.org/ticket/89786
1374 def description(self): # pylint: disable=E0202
1375 return '\n'.join(self._description_lines)
1376
1377 def set_description(self, desc):
1378 if isinstance(desc, basestring):
1379 lines = desc.splitlines()
1380 else:
1381 lines = [line.rstrip() for line in desc]
1382 while lines and not lines[0]:
1383 lines.pop(0)
1384 while lines and not lines[-1]:
1385 lines.pop(-1)
1386 self._description_lines = lines
[email protected]78936cb2013-04-11 00:17:521387
[email protected]336f9122014-09-04 02:16:551388 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
[email protected]42c20792013-09-12 17:34:491389 """Rewrites the R=/TBR= line(s) as a single line each."""
[email protected]78936cb2013-04-11 00:17:521390 assert isinstance(reviewers, list), reviewers
[email protected]336f9122014-09-04 02:16:551391 if not reviewers and not add_owners_tbr:
[email protected]78936cb2013-04-11 00:17:521392 return
[email protected]42c20792013-09-12 17:34:491393 reviewers = reviewers[:]
[email protected]78936cb2013-04-11 00:17:521394
[email protected]42c20792013-09-12 17:34:491395 # Get the set of R= and TBR= lines and remove them from the desciption.
1396 regexp = re.compile(self.R_LINE)
1397 matches = [regexp.match(line) for line in self._description_lines]
1398 new_desc = [l for i, l in enumerate(self._description_lines)
1399 if not matches[i]]
1400 self.set_description(new_desc)
[email protected]78936cb2013-04-11 00:17:521401
[email protected]42c20792013-09-12 17:34:491402 # Construct new unified R= and TBR= lines.
1403 r_names = []
1404 tbr_names = []
1405 for match in matches:
1406 if not match:
1407 continue
1408 people = cleanup_list([match.group(2).strip()])
1409 if match.group(1) == 'TBR':
1410 tbr_names.extend(people)
1411 else:
1412 r_names.extend(people)
1413 for name in r_names:
1414 if name not in reviewers:
1415 reviewers.append(name)
[email protected]336f9122014-09-04 02:16:551416 if add_owners_tbr:
1417 owners_db = owners.Database(change.RepositoryRoot(),
1418 fopen=file, os_path=os.path, glob=glob.glob)
1419 all_reviewers = set(tbr_names + reviewers)
1420 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1421 all_reviewers)
1422 tbr_names.extend(owners_db.reviewers_for(missing_files,
1423 change.author_email))
[email protected]42c20792013-09-12 17:34:491424 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1425 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1426
1427 # Put the new lines in the description where the old first R= line was.
1428 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1429 if 0 <= line_loc < len(self._description_lines):
1430 if new_tbr_line:
1431 self._description_lines.insert(line_loc, new_tbr_line)
1432 if new_r_line:
1433 self._description_lines.insert(line_loc, new_r_line)
[email protected]78936cb2013-04-11 00:17:521434 else:
[email protected]42c20792013-09-12 17:34:491435 if new_r_line:
1436 self.append_footer(new_r_line)
1437 if new_tbr_line:
1438 self.append_footer(new_tbr_line)
[email protected]78936cb2013-04-11 00:17:521439
1440 def prompt(self):
1441 """Asks the user to update the description."""
[email protected]42c20792013-09-12 17:34:491442 self.set_description([
1443 '# Enter a description of the change.',
1444 '# This will be displayed on the codereview site.',
1445 '# The first line will also be used as the subject of the review.',
[email protected]bd1073e2013-06-01 00:34:381446 '#--------------------This line is 72 characters long'
[email protected]42c20792013-09-12 17:34:491447 '--------------------',
1448 ] + self._description_lines)
[email protected]78936cb2013-04-11 00:17:521449
[email protected]42c20792013-09-12 17:34:491450 regexp = re.compile(self.BUG_LINE)
1451 if not any((regexp.match(line) for line in self._description_lines)):
[email protected]90752582014-01-14 21:04:501452 self.append_footer('BUG=%s' % settings.GetBugPrefix())
[email protected]42c20792013-09-12 17:34:491453 content = gclient_utils.RunEditor(self.description, True,
[email protected]615a2622013-05-03 13:20:141454 git_editor=settings.GetGitEditor())
[email protected]0e0436a2011-10-25 13:32:411455 if not content:
1456 DieWithError('Running editor failed')
[email protected]42c20792013-09-12 17:34:491457 lines = content.splitlines()
[email protected]78936cb2013-04-11 00:17:521458
1459 # Strip off comments.
[email protected]42c20792013-09-12 17:34:491460 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1461 if not clean_lines:
[email protected]0e0436a2011-10-25 13:32:411462 DieWithError('No CL description, aborting')
[email protected]42c20792013-09-12 17:34:491463 self.set_description(clean_lines)
[email protected]20254fc2011-03-22 18:28:591464
[email protected]78936cb2013-04-11 00:17:521465 def append_footer(self, line):
[email protected]42c20792013-09-12 17:34:491466 if self._description_lines:
1467 # Add an empty line if either the last line or the new line isn't a tag.
1468 last_line = self._description_lines[-1]
1469 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1470 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1471 self._description_lines.append('')
1472 self._description_lines.append(line)
[email protected]20254fc2011-03-22 18:28:591473
[email protected]78936cb2013-04-11 00:17:521474 def get_reviewers(self):
1475 """Retrieves the list of reviewers."""
[email protected]42c20792013-09-12 17:34:491476 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1477 reviewers = [match.group(2).strip() for match in matches if match]
[email protected]78936cb2013-04-11 00:17:521478 return cleanup_list(reviewers)
[email protected]20254fc2011-03-22 18:28:591479
1480
[email protected]e52678e2013-04-26 18:34:441481def get_approving_reviewers(props):
1482 """Retrieves the reviewers that approved a CL from the issue properties with
1483 messages.
1484
1485 Note that the list may contain reviewers that are not committer, thus are not
1486 considered by the CQ.
1487 """
1488 return sorted(
1489 set(
1490 message['sender']
1491 for message in props['messages']
1492 if message['approval'] and message['sender'] in props['reviewers']
1493 )
1494 )
1495
1496
[email protected]cc51cd02010-12-23 00:48:391497def FindCodereviewSettingsFile(filename='codereview.settings'):
1498 """Finds the given file starting in the cwd and going up.
1499
1500 Only looks up to the top of the repository unless an
1501 'inherit-review-settings-ok' file exists in the root of the repository.
1502 """
1503 inherit_ok_file = 'inherit-review-settings-ok'
1504 cwd = os.getcwd()
[email protected]8b0553c2014-02-11 00:33:371505 root = settings.GetRoot()
[email protected]cc51cd02010-12-23 00:48:391506 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1507 root = '/'
1508 while True:
1509 if filename in os.listdir(cwd):
1510 if os.path.isfile(os.path.join(cwd, filename)):
1511 return open(os.path.join(cwd, filename))
1512 if cwd == root:
1513 break
1514 cwd = os.path.dirname(cwd)
1515
1516
1517def LoadCodereviewSettingsFromFile(fileobj):
1518 """Parse a codereview.settings file and updates hooks."""
[email protected]99ac1c52012-01-16 14:52:121519 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
[email protected]cc51cd02010-12-23 00:48:391520
[email protected]cc51cd02010-12-23 00:48:391521 def SetProperty(name, setting, unset_error_ok=False):
1522 fullname = 'rietveld.' + name
1523 if setting in keyvals:
1524 RunGit(['config', fullname, keyvals[setting]])
1525 else:
1526 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1527
1528 SetProperty('server', 'CODE_REVIEW_SERVER')
1529 # Only server setting is required. Other settings can be absent.
1530 # In that case, we ignore errors raised during option deletion attempt.
1531 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
[email protected]c1737d02013-05-29 14:17:281532 SetProperty('private', 'PRIVATE', unset_error_ok=True)
[email protected]cc51cd02010-12-23 00:48:391533 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1534 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
[email protected]90752582014-01-14 21:04:501535 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
[email protected]44202a22014-03-11 19:22:181536 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
[email protected]6abc6522014-12-02 07:34:491537 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1538 unset_error_ok=True)
[email protected]44202a22014-03-11 19:22:181539 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
[email protected]152cf832014-06-11 21:37:491540 SetProperty('project', 'PROJECT', unset_error_ok=True)
[email protected]566a02a2014-08-22 01:34:131541 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
[email protected]5626a922015-02-26 14:03:301542 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1543 unset_error_ok=True)
[email protected]cc51cd02010-12-23 00:48:391544
[email protected]7044efc2013-11-28 01:51:211545 if 'GERRIT_HOST' in keyvals:
[email protected]e8077812012-02-03 03:41:461546 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
[email protected]e8077812012-02-03 03:41:461547
[email protected]54b400c2016-01-14 10:08:251548 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
1549 RunGit(['config', 'gerrit.squash-uploads',
1550 keyvals['GERRIT_SQUASH_UPLOADS']])
1551
[email protected]cc51cd02010-12-23 00:48:391552 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1553 #should be of the form
1554 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1555 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1556 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1557 keyvals['ORIGIN_URL_CONFIG']])
1558
[email protected]cc51cd02010-12-23 00:48:391559
[email protected]426f69b2012-08-02 23:41:491560def urlretrieve(source, destination):
1561 """urllib is broken for SSL connections via a proxy therefore we
1562 can't use urllib.urlretrieve()."""
1563 with open(destination, 'w') as f:
1564 f.write(urllib2.urlopen(source).read())
1565
1566
[email protected]712d6102013-11-27 00:52:581567def hasSheBang(fname):
1568 """Checks fname is a #! script."""
1569 with open(fname) as f:
1570 return f.read(2).startswith('#!')
1571
1572
[email protected]78c4b982012-02-14 02:20:261573def DownloadHooks(force):
1574 """downloads hooks
1575
1576 Args:
1577 force: True to update hooks. False to install hooks if not present.
1578 """
1579 if not settings.GetIsGerrit():
1580 return
[email protected]712d6102013-11-27 00:52:581581 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
[email protected]78c4b982012-02-14 02:20:261582 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1583 if not os.access(dst, os.X_OK):
1584 if os.path.exists(dst):
1585 if not force:
1586 return
[email protected]78c4b982012-02-14 02:20:261587 try:
[email protected]426f69b2012-08-02 23:41:491588 urlretrieve(src, dst)
[email protected]712d6102013-11-27 00:52:581589 if not hasSheBang(dst):
1590 DieWithError('Not a script: %s\n'
1591 'You need to download from\n%s\n'
1592 'into .git/hooks/commit-msg and '
1593 'chmod +x .git/hooks/commit-msg' % (dst, src))
[email protected]78c4b982012-02-14 02:20:261594 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1595 except Exception:
1596 if os.path.exists(dst):
1597 os.remove(dst)
[email protected]712d6102013-11-27 00:52:581598 DieWithError('\nFailed to download hooks.\n'
1599 'You need to download from\n%s\n'
1600 'into .git/hooks/commit-msg and '
1601 'chmod +x .git/hooks/commit-msg' % src)
[email protected]78c4b982012-02-14 02:20:261602
1603
[email protected]0633fb42013-08-16 20:06:141604@subcommand.usage('[repo root containing codereview.settings]')
[email protected]cc51cd02010-12-23 00:48:391605def CMDconfig(parser, args):
[email protected]d9c1b202013-07-24 23:52:111606 """Edits configuration for this tree."""
[email protected]cc51cd02010-12-23 00:48:391607
[email protected]87884cc2014-01-03 22:23:411608 parser.add_option('--activate-update', action='store_true',
1609 help='activate auto-updating [rietveld] section in '
1610 '.git/config')
1611 parser.add_option('--deactivate-update', action='store_true',
1612 help='deactivate auto-updating [rietveld] section in '
1613 '.git/config')
1614 options, args = parser.parse_args(args)
1615
1616 if options.deactivate_update:
1617 RunGit(['config', 'rietveld.autoupdate', 'false'])
1618 return
1619
1620 if options.activate_update:
1621 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1622 return
1623
[email protected]cc51cd02010-12-23 00:48:391624 if len(args) == 0:
1625 GetCodereviewSettingsInteractively()
[email protected]78c4b982012-02-14 02:20:261626 DownloadHooks(True)
[email protected]cc51cd02010-12-23 00:48:391627 return 0
1628
1629 url = args[0]
1630 if not url.endswith('codereview.settings'):
1631 url = os.path.join(url, 'codereview.settings')
1632
1633 # Load code review settings and download hooks (if available).
1634 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
[email protected]78c4b982012-02-14 02:20:261635 DownloadHooks(True)
[email protected]cc51cd02010-12-23 00:48:391636 return 0
1637
1638
[email protected]6b0051e2012-04-03 15:45:081639def CMDbaseurl(parser, args):
[email protected]d9c1b202013-07-24 23:52:111640 """Gets or sets base-url for this branch."""
[email protected]6b0051e2012-04-03 15:45:081641 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1642 branch = ShortBranchName(branchref)
1643 _, args = parser.parse_args(args)
1644 if not args:
1645 print("Current base-url:")
1646 return RunGit(['config', 'branch.%s.base-url' % branch],
1647 error_ok=False).strip()
1648 else:
1649 print("Setting base-url to %s" % args[0])
1650 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1651 error_ok=False).strip()
1652
1653
[email protected]b99fbd92014-09-11 17:29:281654def color_for_status(status):
1655 """Maps a Changelist status to color, for CMDstatus and other tools."""
1656 return {
1657 'unsent': Fore.RED,
1658 'waiting': Fore.BLUE,
1659 'reply': Fore.YELLOW,
1660 'lgtm': Fore.GREEN,
1661 'commit': Fore.MAGENTA,
1662 'closed': Fore.CYAN,
1663 'error': Fore.WHITE,
1664 }.get(status, Fore.WHITE)
1665
[email protected]a6de1f42015-06-10 04:23:171666def fetch_cl_status(branch, auth_config=None):
1667 """Fetches information for an issue and returns (branch, issue, status)."""
1668 cl = Changelist(branchref=branch, auth_config=auth_config)
1669 url = cl.GetIssueURL()
1670 status = cl.GetStatus()
[email protected]ffde55c2015-03-12 00:44:171671
[email protected]a6de1f42015-06-10 04:23:171672 if url and (not status or status == 'error'):
[email protected]ffde55c2015-03-12 00:44:171673 # The issue probably doesn't exist anymore.
[email protected]a6de1f42015-06-10 04:23:171674 url += ' (broken)'
[email protected]ffde55c2015-03-12 00:44:171675
[email protected]a6de1f42015-06-10 04:23:171676 return (branch, url, status)
[email protected]ffde55c2015-03-12 00:44:171677
[email protected]cf6a5d22015-04-09 22:02:001678def get_cl_statuses(
1679 branches, fine_grained, max_processes=None, auth_config=None):
[email protected]ffde55c2015-03-12 00:44:171680 """Returns a blocking iterable of (branch, issue, color) for given branches.
1681
1682 If fine_grained is true, this will fetch CL statuses from the server.
1683 Otherwise, simply indicate if there's a matching url for the given branches.
1684
1685 If max_processes is specified, it is used as the maximum number of processes
1686 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1687 spawned.
1688 """
1689 # Silence upload.py otherwise it becomes unwieldly.
1690 upload.verbosity = 0
1691
1692 if fine_grained:
1693 # Process one branch synchronously to work through authentication, then
1694 # spawn processes to process all the other branches in parallel.
1695 if branches:
[email protected]cf6a5d22015-04-09 22:02:001696 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1697 yield fetch(branches[0])
[email protected]ffde55c2015-03-12 00:44:171698
1699 branches_to_fetch = branches[1:]
1700 pool = ThreadPool(
1701 min(max_processes, len(branches_to_fetch))
1702 if max_processes is not None
1703 else len(branches_to_fetch))
[email protected]cf6a5d22015-04-09 22:02:001704 for x in pool.imap_unordered(fetch, branches_to_fetch):
[email protected]ffde55c2015-03-12 00:44:171705 yield x
1706 else:
1707 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1708 for b in branches:
[email protected]a6de1f42015-06-10 04:23:171709 cl = Changelist(branchref=b, auth_config=auth_config)
1710 url = cl.GetIssueURL()
1711 yield (b, url, 'waiting' if url else 'error')
[email protected]b99fbd92014-09-11 17:29:281712
[email protected]2dd99862015-06-22 12:22:181713
1714def upload_branch_deps(cl, args):
1715 """Uploads CLs of local branches that are dependents of the current branch.
1716
1717 If the local branch dependency tree looks like:
1718 test1 -> test2.1 -> test3.1
1719 -> test3.2
1720 -> test2.2 -> test3.3
1721
1722 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1723 run on the dependent branches in this order:
1724 test2.1, test3.1, test3.2, test2.2, test3.3
1725
1726 Note: This function does not rebase your local dependent branches. Use it when
1727 you make a change to the parent branch that will not conflict with its
1728 dependent branches, and you would like their dependencies updated in
1729 Rietveld.
1730 """
1731 if git_common.is_dirty_git_tree('upload-branch-deps'):
1732 return 1
1733
1734 root_branch = cl.GetBranch()
1735 if root_branch is None:
1736 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1737 'Get on a branch!')
1738 if not cl.GetIssue() or not cl.GetPatchset():
1739 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1740 'patchset dependencies without an uploaded CL.')
1741
1742 branches = RunGit(['for-each-ref',
1743 '--format=%(refname:short) %(upstream:short)',
1744 'refs/heads'])
1745 if not branches:
1746 print('No local branches found.')
1747 return 0
1748
1749 # Create a dictionary of all local branches to the branches that are dependent
1750 # on it.
1751 tracked_to_dependents = collections.defaultdict(list)
1752 for b in branches.splitlines():
1753 tokens = b.split()
1754 if len(tokens) == 2:
1755 branch_name, tracked = tokens
1756 tracked_to_dependents[tracked].append(branch_name)
1757
1758 print
1759 print 'The dependent local branches of %s are:' % root_branch
1760 dependents = []
1761 def traverse_dependents_preorder(branch, padding=''):
1762 dependents_to_process = tracked_to_dependents.get(branch, [])
1763 padding += ' '
1764 for dependent in dependents_to_process:
1765 print '%s%s' % (padding, dependent)
1766 dependents.append(dependent)
1767 traverse_dependents_preorder(dependent, padding)
1768 traverse_dependents_preorder(root_branch)
1769 print
1770
1771 if not dependents:
1772 print 'There are no dependent local branches for %s' % root_branch
1773 return 0
1774
1775 print ('This command will checkout all dependent branches and run '
1776 '"git cl upload".')
1777 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1778
[email protected]962f9462016-02-03 20:00:421779 # Add a default patchset title to all upload calls in Rietveld.
1780 if not settings.GetIsGerrit():
1781 args.extend(['-t', 'Updated patchset dependency'])
1782
[email protected]2dd99862015-06-22 12:22:181783 # Record all dependents that failed to upload.
1784 failures = {}
1785 # Go through all dependents, checkout the branch and upload.
1786 try:
1787 for dependent_branch in dependents:
1788 print
1789 print '--------------------------------------'
1790 print 'Running "git cl upload" from %s:' % dependent_branch
1791 RunGit(['checkout', '-q', dependent_branch])
1792 print
1793 try:
1794 if CMDupload(OptionParser(), args) != 0:
1795 print 'Upload failed for %s!' % dependent_branch
1796 failures[dependent_branch] = 1
1797 except: # pylint: disable=W0702
1798 failures[dependent_branch] = 1
1799 print
1800 finally:
1801 # Swap back to the original root branch.
1802 RunGit(['checkout', '-q', root_branch])
1803
1804 print
1805 print 'Upload complete for dependent branches!'
1806 for dependent_branch in dependents:
1807 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1808 print ' %s : %s' % (dependent_branch, upload_status)
1809 print
1810
1811 return 0
1812
1813
[email protected]cc51cd02010-12-23 00:48:391814def CMDstatus(parser, args):
[email protected]d9c1b202013-07-24 23:52:111815 """Show status of changelists.
1816
1817 Colors are used to tell the state of the CL unless --fast is used:
[email protected]aeab41a2013-12-10 20:01:221818 - Red not sent for review or broken
1819 - Blue waiting for review
1820 - Yellow waiting for you to reply to review
1821 - Green LGTM'ed
1822 - Magenta in the commit queue
1823 - Cyan was committed, branch can be deleted
[email protected]d9c1b202013-07-24 23:52:111824
1825 Also see 'git cl comments'.
1826 """
[email protected]cc51cd02010-12-23 00:48:391827 parser.add_option('--field',
1828 help='print only specific field (desc|id|patch|url)')
[email protected]1033efd2013-07-23 23:25:091829 parser.add_option('-f', '--fast', action='store_true',
1830 help='Do not retrieve review status')
[email protected]ffde55c2015-03-12 00:44:171831 parser.add_option(
1832 '-j', '--maxjobs', action='store', type=int,
1833 help='The maximum number of jobs to use when retrieving review status')
[email protected]cf6a5d22015-04-09 22:02:001834
1835 auth.add_auth_options(parser)
1836 options, args = parser.parse_args(args)
[email protected]39c0b222013-08-17 16:57:011837 if args:
1838 parser.error('Unsupported args: %s' % args)
[email protected]cf6a5d22015-04-09 22:02:001839 auth_config = auth.extract_auth_config_from_options(options)
[email protected]cc51cd02010-12-23 00:48:391840
[email protected]cc51cd02010-12-23 00:48:391841 if options.field:
[email protected]cf6a5d22015-04-09 22:02:001842 cl = Changelist(auth_config=auth_config)
[email protected]cc51cd02010-12-23 00:48:391843 if options.field.startswith('desc'):
1844 print cl.GetDescription()
1845 elif options.field == 'id':
1846 issueid = cl.GetIssue()
1847 if issueid:
1848 print issueid
1849 elif options.field == 'patch':
1850 patchset = cl.GetPatchset()
1851 if patchset:
1852 print patchset
1853 elif options.field == 'url':
1854 url = cl.GetIssueURL()
1855 if url:
1856 print url
[email protected]e25c75b2013-07-23 18:30:561857 return 0
1858
1859 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1860 if not branches:
1861 print('No local branch found.')
1862 return 0
1863
[email protected]cf6a5d22015-04-09 22:02:001864 changes = (
1865 Changelist(branchref=b, auth_config=auth_config)
1866 for b in branches.splitlines())
[email protected]a4c03052014-04-25 19:06:361867 branches = [c.GetBranch() for c in changes]
[email protected]e25c75b2013-07-23 18:30:561868 alignment = max(5, max(len(b) for b in branches))
1869 print 'Branches associated with reviews:'
[email protected]ffde55c2015-03-12 00:44:171870 output = get_cl_statuses(branches,
1871 fine_grained=not options.fast,
[email protected]cf6a5d22015-04-09 22:02:001872 max_processes=options.maxjobs,
1873 auth_config=auth_config)
[email protected]1033efd2013-07-23 23:25:091874
[email protected]ffde55c2015-03-12 00:44:171875 branch_statuses = {}
[email protected]1033efd2013-07-23 23:25:091876 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
[email protected]e25c75b2013-07-23 18:30:561877 for branch in sorted(branches):
[email protected]ffde55c2015-03-12 00:44:171878 while branch not in branch_statuses:
[email protected]a6de1f42015-06-10 04:23:171879 b, i, status = output.next()
1880 branch_statuses[b] = (i, status)
1881 issue_url, status = branch_statuses.pop(branch)
1882 color = color_for_status(status)
[email protected]885f6512013-07-27 02:17:261883 reset = Fore.RESET
1884 if not sys.stdout.isatty():
1885 color = ''
1886 reset = ''
[email protected]a6de1f42015-06-10 04:23:171887 status_str = '(%s)' % status if status else ''
1888 print ' %*s : %s%s %s%s' % (
1889 alignment, ShortBranchName(branch), color, issue_url, status_str,
1890 reset)
[email protected]1033efd2013-07-23 23:25:091891
[email protected]cf6a5d22015-04-09 22:02:001892 cl = Changelist(auth_config=auth_config)
[email protected]e25c75b2013-07-23 18:30:561893 print
1894 print 'Current branch:',
[email protected]e25c75b2013-07-23 18:30:561895 print cl.GetBranch()
[email protected]ee87f582015-07-31 18:46:251896 if not cl.GetIssue():
1897 print 'No issue assigned.'
1898 return 0
[email protected]e25c75b2013-07-23 18:30:561899 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
[email protected]85616e02014-07-28 15:37:551900 if not options.fast:
1901 print 'Issue description:'
1902 print cl.GetDescription(pretty=True)
[email protected]cc51cd02010-12-23 00:48:391903 return 0
1904
1905
[email protected]39c0b222013-08-17 16:57:011906def colorize_CMDstatus_doc():
1907 """To be called once in main() to add colors to git cl status help."""
1908 colors = [i for i in dir(Fore) if i[0].isupper()]
1909
1910 def colorize_line(line):
1911 for color in colors:
1912 if color in line.upper():
1913 # Extract whitespaces first and the leading '-'.
1914 indent = len(line) - len(line.lstrip(' ')) + 1
1915 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1916 return line
1917
1918 lines = CMDstatus.__doc__.splitlines()
1919 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1920
1921
[email protected]0633fb42013-08-16 20:06:141922@subcommand.usage('[issue_number]')
[email protected]cc51cd02010-12-23 00:48:391923def CMDissue(parser, args):
[email protected]d9c1b202013-07-24 23:52:111924 """Sets or displays the current code review issue number.
[email protected]cc51cd02010-12-23 00:48:391925
1926 Pass issue number 0 to clear the current issue.
[email protected]d9c1b202013-07-24 23:52:111927 """
[email protected]406c4402015-03-03 17:22:281928 parser.add_option('-r', '--reverse', action='store_true',
1929 help='Lookup the branch(es) for the specified issues. If '
1930 'no issues are specified, all branches with mapped '
1931 'issues will be listed.')
1932 options, args = parser.parse_args(args)
[email protected]cc51cd02010-12-23 00:48:391933
[email protected]406c4402015-03-03 17:22:281934 if options.reverse:
1935 branches = RunGit(['for-each-ref', 'refs/heads',
1936 '--format=%(refname:short)']).splitlines()
1937
1938 # Reverse issue lookup.
1939 issue_branch_map = {}
1940 for branch in branches:
1941 cl = Changelist(branchref=branch)
1942 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1943 if not args:
1944 args = sorted(issue_branch_map.iterkeys())
1945 for issue in args:
1946 if not issue:
1947 continue
1948 print 'Branch for issue number %s: %s' % (
1949 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1950 else:
1951 cl = Changelist()
1952 if len(args) > 0:
1953 try:
1954 issue = int(args[0])
1955 except ValueError:
1956 DieWithError('Pass a number to set the issue or none to list it.\n'
1957 'Maybe you want to run git cl status?')
1958 cl.SetIssue(issue)
1959 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
[email protected]cc51cd02010-12-23 00:48:391960 return 0
1961
1962
[email protected]9977a2e2012-06-06 22:30:561963def CMDcomments(parser, args):
[email protected]e4efd512014-11-05 09:05:291964 """Shows or posts review comments for any changelist."""
1965 parser.add_option('-a', '--add-comment', dest='comment',
1966 help='comment to add to an issue')
1967 parser.add_option('-i', dest='issue',
1968 help="review issue id (defaults to current issue)")
[email protected]c85ac942015-09-15 16:34:431969 parser.add_option('-j', '--json-file',
1970 help='File to write JSON summary to')
[email protected]cf6a5d22015-04-09 22:02:001971 auth.add_auth_options(parser)
[email protected]e4efd512014-11-05 09:05:291972 options, args = parser.parse_args(args)
[email protected]cf6a5d22015-04-09 22:02:001973 auth_config = auth.extract_auth_config_from_options(options)
[email protected]9977a2e2012-06-06 22:30:561974
[email protected]e4efd512014-11-05 09:05:291975 issue = None
1976 if options.issue:
1977 try:
1978 issue = int(options.issue)
1979 except ValueError:
1980 DieWithError('A review issue id is expected to be a number')
1981
[email protected]cf6a5d22015-04-09 22:02:001982 cl = Changelist(issue=issue, auth_config=auth_config)
[email protected]e4efd512014-11-05 09:05:291983
1984 if options.comment:
1985 cl.AddComment(options.comment)
1986 return 0
1987
1988 data = cl.GetIssueProperties()
[email protected]c85ac942015-09-15 16:34:431989 summary = []
[email protected]5cab2d32014-11-11 18:32:411990 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
[email protected]c85ac942015-09-15 16:34:431991 summary.append({
1992 'date': message['date'],
1993 'lgtm': False,
1994 'message': message['text'],
1995 'not_lgtm': False,
1996 'sender': message['sender'],
1997 })
[email protected]e4efd512014-11-05 09:05:291998 if message['disapproval']:
1999 color = Fore.RED
[email protected]c85ac942015-09-15 16:34:432000 summary[-1]['not lgtm'] = True
[email protected]e4efd512014-11-05 09:05:292001 elif message['approval']:
2002 color = Fore.GREEN
[email protected]c85ac942015-09-15 16:34:432003 summary[-1]['lgtm'] = True
[email protected]e4efd512014-11-05 09:05:292004 elif message['sender'] == data['owner_email']:
2005 color = Fore.MAGENTA
2006 else:
2007 color = Fore.BLUE
2008 print '\n%s%s %s%s' % (
2009 color, message['date'].split('.', 1)[0], message['sender'],
2010 Fore.RESET)
2011 if message['text'].strip():
2012 print '\n'.join(' ' + l for l in message['text'].splitlines())
[email protected]c85ac942015-09-15 16:34:432013 if options.json_file:
2014 with open(options.json_file, 'wb') as f:
2015 json.dump(summary, f)
[email protected]9977a2e2012-06-06 22:30:562016 return 0
2017
2018
[email protected]eec76592013-05-20 16:27:572019def CMDdescription(parser, args):
[email protected]d9c1b202013-07-24 23:52:112020 """Brings up the editor for the current CL's description."""
[email protected]34fb6b12015-07-13 20:03:262021 parser.add_option('-d', '--display', action='store_true',
2022 help='Display the description instead of opening an editor')
[email protected]cf6a5d22015-04-09 22:02:002023 auth.add_auth_options(parser)
2024 options, _ = parser.parse_args(args)
2025 auth_config = auth.extract_auth_config_from_options(options)
2026 cl = Changelist(auth_config=auth_config)
[email protected]eec76592013-05-20 16:27:572027 if not cl.GetIssue():
2028 DieWithError('This branch has no associated changelist.')
2029 description = ChangeDescription(cl.GetDescription())
[email protected]34fb6b12015-07-13 20:03:262030 if options.display:
2031 print description.description
2032 return 0
[email protected]eec76592013-05-20 16:27:572033 description.prompt()
[email protected]063e4e52015-04-03 06:51:442034 if cl.GetDescription() != description.description:
2035 cl.UpdateDescription(description.description)
[email protected]eec76592013-05-20 16:27:572036 return 0
2037
2038
[email protected]cc51cd02010-12-23 00:48:392039def CreateDescriptionFromLog(args):
2040 """Pulls out the commit log to use as a base for the CL description."""
2041 log_args = []
2042 if len(args) == 1 and not args[0].endswith('.'):
2043 log_args = [args[0] + '..']
2044 elif len(args) == 1 and args[0].endswith('...'):
2045 log_args = [args[0][:-1]]
2046 elif len(args) == 2:
2047 log_args = [args[0] + '..' + args[1]]
2048 else:
2049 log_args = args[:] # Hope for the best!
[email protected]373af802012-05-25 21:07:332050 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
[email protected]cc51cd02010-12-23 00:48:392051
2052
[email protected]44202a22014-03-11 19:22:182053def CMDlint(parser, args):
2054 """Runs cpplint on the current changelist."""
[email protected]f204d4b2014-03-13 07:40:552055 parser.add_option('--filter', action='append', metavar='-x,+y',
2056 help='Comma-separated list of cpplint\'s category-filters')
[email protected]cf6a5d22015-04-09 22:02:002057 auth.add_auth_options(parser)
2058 options, args = parser.parse_args(args)
2059 auth_config = auth.extract_auth_config_from_options(options)
[email protected]44202a22014-03-11 19:22:182060
2061 # Access to a protected member _XX of a client class
2062 # pylint: disable=W0212
2063 try:
2064 import cpplint
2065 import cpplint_chromium
2066 except ImportError:
2067 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
2068 return 1
2069
2070 # Change the current working directory before calling lint so that it
2071 # shows the correct base.
2072 previous_cwd = os.getcwd()
2073 os.chdir(settings.GetRoot())
2074 try:
[email protected]cf6a5d22015-04-09 22:02:002075 cl = Changelist(auth_config=auth_config)
[email protected]44202a22014-03-11 19:22:182076 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
2077 files = [f.LocalPath() for f in change.AffectedFiles()]
[email protected]5839eb52014-05-30 16:20:512078 if not files:
2079 print "Cannot lint an empty CL"
2080 return 1
[email protected]44202a22014-03-11 19:22:182081
2082 # Process cpplints arguments if any.
[email protected]f204d4b2014-03-13 07:40:552083 command = args + files
2084 if options.filter:
2085 command = ['--filter=' + ','.join(options.filter)] + command
2086 filenames = cpplint.ParseArguments(command)
[email protected]44202a22014-03-11 19:22:182087
2088 white_regex = re.compile(settings.GetLintRegex())
2089 black_regex = re.compile(settings.GetLintIgnoreRegex())
2090 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
2091 for filename in filenames:
2092 if white_regex.match(filename):
2093 if black_regex.match(filename):
2094 print "Ignoring file %s" % filename
2095 else:
2096 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
2097 extra_check_functions)
2098 else:
2099 print "Skipping file %s" % filename
2100 finally:
2101 os.chdir(previous_cwd)
2102 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
2103 if cpplint._cpplint_state.error_count != 0:
2104 return 1
2105 return 0
2106
2107
[email protected]cc51cd02010-12-23 00:48:392108def CMDpresubmit(parser, args):
[email protected]d9c1b202013-07-24 23:52:112109 """Runs presubmit tests on the current changelist."""
[email protected]375a9022013-01-07 01:12:052110 parser.add_option('-u', '--upload', action='store_true',
[email protected]cc51cd02010-12-23 00:48:392111 help='Run upload hook instead of the push/dcommit hook')
[email protected]375a9022013-01-07 01:12:052112 parser.add_option('-f', '--force', action='store_true',
[email protected]495ad152012-09-04 23:07:422113 help='Run checks even if tree is dirty')
[email protected]cf6a5d22015-04-09 22:02:002114 auth.add_auth_options(parser)
2115 options, args = parser.parse_args(args)
2116 auth_config = auth.extract_auth_config_from_options(options)
[email protected]cc51cd02010-12-23 00:48:392117
[email protected]71437c02015-04-09 19:29:402118 if not options.force and git_common.is_dirty_git_tree('presubmit'):
[email protected]259e4682012-10-25 07:36:332119 print 'use --force to check even if tree is dirty.'
[email protected]cc51cd02010-12-23 00:48:392120 return 1
2121
[email protected]cf6a5d22015-04-09 22:02:002122 cl = Changelist(auth_config=auth_config)
[email protected]cc51cd02010-12-23 00:48:392123 if args:
2124 base_branch = args[0]
2125 else:
[email protected]0f58fa82012-11-05 01:45:202126 # Default to diffing against the common ancestor of the upstream branch.
[email protected]8b0553c2014-02-11 00:33:372127 base_branch = cl.GetCommonAncestorWithUpstream()
[email protected]cc51cd02010-12-23 00:48:392128
[email protected]051ad0e2013-03-04 21:57:342129 cl.RunHook(
2130 committing=not options.upload,
2131 may_prompt=False,
2132 verbose=options.verbose,
2133 change=cl.GetChange(base_branch, None))
[email protected]0a2bb372011-03-25 01:16:222134 return 0
[email protected]cc51cd02010-12-23 00:48:392135
2136
[email protected]aebe87f2012-10-22 20:34:212137def AddChangeIdToCommitMessage(options, args):
2138 """Re-commits using the current message, assumes the commit hook is in
2139 place.
2140 """
2141 log_desc = options.message or CreateDescriptionFromLog(args)
2142 git_command = ['commit', '--amend', '-m', log_desc]
2143 RunGit(git_command)
2144 new_log_desc = CreateDescriptionFromLog(args)
2145 if CHANGE_ID in new_log_desc:
2146 print 'git-cl: Added Change-Id to commit message.'
2147 else:
2148 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
2149
2150
[email protected]336f9122014-09-04 02:16:552151def GerritUpload(options, args, cl, change):
[email protected]e8077812012-02-03 03:41:462152 """upload the current branch to gerrit."""
2153 # We assume the remote called "origin" is the one we want.
2154 # It is probably not worthwhile to support different workflows.
[email protected]27386dd2015-02-16 10:45:392155 gerrit_remote = 'origin'
[email protected]609f3952015-05-04 22:47:042156
2157 remote, remote_branch = cl.GetRemoteBranch()
2158 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2159 pending_prefix='')
[email protected]cc51cd02010-12-23 00:48:392160
[email protected]78936cb2013-04-11 00:17:522161 change_desc = ChangeDescription(
2162 options.message or CreateDescriptionFromLog(args))
2163 if not change_desc.description:
[email protected]962f9462016-02-03 20:00:422164 print "\nDescription is empty. Aborting..."
2165 return 1
2166
2167 if options.title:
2168 print "\nPatch titles (-t) are not supported in Gerrit. Aborting..."
[email protected]cc51cd02010-12-23 00:48:392169 return 1
[email protected]f75c2302014-05-01 08:19:302170
[email protected]27386dd2015-02-16 10:45:392171 if options.squash:
2172 # Try to get the message from a previous upload.
2173 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
[email protected]13502e02016-02-18 10:18:292174 message = RunGitSilent(['show', '--format=%B', '-s', shadow_branch])
[email protected]27386dd2015-02-16 10:45:392175 if not message:
2176 if not options.force:
2177 change_desc.prompt()
2178
2179 if CHANGE_ID not in change_desc.description:
2180 # Run the commit-msg hook without modifying the head commit by writing
2181 # the commit message to a temporary file and running the hook over it,
2182 # then reading the file back in.
2183 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
2184 'commit-msg')
2185 file_handle, msg_file = tempfile.mkstemp(text=True,
2186 prefix='commit_msg')
[email protected]a83663a2016-01-14 16:01:002187 logging.debug("%s %s", file_handle, msg_file)
[email protected]27386dd2015-02-16 10:45:392188 try:
2189 try:
[email protected]a83663a2016-01-14 16:01:002190 try:
2191 fileobj = os.fdopen(file_handle, 'w')
2192 except OSError:
2193 # if fdopen fails, file_handle remains open.
2194 # See https://docs.python.org/2/library/os.html#os.fdopen.
2195 os.close(file_handle)
2196 raise
2197 with fileobj:
2198 # This will close the file_handle.
[email protected]27386dd2015-02-16 10:45:392199 fileobj.write(change_desc.description)
[email protected]a83663a2016-01-14 16:01:002200 logging.debug("%s %s finish editing", file_handle, msg_file)
[email protected]27386dd2015-02-16 10:45:392201 finally:
[email protected]27386dd2015-02-16 10:45:392202 RunCommand([commit_msg_hook, msg_file])
2203 change_desc.set_description(gclient_utils.FileRead(msg_file))
2204 finally:
2205 os.remove(msg_file)
2206
2207 if not change_desc.description:
2208 print "Description is empty; aborting."
2209 return 1
2210
2211 message = change_desc.description
2212
2213 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2214 if remote is '.':
2215 # If our upstream branch is local, we base our squashed commit on its
2216 # squashed version.
2217 parent = ('refs/heads/git_cl_uploads/' +
2218 scm.GIT.ShortBranchName(upstream_branch))
2219
2220 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
2221 # will create additional CLs when uploading.
2222 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2223 RunGitSilent(['rev-parse', parent + ':'])):
2224 print 'Upload upstream branch ' + upstream_branch + ' first.'
2225 return 1
2226 else:
2227 parent = cl.GetCommonAncestorWithUpstream()
2228
2229 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2230 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2231 '-m', message]).strip()
2232 else:
2233 if CHANGE_ID not in change_desc.description:
2234 AddChangeIdToCommitMessage(options, args)
2235 ref_to_push = 'HEAD'
2236 parent = '%s/%s' % (gerrit_remote, branch)
2237
2238 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2239 ref_to_push)]).splitlines()
[email protected]f75c2302014-05-01 08:19:302240 if len(commits) > 1:
2241 print('WARNING: This will upload %d commits. Run the following command '
2242 'to see which commits will be uploaded: ' % len(commits))
[email protected]27386dd2015-02-16 10:45:392243 print('git log %s..%s' % (parent, ref_to_push))
2244 print('You can also use `git squash-branch` to squash these into a single '
[email protected]f75c2302014-05-01 08:19:302245 'commit.')
2246 ask_for_data('About to upload; enter to confirm.')
2247
[email protected]336f9122014-09-04 02:16:552248 if options.reviewers or options.tbr_owners:
2249 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
[email protected]cc51cd02010-12-23 00:48:392250
[email protected]e8077812012-02-03 03:41:462251 receive_options = []
2252 cc = cl.GetCCList().split(',')
2253 if options.cc:
[email protected]eb52a5c2013-04-10 23:17:092254 cc.extend(options.cc)
[email protected]e8077812012-02-03 03:41:462255 cc = filter(None, cc)
2256 if cc:
2257 receive_options += ['--cc=' + email for email in cc]
[email protected]78936cb2013-04-11 00:17:522258 if change_desc.get_reviewers():
2259 receive_options.extend(
2260 '--reviewer=' + email for email in change_desc.get_reviewers())
[email protected]cc51cd02010-12-23 00:48:392261
[email protected]e8077812012-02-03 03:41:462262 git_command = ['push']
2263 if receive_options:
[email protected]19bbfa22012-02-03 16:18:112264 git_command.append('--receive-pack=git receive-pack %s' %
[email protected]e8077812012-02-03 03:41:462265 ' '.join(receive_options))
[email protected]27386dd2015-02-16 10:45:392266 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
[email protected]e8077812012-02-03 03:41:462267 RunGit(git_command)
[email protected]27386dd2015-02-16 10:45:392268
2269 if options.squash:
2270 head = RunGit(['rev-parse', 'HEAD']).strip()
2271 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2272
[email protected]e8077812012-02-03 03:41:462273 # TODO(ukai): parse Change-Id: and set issue number?
2274 return 0
[email protected]970c5222011-03-12 00:32:242275
[email protected]cc51cd02010-12-23 00:48:392276
[email protected]455dc922015-01-26 20:15:502277def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2278 """Computes the remote branch ref to use for the CL.
2279
2280 Args:
2281 remote (str): The git remote for the CL.
2282 remote_branch (str): The git remote branch for the CL.
2283 target_branch (str): The target branch specified by the user.
2284 pending_prefix (str): The pending prefix from the settings.
2285 """
2286 if not (remote and remote_branch):
2287 return None
[email protected]27386dd2015-02-16 10:45:392288
[email protected]455dc922015-01-26 20:15:502289 if target_branch:
2290 # Cannonicalize branch references to the equivalent local full symbolic
2291 # refs, which are then translated into the remote full symbolic refs
2292 # below.
2293 if '/' not in target_branch:
2294 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2295 else:
2296 prefix_replacements = (
2297 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2298 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2299 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2300 )
2301 match = None
2302 for regex, replacement in prefix_replacements:
2303 match = re.search(regex, target_branch)
2304 if match:
2305 remote_branch = target_branch.replace(match.group(0), replacement)
2306 break
2307 if not match:
2308 # This is a branch path but not one we recognize; use as-is.
2309 remote_branch = target_branch
[email protected]c68112d2015-03-03 12:48:062310 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2311 # Handle the refs that need to land in different refs.
2312 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
[email protected]27386dd2015-02-16 10:45:392313
[email protected]455dc922015-01-26 20:15:502314 # Create the true path to the remote branch.
2315 # Does the following translation:
2316 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2317 # * refs/remotes/origin/master -> refs/heads/master
2318 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2319 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2320 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2321 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2322 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2323 'refs/heads/')
2324 elif remote_branch.startswith('refs/remotes/branch-heads'):
2325 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2326 # If a pending prefix exists then replace refs/ with it.
2327 if pending_prefix:
2328 remote_branch = remote_branch.replace('refs/', pending_prefix)
2329 return remote_branch
2330
2331
[email protected]336f9122014-09-04 02:16:552332def RietveldUpload(options, args, cl, change):
[email protected]e8077812012-02-03 03:41:462333 """upload the patch to rietveld."""
[email protected]cc51cd02010-12-23 00:48:392334 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2335 upload_args.extend(['--server', cl.GetRietveldServer()])
[email protected]cf6a5d22015-04-09 22:02:002336 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
[email protected]cc51cd02010-12-23 00:48:392337 if options.emulate_svn_auto_props:
2338 upload_args.append('--emulate_svn_auto_props')
[email protected]cc51cd02010-12-23 00:48:392339
2340 change_desc = None
2341
[email protected]91141372014-01-09 23:27:202342 if options.email is not None:
2343 upload_args.extend(['--email', options.email])
2344
[email protected]cc51cd02010-12-23 00:48:392345 if cl.GetIssue():
[email protected]420d3b82012-05-14 18:41:382346 if options.title:
2347 upload_args.extend(['--title', options.title])
[email protected]afadfca2013-05-29 14:15:532348 if options.message:
2349 upload_args.extend(['--message', options.message])
[email protected]52424302012-08-29 15:14:302350 upload_args.extend(['--issue', str(cl.GetIssue())])
[email protected]cc51cd02010-12-23 00:48:392351 print ("This branch is associated with issue %s. "
2352 "Adding patch to that issue." % cl.GetIssue())
2353 else:
[email protected]420d3b82012-05-14 18:41:382354 if options.title:
2355 upload_args.extend(['--title', options.title])
[email protected]43e34f02013-03-25 14:52:482356 message = options.title or options.message or CreateDescriptionFromLog(args)
[email protected]78936cb2013-04-11 00:17:522357 change_desc = ChangeDescription(message)
[email protected]336f9122014-09-04 02:16:552358 if options.reviewers or options.tbr_owners:
2359 change_desc.update_reviewers(options.reviewers,
2360 options.tbr_owners,
2361 change)
[email protected]71e12a92012-02-14 02:34:152362 if not options.force:
[email protected]78936cb2013-04-11 00:17:522363 change_desc.prompt()
[email protected]970c5222011-03-12 00:32:242364
[email protected]78936cb2013-04-11 00:17:522365 if not change_desc.description:
[email protected]cc51cd02010-12-23 00:48:392366 print "Description is empty; aborting."
2367 return 1
[email protected]970c5222011-03-12 00:32:242368
[email protected]71e12a92012-02-14 02:34:152369 upload_args.extend(['--message', change_desc.description])
[email protected]78936cb2013-04-11 00:17:522370 if change_desc.get_reviewers():
2371 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
[email protected]a3353652011-11-30 14:26:572372 if options.send_mail:
[email protected]78936cb2013-04-11 00:17:522373 if not change_desc.get_reviewers():
[email protected]a3353652011-11-30 14:26:572374 DieWithError("Must specify reviewers to send email.")
2375 upload_args.append('--send_mail')
[email protected]99918ab2013-09-30 06:17:282376
2377 # We check this before applying rietveld.private assuming that in
2378 # rietveld.cc only addresses which we can send private CLs to are listed
2379 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2380 # --private is specified explicitly on the command line.
2381 if options.private:
2382 logging.warn('rietveld.cc is ignored since private flag is specified. '
2383 'You need to review and add them manually if necessary.')
2384 cc = cl.GetCCListWithoutDefault()
2385 else:
2386 cc = cl.GetCCList()
2387 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
[email protected]b2a7c332011-02-25 20:30:372388 if cc:
2389 upload_args.extend(['--cc', cc])
[email protected]cc51cd02010-12-23 00:48:392390
[email protected]c1737d02013-05-29 14:17:282391 if options.private or settings.GetDefaultPrivateFlag() == "True":
2392 upload_args.append('--private')
2393
[email protected]53937ba2012-10-02 18:20:432394 upload_args.extend(['--git_similarity', str(options.similarity)])
[email protected]79540052012-10-19 23:15:262395 if not options.find_copies:
2396 upload_args.extend(['--git_no_find_copies'])
[email protected]53937ba2012-10-02 18:20:432397
[email protected]cc51cd02010-12-23 00:48:392398 # Include the upstream repo's URL in the change -- this is useful for
2399 # projects that have their source spread across multiple repos.
[email protected]6b0051e2012-04-03 15:45:082400 remote_url = cl.GetGitBaseUrlFromConfig()
2401 if not remote_url:
2402 if settings.GetIsGitSvn():
[email protected]6abc6522014-12-02 07:34:492403 remote_url = cl.GetGitSvnRemoteUrl()
[email protected]6b0051e2012-04-03 15:45:082404 else:
[email protected]80c51ae2014-10-17 18:43:022405 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2406 remote_url = (cl.GetRemoteUrl() + '@'
2407 + cl.GetUpstreamBranch().split('/')[-1])
[email protected]cc51cd02010-12-23 00:48:392408 if remote_url:
2409 upload_args.extend(['--base_url', remote_url])
[email protected]d1e37582014-12-10 20:58:242410 remote, remote_branch = cl.GetRemoteBranch()
[email protected]455dc922015-01-26 20:15:502411 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2412 settings.GetPendingRefPrefix())
2413 if target_ref:
2414 upload_args.extend(['--target_ref', target_ref])
[email protected]cc51cd02010-12-23 00:48:392415
[email protected]d91b7e32015-06-23 11:24:072416 # Look for dependent patchsets. See crbug.com/480453 for more details.
2417 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2418 upstream_branch = ShortBranchName(upstream_branch)
2419 if remote is '.':
2420 # A local branch is being tracked.
2421 local_branch = ShortBranchName(upstream_branch)
[email protected]78948ed2015-07-08 23:09:572422 if settings.GetIsSkipDependencyUpload(local_branch):
[email protected]d91b7e32015-06-23 11:24:072423 print
[email protected]78948ed2015-07-08 23:09:572424 print ('Skipping dependency patchset upload because git config '
2425 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
[email protected]d91b7e32015-06-23 11:24:072426 print
[email protected]78948ed2015-07-08 23:09:572427 else:
2428 auth_config = auth.extract_auth_config_from_options(options)
2429 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2430 branch_cl_issue_url = branch_cl.GetIssueURL()
2431 branch_cl_issue = branch_cl.GetIssue()
2432 branch_cl_patchset = branch_cl.GetPatchset()
2433 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2434 upload_args.extend(
2435 ['--depends_on_patchset', '%s:%s' % (
2436 branch_cl_issue, branch_cl_patchset)])
2437 print
2438 print ('The current branch (%s) is tracking a local branch (%s) with '
2439 'an associated CL.') % (cl.GetBranch(), local_branch)
2440 print 'Adding %s/#ps%s as a dependency patchset.' % (
2441 branch_cl_issue_url, branch_cl_patchset)
2442 print
[email protected]d91b7e32015-06-23 11:24:072443
[email protected]152cf832014-06-11 21:37:492444 project = settings.GetProject()
2445 if project:
2446 upload_args.extend(['--project', project])
2447
[email protected]ef966222015-04-07 11:15:012448 if options.cq_dry_run:
2449 upload_args.extend(['--cq_dry_run'])
2450
[email protected]cc51cd02010-12-23 00:48:392451 try:
[email protected]82880192012-11-26 15:41:572452 upload_args = ['upload'] + upload_args + args
2453 logging.info('upload.RealMain(%s)', upload_args)
2454 issue, patchset = upload.RealMain(upload_args)
[email protected]911fce12013-07-29 23:01:132455 issue = int(issue)
2456 patchset = int(patchset)
[email protected]9ce0dff2011-04-04 17:56:502457 except KeyboardInterrupt:
2458 sys.exit(1)
[email protected]cc51cd02010-12-23 00:48:392459 except:
2460 # If we got an exception after the user typed a description for their
2461 # change, back up the description before re-raising.
2462 if change_desc:
2463 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2464 print '\nGot exception while uploading -- saving description to %s\n' \
2465 % backup_path
2466 backup_file = open(backup_path, 'w')
[email protected]970c5222011-03-12 00:32:242467 backup_file.write(change_desc.description)
[email protected]cc51cd02010-12-23 00:48:392468 backup_file.close()
2469 raise
2470
2471 if not cl.GetIssue():
2472 cl.SetIssue(issue)
2473 cl.SetPatchset(patchset)
[email protected]27bb3872011-05-30 20:33:192474
2475 if options.use_commit_queue:
2476 cl.SetFlag('commit', '1')
[email protected]cc51cd02010-12-23 00:48:392477 return 0
2478
2479
[email protected]eb52a5c2013-04-10 23:17:092480def cleanup_list(l):
2481 """Fixes a list so that comma separated items are put as individual items.
2482
2483 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2484 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2485 """
2486 items = sum((i.split(',') for i in l), [])
2487 stripped_items = (i.strip() for i in items)
2488 return sorted(filter(None, stripped_items))
2489
2490
[email protected]0633fb42013-08-16 20:06:142491@subcommand.usage('[args to "git diff"]')
[email protected]e8077812012-02-03 03:41:462492def CMDupload(parser, args):
[email protected]78948ed2015-07-08 23:09:572493 """Uploads the current changelist to codereview.
2494
2495 Can skip dependency patchset uploads for a branch by running:
2496 git config branch.branch_name.skip-deps-uploads True
2497 To unset run:
2498 git config --unset branch.branch_name.skip-deps-uploads
2499 Can also set the above globally by using the --global flag.
2500 """
[email protected]e8077812012-02-03 03:41:462501 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2502 help='bypass upload presubmit hook')
[email protected]b65c43c2013-06-10 22:04:492503 parser.add_option('--bypass-watchlists', action='store_true',
2504 dest='bypass_watchlists',
2505 help='bypass watchlists auto CC-ing reviewers')
[email protected]e8077812012-02-03 03:41:462506 parser.add_option('-f', action='store_true', dest='force',
2507 help="force yes to questions (don't prompt)")
[email protected]420d3b82012-05-14 18:41:382508 parser.add_option('-m', dest='message', help='message for patchset')
[email protected]962f9462016-02-03 20:00:422509 parser.add_option('-t', dest='title',
2510 help='title for patchset (Rietveld only)')
[email protected]e8077812012-02-03 03:41:462511 parser.add_option('-r', '--reviewers',
[email protected]eb52a5c2013-04-10 23:17:092512 action='append', default=[],
[email protected]e8077812012-02-03 03:41:462513 help='reviewer email addresses')
2514 parser.add_option('--cc',
[email protected]eb52a5c2013-04-10 23:17:092515 action='append', default=[],
[email protected]e8077812012-02-03 03:41:462516 help='cc email addresses')
[email protected]36f47302013-04-05 01:08:312517 parser.add_option('-s', '--send-mail', action='store_true',
[email protected]e8077812012-02-03 03:41:462518 help='send email to reviewer immediately')
[email protected]b9f27512014-08-08 15:52:332519 parser.add_option('--emulate_svn_auto_props',
2520 '--emulate-svn-auto-props',
2521 action="store_true",
[email protected]e8077812012-02-03 03:41:462522 dest="emulate_svn_auto_props",
2523 help="Emulate Subversion's auto properties feature.")
[email protected]e8077812012-02-03 03:41:462524 parser.add_option('-c', '--use-commit-queue', action='store_true',
2525 help='tell the commit queue to commit this patchset')
[email protected]c1737d02013-05-29 14:17:282526 parser.add_option('--private', action='store_true',
2527 help='set the review private (rietveld only)')
[email protected]8ef7ab22012-11-28 04:24:522528 parser.add_option('--target_branch',
[email protected]b9f27512014-08-08 15:52:332529 '--target-branch',
[email protected]455dc922015-01-26 20:15:502530 metavar='TARGET',
2531 help='Apply CL to remote ref TARGET. ' +
2532 'Default: remote branch head, or master')
[email protected]27386dd2015-02-16 10:45:392533 parser.add_option('--squash', action='store_true',
2534 help='Squash multiple commits into one (Gerrit only)')
[email protected]54b400c2016-01-14 10:08:252535 parser.add_option('--no-squash', action='store_true',
2536 help='Don\'t squash multiple commits into one ' +
2537 '(Gerrit only)')
[email protected]91141372014-01-09 23:27:202538 parser.add_option('--email', default=None,
2539 help='email address to use to connect to Rietveld')
[email protected]336f9122014-09-04 02:16:552540 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2541 help='add a set of OWNERS to TBR')
[email protected]d50452a2015-11-23 16:38:152542 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
2543 action='store_true',
[email protected]ef966222015-04-07 11:15:012544 help='Send the patchset to do a CQ dry run right after '
2545 'upload.')
[email protected]2dd99862015-06-22 12:22:182546 parser.add_option('--dependencies', action='store_true',
2547 help='Uploads CLs of all the local branches that depend on '
2548 'the current branch')
[email protected]91141372014-01-09 23:27:202549
[email protected]2dd99862015-06-22 12:22:182550 orig_args = args
[email protected]53937ba2012-10-02 18:20:432551 add_git_similarity(parser)
[email protected]cf6a5d22015-04-09 22:02:002552 auth.add_auth_options(parser)
[email protected]e8077812012-02-03 03:41:462553 (options, args) = parser.parse_args(args)
[email protected]cf6a5d22015-04-09 22:02:002554 auth_config = auth.extract_auth_config_from_options(options)
[email protected]e8077812012-02-03 03:41:462555
[email protected]71437c02015-04-09 19:29:402556 if git_common.is_dirty_git_tree('upload'):
[email protected]e8077812012-02-03 03:41:462557 return 1
2558
[email protected]eb52a5c2013-04-10 23:17:092559 options.reviewers = cleanup_list(options.reviewers)
2560 options.cc = cleanup_list(options.cc)
2561
[email protected]cf6a5d22015-04-09 22:02:002562 cl = Changelist(auth_config=auth_config)
[email protected]e8077812012-02-03 03:41:462563 if args:
2564 # TODO(ukai): is it ok for gerrit case?
2565 base_branch = args[0]
2566 else:
[email protected]64e14362015-01-07 00:29:292567 if cl.GetBranch() is None:
2568 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2569
[email protected]0f58fa82012-11-05 01:45:202570 # Default to diffing against common ancestor of upstream branch
[email protected]8b0553c2014-02-11 00:33:372571 base_branch = cl.GetCommonAncestorWithUpstream()
[email protected]5e07e062013-02-28 23:55:442572 args = [base_branch, 'HEAD']
[email protected]e8077812012-02-03 03:41:462573
[email protected]eed4df32015-04-10 21:30:202574 # Make sure authenticated to Rietveld before running expensive hooks. It is
2575 # a fast, best efforts check. Rietveld still can reject the authentication
2576 # during the actual upload.
2577 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2578 authenticator = auth.get_authenticator_for_host(
2579 cl.GetRietveldServer(), auth_config)
2580 if not authenticator.has_cached_credentials():
2581 raise auth.LoginRequiredError(cl.GetRietveldServer())
2582
[email protected]051ad0e2013-03-04 21:57:342583 # Apply watchlists on upload.
2584 change = cl.GetChange(base_branch, None)
2585 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2586 files = [f.LocalPath() for f in change.AffectedFiles()]
[email protected]b65c43c2013-06-10 22:04:492587 if not options.bypass_watchlists:
2588 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
[email protected]051ad0e2013-03-04 21:57:342589
[email protected]e8077812012-02-03 03:41:462590 if not options.bypass_hooks:
[email protected]336f9122014-09-04 02:16:552591 if options.reviewers or options.tbr_owners:
[email protected]b5cded62014-03-25 17:47:572592 # Set the reviewer list now so that presubmit checks can access it.
2593 change_description = ChangeDescription(change.FullDescriptionText())
[email protected]336f9122014-09-04 02:16:552594 change_description.update_reviewers(options.reviewers,
2595 options.tbr_owners,
2596 change)
[email protected]b5cded62014-03-25 17:47:572597 change.SetDescriptionText(change_description.description)
[email protected]051ad0e2013-03-04 21:57:342598 hook_results = cl.RunHook(committing=False,
[email protected]e8077812012-02-03 03:41:462599 may_prompt=not options.force,
2600 verbose=options.verbose,
[email protected]051ad0e2013-03-04 21:57:342601 change=change)
[email protected]e8077812012-02-03 03:41:462602 if not hook_results.should_continue():
2603 return 1
2604 if not options.reviewers and hook_results.reviewers:
[email protected]eb52a5c2013-04-10 23:17:092605 options.reviewers = hook_results.reviewers.split(',')
[email protected]e8077812012-02-03 03:41:462606
[email protected]5974d7a2013-04-02 20:50:372607 if cl.GetIssue():
[email protected]1033efd2013-07-23 23:25:092608 latest_patchset = cl.GetMostRecentPatchset()
[email protected]5974d7a2013-04-02 20:50:372609 local_patchset = cl.GetPatchset()
[email protected]07d149f2013-04-03 11:40:232610 if latest_patchset and local_patchset and local_patchset != latest_patchset:
[email protected]5974d7a2013-04-02 20:50:372611 print ('The last upload made from this repository was patchset #%d but '
2612 'the most recent patchset on the server is #%d.'
2613 % (local_patchset, latest_patchset))
[email protected]c7192782013-04-09 23:28:462614 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2615 'from another machine or branch the patch you\'re uploading now '
2616 'might not include those changes.')
[email protected]5974d7a2013-04-02 20:50:372617 ask_for_data('About to upload; enter to confirm.')
2618
[email protected]79540052012-10-19 23:15:262619 print_stats(options.similarity, options.find_copies, args)
[email protected]e8077812012-02-03 03:41:462620 if settings.GetIsGerrit():
[email protected]54b400c2016-01-14 10:08:252621 if options.squash and options.no_squash:
2622 DieWithError('Can only use one of --squash or --no-squash')
2623
2624 options.squash = ((settings.GetSquashGerritUploads() or options.squash) and
2625 not options.no_squash)
2626
[email protected]1e67bb72016-02-11 12:15:492627 ret = GerritUpload(options, args, cl, change)
2628 else:
2629 ret = RietveldUpload(options, args, cl, change)
[email protected]caa16552013-03-18 20:45:052630 if not ret:
[email protected]4a6cd042013-04-12 15:40:422631 git_set_branch_value('last-upload-hash',
2632 RunGit(['rev-parse', 'HEAD']).strip())
[email protected]5626a922015-02-26 14:03:302633 # Run post upload hooks, if specified.
2634 if settings.GetRunPostUploadHook():
2635 presubmit_support.DoPostUploadExecuter(
2636 change,
2637 cl,
2638 settings.GetRoot(),
2639 options.verbose,
2640 sys.stdout)
[email protected]caa16552013-03-18 20:45:052641
[email protected]2dd99862015-06-22 12:22:182642 # Upload all dependencies if specified.
2643 if options.dependencies:
2644 print
2645 print '--dependencies has been specified.'
2646 print 'All dependent local branches will be re-uploaded.'
2647 print
2648 # Remove the dependencies flag from args so that we do not end up in a
2649 # loop.
2650 orig_args.remove('--dependencies')
2651 upload_branch_deps(cl, orig_args)
[email protected]caa16552013-03-18 20:45:052652 return ret
[email protected]e8077812012-02-03 03:41:462653
2654
[email protected]9bb85e22012-06-13 20:28:232655def IsSubmoduleMergeCommit(ref):
2656 # When submodules are added to the repo, we expect there to be a single
2657 # non-git-svn merge commit at remote HEAD with a signature comment.
2658 pattern = '^SVN changes up to revision [0-9]*$'
[email protected]e84b7542012-06-15 21:26:582659 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
[email protected]9bb85e22012-06-13 20:28:232660 return RunGit(cmd) != ''
2661
2662
[email protected]cc51cd02010-12-23 00:48:392663def SendUpstream(parser, args, cmd):
[email protected]cee6dc42014-05-07 17:04:032664 """Common code for CMDland and CmdDCommit
[email protected]cc51cd02010-12-23 00:48:392665
[email protected]5724c962014-04-11 09:32:562666 Squashes branch into a single commit.
[email protected]cc51cd02010-12-23 00:48:392667 Updates changelog with metadata (e.g. pointer to review).
2668 Pushes/dcommits the code upstream.
2669 Updates review and closes.
2670 """
2671 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2672 help='bypass upload presubmit hook')
2673 parser.add_option('-m', dest='message',
2674 help="override review description")
2675 parser.add_option('-f', action='store_true', dest='force',
2676 help="force yes to questions (don't prompt)")
2677 parser.add_option('-c', dest='contributor',
2678 help="external contributor for patch (appended to " +
2679 "description and used as author for git). Should be " +
2680 "formatted as 'First Last <[email protected]>'")
[email protected]53937ba2012-10-02 18:20:432681 add_git_similarity(parser)
[email protected]cf6a5d22015-04-09 22:02:002682 auth.add_auth_options(parser)
[email protected]cc51cd02010-12-23 00:48:392683 (options, args) = parser.parse_args(args)
[email protected]cf6a5d22015-04-09 22:02:002684 auth_config = auth.extract_auth_config_from_options(options)
2685
2686 cl = Changelist(auth_config=auth_config)
[email protected]cc51cd02010-12-23 00:48:392687
[email protected]5724c962014-04-11 09:32:562688 current = cl.GetBranch()
2689 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2690 if not settings.GetIsGitSvn() and remote == '.':
2691 print
2692 print 'Attempting to push branch %r into another local branch!' % current
2693 print
2694 print 'Either reparent this branch on top of origin/master:'
2695 print ' git reparent-branch --root'
2696 print
2697 print 'OR run `git rebase-update` if you think the parent branch is already'
2698 print 'committed.'
2699 print
2700 print ' Current parent: %r' % upstream_branch
2701 return 1
2702
[email protected]566a02a2014-08-22 01:34:132703 if not args or cmd == 'land':
[email protected]cc51cd02010-12-23 00:48:392704 # Default to merging against our best guess of the upstream branch.
2705 args = [cl.GetUpstreamBranch()]
2706
[email protected]13f623c2011-07-22 16:02:232707 if options.contributor:
2708 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2709 print "Please provide contibutor as 'First Last <[email protected]>'"
2710 return 1
2711
[email protected]cc51cd02010-12-23 00:48:392712 base_branch = args[0]
[email protected]9bb85e22012-06-13 20:28:232713 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
[email protected]cc51cd02010-12-23 00:48:392714
[email protected]71437c02015-04-09 19:29:402715 if git_common.is_dirty_git_tree(cmd):
[email protected]cc51cd02010-12-23 00:48:392716 return 1
2717
2718 # This rev-list syntax means "show all commits not in my branch that
2719 # are in base_branch".
2720 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2721 base_branch]).splitlines()
2722 if upstream_commits:
2723 print ('Base branch "%s" has %d commits '
2724 'not in this branch.' % (base_branch, len(upstream_commits)))
2725 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2726 return 1
2727
[email protected]9bb85e22012-06-13 20:28:232728 # This is the revision `svn dcommit` will commit on top of.
[email protected]566a02a2014-08-22 01:34:132729 svn_head = None
2730 if cmd == 'dcommit' or base_has_submodules:
2731 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2732 '--pretty=format:%H'])
[email protected]9bb85e22012-06-13 20:28:232733
[email protected]cc51cd02010-12-23 00:48:392734 if cmd == 'dcommit':
[email protected]9bb85e22012-06-13 20:28:232735 # If the base_head is a submodule merge commit, the first parent of the
2736 # base_head should be a git-svn commit, which is what we're interested in.
2737 base_svn_head = base_branch
2738 if base_has_submodules:
2739 base_svn_head += '^1'
2740
2741 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
[email protected]cc51cd02010-12-23 00:48:392742 if extra_commits:
2743 print ('This branch has %d additional commits not upstreamed yet.'
2744 % len(extra_commits.splitlines()))
2745 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2746 'before attempting to %s.' % (base_branch, cmd))
2747 return 1
2748
[email protected]e6896b52014-08-29 01:38:032749 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
[email protected]b0a63912012-01-17 18:10:162750 if not options.bypass_hooks:
[email protected]13f623c2011-07-22 16:02:232751 author = None
2752 if options.contributor:
2753 author = re.search(r'\<(.*)\>', options.contributor).group(1)
[email protected]b0a63912012-01-17 18:10:162754 hook_results = cl.RunHook(
2755 committing=True,
[email protected]b0a63912012-01-17 18:10:162756 may_prompt=not options.force,
2757 verbose=options.verbose,
[email protected]e6896b52014-08-29 01:38:032758 change=cl.GetChange(merge_base, author))
[email protected]b0a63912012-01-17 18:10:162759 if not hook_results.should_continue():
2760 return 1
[email protected]cc51cd02010-12-23 00:48:392761
[email protected]566a02a2014-08-22 01:34:132762 # Check the tree status if the tree status URL is set.
2763 status = GetTreeStatus()
2764 if 'closed' == status:
2765 print('The tree is closed. Please wait for it to reopen. Use '
2766 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2767 return 1
2768 elif 'unknown' == status:
2769 print('Unable to determine tree status. Please verify manually and '
2770 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2771 return 1
[email protected]cc51cd02010-12-23 00:48:392772
[email protected]78936cb2013-04-11 00:17:522773 change_desc = ChangeDescription(options.message)
2774 if not change_desc.description and cl.GetIssue():
2775 change_desc = ChangeDescription(cl.GetDescription())
[email protected]cc51cd02010-12-23 00:48:392776
[email protected]78936cb2013-04-11 00:17:522777 if not change_desc.description:
[email protected]1a173982012-08-29 20:43:052778 if not cl.GetIssue() and options.bypass_hooks:
[email protected]e6896b52014-08-29 01:38:032779 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
[email protected]1a173982012-08-29 20:43:052780 else:
2781 print 'No description set.'
2782 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2783 return 1
[email protected]cc51cd02010-12-23 00:48:392784
[email protected]78936cb2013-04-11 00:17:522785 # Keep a separate copy for the commit message, because the commit message
2786 # contains the link to the Rietveld issue, while the Rietveld message contains
2787 # the commit viewvc url.
[email protected]e52678e2013-04-26 18:34:442788 # Keep a separate copy for the commit message.
2789 if cl.GetIssue():
[email protected]cf087782013-07-23 13:08:482790 change_desc.update_reviewers(cl.GetApprovingReviewers())
[email protected]e52678e2013-04-26 18:34:442791
[email protected]78936cb2013-04-11 00:17:522792 commit_desc = ChangeDescription(change_desc.description)
[email protected]cc73ad62011-07-06 17:39:262793 if cl.GetIssue():
[email protected]4c61dcc2015-06-08 22:31:292794 # Xcode won't linkify this URL unless there is a non-whitespace character
[email protected]4b39c5f2015-07-07 10:33:122795 # after it. Add a period on a new line to circumvent this. Also add a space
2796 # before the period to make sure that Gitiles continues to correctly resolve
2797 # the URL.
2798 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
[email protected]cc51cd02010-12-23 00:48:392799 if options.contributor:
[email protected]78936cb2013-04-11 00:17:522800 commit_desc.append_footer('Patch from %s.' % options.contributor)
2801
[email protected]eec3ea32013-08-15 20:31:392802 print('Description:')
2803 print(commit_desc.description)
[email protected]cc51cd02010-12-23 00:48:392804
[email protected]e6896b52014-08-29 01:38:032805 branches = [merge_base, cl.GetBranchRef()]
[email protected]cc51cd02010-12-23 00:48:392806 if not options.force:
[email protected]79540052012-10-19 23:15:262807 print_stats(options.similarity, options.find_copies, branches)
[email protected]cc51cd02010-12-23 00:48:392808
[email protected]9bb85e22012-06-13 20:28:232809 # We want to squash all this branch's commits into one commit with the proper
2810 # description. We do this by doing a "reset --soft" to the base branch (which
2811 # keeps the working copy the same), then dcommitting that. If origin/master
2812 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2813 # commit onto a branch based on the git-svn head.
[email protected]cc51cd02010-12-23 00:48:392814 MERGE_BRANCH = 'git-cl-commit'
[email protected]9bb85e22012-06-13 20:28:232815 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2816 # Delete the branches if they exist.
2817 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2818 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2819 result = RunGitWithCode(showref_cmd)
2820 if result[0] == 0:
2821 RunGit(['branch', '-D', branch])
[email protected]cc51cd02010-12-23 00:48:392822
2823 # We might be in a directory that's present in this branch but not in the
2824 # trunk. Move up to the top of the tree so that git commands that expect a
2825 # valid CWD won't fail after we check out the merge branch.
[email protected]8b0553c2014-02-11 00:33:372826 rel_base_path = settings.GetRelativeRoot()
[email protected]cc51cd02010-12-23 00:48:392827 if rel_base_path:
2828 os.chdir(rel_base_path)
2829
2830 # Stuff our change into the merge branch.
2831 # We wrap in a try...finally block so if anything goes wrong,
2832 # we clean up the branches.
[email protected]0ba7f962011-01-11 22:13:582833 retcode = -1
[email protected]e6896b52014-08-29 01:38:032834 pushed_to_pending = False
[email protected]566a02a2014-08-22 01:34:132835 pending_ref = None
[email protected]e6896b52014-08-29 01:38:032836 revision = None
[email protected]cc51cd02010-12-23 00:48:392837 try:
[email protected]b4a75c42011-03-08 08:35:382838 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
[email protected]e6896b52014-08-29 01:38:032839 RunGit(['reset', '--soft', merge_base])
[email protected]cc51cd02010-12-23 00:48:392840 if options.contributor:
[email protected]78936cb2013-04-11 00:17:522841 RunGit(
2842 [
2843 'commit', '--author', options.contributor,
2844 '-m', commit_desc.description,
2845 ])
[email protected]cc51cd02010-12-23 00:48:392846 else:
[email protected]78936cb2013-04-11 00:17:522847 RunGit(['commit', '-m', commit_desc.description])
[email protected]9bb85e22012-06-13 20:28:232848 if base_has_submodules:
2849 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2850 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2851 RunGit(['checkout', CHERRY_PICK_BRANCH])
2852 RunGit(['cherry-pick', cherry_pick_commit])
[email protected]566a02a2014-08-22 01:34:132853 if cmd == 'land':
[email protected]0f58fa82012-11-05 01:45:202854 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
[email protected]566a02a2014-08-22 01:34:132855 pending_prefix = settings.GetPendingRefPrefix()
2856 if not pending_prefix or branch.startswith(pending_prefix):
2857 # If not using refs/pending/heads/* at all, or target ref is already set
2858 # to pending, then push to the target ref directly.
2859 retcode, output = RunGitWithCode(
2860 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
[email protected]e6896b52014-08-29 01:38:032861 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
[email protected]566a02a2014-08-22 01:34:132862 else:
2863 # Cherry-pick the change on top of pending ref and then push it.
2864 assert branch.startswith('refs/'), branch
2865 assert pending_prefix[-1] == '/', pending_prefix
2866 pending_ref = pending_prefix + branch[len('refs/'):]
2867 retcode, output = PushToGitPending(remote, pending_ref, branch)
[email protected]e6896b52014-08-29 01:38:032868 pushed_to_pending = (retcode == 0)
[email protected]34504a12014-08-29 23:51:372869 if retcode == 0:
2870 revision = RunGit(['rev-parse', 'HEAD']).strip()
[email protected]cc51cd02010-12-23 00:48:392871 else:
2872 # dcommit the merge branch.
[email protected]a1950c42014-12-05 22:15:562873 cmd_args = [
[email protected]6abc6522014-12-02 07:34:492874 'svn', 'dcommit',
2875 '-C%s' % options.similarity,
2876 '--no-rebase', '--rmdir',
2877 ]
2878 if settings.GetForceHttpsCommitUrl():
2879 # Allow forcing https commit URLs for some projects that don't allow
2880 # committing to http URLs (like Google Code).
2881 remote_url = cl.GetGitSvnRemoteUrl()
2882 if urlparse.urlparse(remote_url).scheme == 'http':
2883 remote_url = remote_url.replace('http://', 'https://')
[email protected]a1950c42014-12-05 22:15:562884 cmd_args.append('--commit-url=%s' % remote_url)
2885 _, output = RunGitWithCode(cmd_args)
[email protected]34504a12014-08-29 23:51:372886 if 'Committed r' in output:
2887 revision = re.match(
2888 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2889 logging.debug(output)
[email protected]cc51cd02010-12-23 00:48:392890 finally:
2891 # And then swap back to the original branch and clean up.
2892 RunGit(['checkout', '-q', cl.GetBranch()])
2893 RunGit(['branch', '-D', MERGE_BRANCH])
[email protected]9bb85e22012-06-13 20:28:232894 if base_has_submodules:
2895 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
[email protected]cc51cd02010-12-23 00:48:392896
[email protected]34504a12014-08-29 23:51:372897 if not revision:
[email protected]6c217b12014-08-29 22:10:592898 print 'Failed to push. If this persists, please file a bug.'
[email protected]34504a12014-08-29 23:51:372899 return 1
[email protected]6c217b12014-08-29 22:10:592900
[email protected]bbe9cc52014-09-05 18:25:512901 killed = False
[email protected]6c217b12014-08-29 22:10:592902 if pushed_to_pending:
[email protected]e6896b52014-08-29 01:38:032903 try:
2904 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2905 # We set pushed_to_pending to False, since it made it all the way to the
2906 # real ref.
2907 pushed_to_pending = False
2908 except KeyboardInterrupt:
[email protected]bbe9cc52014-09-05 18:25:512909 killed = True
[email protected]e6896b52014-08-29 01:38:032910
[email protected]cc51cd02010-12-23 00:48:392911 if cl.GetIssue():
[email protected]e6896b52014-08-29 01:38:032912 to_pending = ' to pending queue' if pushed_to_pending else ''
[email protected]cc51cd02010-12-23 00:48:392913 viewvc_url = settings.GetViewVCUrl()
[email protected]e6896b52014-08-29 01:38:032914 if not to_pending:
2915 if viewvc_url and revision:
2916 change_desc.append_footer(
2917 'Committed: %s%s' % (viewvc_url, revision))
2918 elif revision:
2919 change_desc.append_footer('Committed: %s' % (revision,))
[email protected]cc51cd02010-12-23 00:48:392920 print ('Closing issue '
2921 '(you may be prompted for your codereview password)...')
[email protected]78936cb2013-04-11 00:17:522922 cl.UpdateDescription(change_desc.description)
[email protected]cc51cd02010-12-23 00:48:392923 cl.CloseIssue()
[email protected]1033efd2013-07-23 23:25:092924 props = cl.GetIssueProperties()
[email protected]34b5d822013-02-18 01:39:242925 patch_num = len(props['patchsets'])
[email protected]52d224a2014-08-27 14:44:412926 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
[email protected]782570c2014-09-26 21:48:022927 patch_num, props['patchsets'][-1], to_pending, revision)
[email protected]3ec0d542014-01-14 20:00:032928 if options.bypass_hooks:
2929 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2930 else:
2931 comment += ' (presubmit successful).'
[email protected]b85a3162013-01-26 01:11:132932 cl.RpcServer().add_comment(cl.GetIssue(), comment)
[email protected]1033efd2013-07-23 23:25:092933 cl.SetIssue(None)
[email protected]0ba7f962011-01-11 22:13:582934
[email protected]6c217b12014-08-29 22:10:592935 if pushed_to_pending:
[email protected]566a02a2014-08-22 01:34:132936 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2937 print 'The commit is in the pending queue (%s).' % pending_ref
2938 print (
[email protected]5f32a962014-09-05 21:33:232939 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
[email protected]566a02a2014-08-22 01:34:132940 'footer.' % branch)
2941
[email protected]6c217b12014-08-29 22:10:592942 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2943 if os.path.isfile(hook):
2944 RunCommand([hook, merge_base], error_ok=True)
[email protected]0ba7f962011-01-11 22:13:582945
[email protected]bbe9cc52014-09-05 18:25:512946 return 1 if killed else 0
[email protected]cc51cd02010-12-23 00:48:392947
2948
[email protected]e6896b52014-08-29 01:38:032949def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2950 print
2951 print 'Waiting for commit to be landed on %s...' % real_ref
2952 print '(If you are impatient, you may Ctrl-C once without harm)'
2953 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2954 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2955
2956 loop = 0
2957 while True:
2958 sys.stdout.write('fetching (%d)... \r' % loop)
2959 sys.stdout.flush()
2960 loop += 1
2961
2962 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2963 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2964 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2965 for commit in commits.splitlines():
2966 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2967 print 'Found commit on %s' % real_ref
2968 return commit
2969
2970 current_rev = to_rev
2971
2972
[email protected]566a02a2014-08-22 01:34:132973def PushToGitPending(remote, pending_ref, upstream_ref):
2974 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2975
2976 Returns:
2977 (retcode of last operation, output log of last operation).
2978 """
2979 assert pending_ref.startswith('refs/'), pending_ref
2980 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2981 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2982 code = 0
2983 out = ''
[email protected]749fbd92014-08-26 21:57:532984 max_attempts = 3
2985 attempts_left = max_attempts
2986 while attempts_left:
2987 if attempts_left != max_attempts:
2988 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2989 attempts_left -= 1
[email protected]566a02a2014-08-22 01:34:132990
2991 # Fetch. Retry fetch errors.
[email protected]749fbd92014-08-26 21:57:532992 print 'Fetching pending ref %s...' % pending_ref
[email protected]566a02a2014-08-22 01:34:132993 code, out = RunGitWithCode(
[email protected]749fbd92014-08-26 21:57:532994 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
[email protected]566a02a2014-08-22 01:34:132995 if code:
[email protected]749fbd92014-08-26 21:57:532996 print 'Fetch failed with exit code %d.' % code
2997 if out.strip():
2998 print out.strip()
[email protected]566a02a2014-08-22 01:34:132999 continue
3000
3001 # Try to cherry pick. Abort on merge conflicts.
[email protected]749fbd92014-08-26 21:57:533002 print 'Cherry-picking commit on top of pending ref...'
[email protected]566a02a2014-08-22 01:34:133003 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
[email protected]749fbd92014-08-26 21:57:533004 code, out = RunGitWithCode(['cherry-pick', cherry])
[email protected]566a02a2014-08-22 01:34:133005 if code:
3006 print (
[email protected]749fbd92014-08-26 21:57:533007 'Your patch doesn\'t apply cleanly to ref \'%s\', '
3008 'the following files have merge conflicts:' % pending_ref)
[email protected]566a02a2014-08-22 01:34:133009 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
3010 print 'Please rebase your patch and try again.'
[email protected]749fbd92014-08-26 21:57:533011 RunGitWithCode(['cherry-pick', '--abort'])
[email protected]566a02a2014-08-22 01:34:133012 return code, out
3013
3014 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
[email protected]749fbd92014-08-26 21:57:533015 print 'Pushing commit to %s... It can take a while.' % pending_ref
[email protected]566a02a2014-08-22 01:34:133016 code, out = RunGitWithCode(
3017 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
3018 if code == 0:
3019 # Success.
[email protected]e6896b52014-08-29 01:38:033020 print 'Commit pushed to pending ref successfully!'
[email protected]566a02a2014-08-22 01:34:133021 return code, out
3022
[email protected]749fbd92014-08-26 21:57:533023 print 'Push failed with exit code %d.' % code
3024 if out.strip():
3025 print out.strip()
3026 if IsFatalPushFailure(out):
3027 print (
3028 'Fatal push error. Make sure your .netrc credentials and git '
3029 'user.email are correct and you have push access to the repo.')
3030 return code, out
3031
3032 print 'All attempts to push to pending ref failed.'
[email protected]566a02a2014-08-22 01:34:133033 return code, out
3034
3035
[email protected]749fbd92014-08-26 21:57:533036def IsFatalPushFailure(push_stdout):
3037 """True if retrying push won't help."""
3038 return '(prohibited by Gerrit)' in push_stdout
3039
3040
[email protected]0633fb42013-08-16 20:06:143041@subcommand.usage('[upstream branch to apply against]')
[email protected]cc51cd02010-12-23 00:48:393042def CMDdcommit(parser, args):
[email protected]d9c1b202013-07-24 23:52:113043 """Commits the current changelist via git-svn."""
[email protected]cc51cd02010-12-23 00:48:393044 if not settings.GetIsGitSvn():
[email protected]f0e41522015-06-10 19:52:013045 if get_footer_svn_id():
3046 # If it looks like previous commits were mirrored with git-svn.
3047 message = """This repository appears to be a git-svn mirror, but no
3048upstream SVN master is set. You probably need to run 'git auto-svn' once."""
3049 else:
3050 message = """This doesn't appear to be an SVN repository.
3051If your project has a true, writeable git repository, you probably want to run
3052'git cl land' instead.
3053If your project has a git mirror of an upstream SVN master, you probably need
3054to run 'git svn init'.
3055
3056Using the wrong command might cause your commit to appear to succeed, and the
3057review to be closed, without actually landing upstream. If you choose to
3058proceed, please verify that the commit lands upstream as expected."""
[email protected]cde3bb62011-01-20 01:16:143059 print(message)
[email protected]90541732011-04-01 17:54:183060 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
[email protected]cc51cd02010-12-23 00:48:393061 return SendUpstream(parser, args, 'dcommit')
3062
3063
[email protected]0633fb42013-08-16 20:06:143064@subcommand.usage('[upstream branch to apply against]')
[email protected]cee6dc42014-05-07 17:04:033065def CMDland(parser, args):
[email protected]d9c1b202013-07-24 23:52:113066 """Commits the current changelist via git."""
[email protected]f0e41522015-06-10 19:52:013067 if settings.GetIsGitSvn() or get_footer_svn_id():
[email protected]cc51cd02010-12-23 00:48:393068 print('This appears to be an SVN repository.')
3069 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
[email protected]f0e41522015-06-10 19:52:013070 print('(Ignore if this is the first commit after migrating from svn->git)')
[email protected]90541732011-04-01 17:54:183071 ask_for_data('[Press enter to push or ctrl-C to quit]')
[email protected]566a02a2014-08-22 01:34:133072 return SendUpstream(parser, args, 'land')
[email protected]cc51cd02010-12-23 00:48:393073
3074
[email protected]fbed6562015-09-25 21:22:363075def ParseIssueNum(arg):
3076 """Parses the issue number from args if present otherwise returns None."""
3077 if re.match(r'\d+', arg):
3078 return arg
3079 if arg.startswith('http'):
3080 return re.sub(r'.*/(\d+)/?', r'\1', arg)
3081 return None
3082
3083
3084@subcommand.usage('<patch url or issue id or issue url>')
[email protected]cc51cd02010-12-23 00:48:393085def CMDpatch(parser, args):
[email protected]e5e59002013-10-02 23:21:253086 """Patches in a code review."""
[email protected]cc51cd02010-12-23 00:48:393087 parser.add_option('-b', dest='newbranch',
3088 help='create a new branch off trunk for the patch')
[email protected]1ef44af2013-10-16 16:24:323089 parser.add_option('-f', '--force', action='store_true',
[email protected]cc51cd02010-12-23 00:48:393090 help='with -b, clobber any existing branch')
[email protected]1ef44af2013-10-16 16:24:323091 parser.add_option('-d', '--directory', action='store', metavar='DIR',
3092 help='Change to the directory DIR immediately, '
3093 'before doing anything else.')
3094 parser.add_option('--reject', action='store_true',
[email protected]6a0b07c2013-07-10 01:29:193095 help='failed patches spew .rej files rather than '
3096 'attempting a 3-way merge')
[email protected]cc51cd02010-12-23 00:48:393097 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
3098 help="don't commit after patch applies")
[email protected]1d88dd32016-02-04 16:25:123099
3100 group = optparse.OptionGroup(parser,
3101 """Options for continuing work on the current issue uploaded
3102from a different clone (e.g. different machine). Must be used independently from
3103the other options. No issue number should be specified, and the branch must have
3104an issue number associated with it""")
3105 group.add_option('--reapply', action='store_true',
3106 dest='reapply',
3107 help="""Reset the branch and reapply the issue.
3108CAUTION: This will undo any local changes in this branch""")
3109
3110 group.add_option('--pull', action='store_true', dest='pull',
3111 help="Performs a pull before reapplying.")
3112 parser.add_option_group(group)
3113
[email protected]cf6a5d22015-04-09 22:02:003114 auth.add_auth_options(parser)
[email protected]cc51cd02010-12-23 00:48:393115 (options, args) = parser.parse_args(args)
[email protected]cf6a5d22015-04-09 22:02:003116 auth_config = auth.extract_auth_config_from_options(options)
3117
[email protected]1d88dd32016-02-04 16:25:123118 issue_arg = None
3119 if options.reapply :
3120 if len(args) > 0:
3121 parser.error("--reapply implies no additional arguments.")
[email protected]fbed6562015-09-25 21:22:363122
[email protected]1d88dd32016-02-04 16:25:123123 cl = Changelist()
3124 issue_arg = cl.GetIssue()
3125 upstream = cl.GetUpstreamBranch()
3126 if upstream == None:
3127 parser.error("No upstream branch specified. Cannot reset branch")
3128
3129 RunGit(['reset', '--hard', upstream])
3130 if options.pull:
3131 RunGit(['pull'])
3132 else:
3133 if len(args) != 1:
3134 parser.error("Must specify issue number")
3135
3136 issue_arg = ParseIssueNum(args[0])
3137
[email protected]fbed6562015-09-25 21:22:363138 # The patch URL works because ParseIssueNum won't do any substitution
3139 # as the re.sub pattern fails to match and just returns it.
3140 if issue_arg == None:
3141 parser.print_help()
3142 return 1
[email protected]cc51cd02010-12-23 00:48:393143
[email protected]46309bf2015-04-03 21:04:493144 # We don't want uncommitted changes mixed up with the patch.
[email protected]71437c02015-04-09 19:29:403145 if git_common.is_dirty_git_tree('patch'):
[email protected]46309bf2015-04-03 21:04:493146 return 1
3147
[email protected]27bb3872011-05-30 20:33:193148 # TODO(maruel): Use apply_issue.py
[email protected]e8077812012-02-03 03:41:463149 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
[email protected]27bb3872011-05-30 20:33:193150
[email protected]87b9bf02013-09-26 20:35:153151 if options.newbranch:
[email protected]1d88dd32016-02-04 16:25:123152 if options.reapply:
3153 parser.error("--reapply excludes any option other than --pull")
[email protected]87b9bf02013-09-26 20:35:153154 if options.force:
3155 RunGit(['branch', '-D', options.newbranch],
3156 stderr=subprocess2.PIPE, error_ok=True)
3157 RunGit(['checkout', '-b', options.newbranch,
3158 Changelist().GetUpstreamBranch()])
3159
[email protected]1ef44af2013-10-16 16:24:323160 return PatchIssue(issue_arg, options.reject, options.nocommit,
[email protected]cf6a5d22015-04-09 22:02:003161 options.directory, auth_config)
[email protected]87b9bf02013-09-26 20:35:153162
3163
[email protected]cf6a5d22015-04-09 22:02:003164def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
[email protected]a872e752015-04-28 23:42:183165 # PatchIssue should never be called with a dirty tree. It is up to the
3166 # caller to check this, but just in case we assert here since the
3167 # consequences of the caller not checking this could be dire.
[email protected]71437c02015-04-09 19:29:403168 assert(not git_common.is_dirty_git_tree('apply'))
[email protected]46309bf2015-04-03 21:04:493169
[email protected]87b9bf02013-09-26 20:35:153170 if type(issue_arg) is int or issue_arg.isdigit():
[email protected]cc51cd02010-12-23 00:48:393171 # Input is an issue id. Figure out the URL.
[email protected]52424302012-08-29 15:14:303172 issue = int(issue_arg)
[email protected]cf6a5d22015-04-09 22:02:003173 cl = Changelist(issue=issue, auth_config=auth_config)
[email protected]1033efd2013-07-23 23:25:093174 patchset = cl.GetMostRecentPatchset()
[email protected]0281f522012-09-14 13:37:593175 patch_data = cl.GetPatchSetDiff(issue, patchset)
[email protected]cc51cd02010-12-23 00:48:393176 else:
[email protected]eb5edbc2012-01-16 17:03:283177 # Assume it's a URL to the patch. Default to https.
3178 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
[email protected]44424542015-06-02 18:35:293179 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
[email protected]e77ebbf2011-03-29 20:35:383180 if not match:
[email protected]cc51cd02010-12-23 00:48:393181 DieWithError('Must pass an issue ID or full URL for '
3182 '\'Download raw patch set\'')
[email protected]44424542015-06-02 18:35:293183 issue = int(match.group(2))
3184 cl = Changelist(issue=issue, auth_config=auth_config)
3185 cl.rietveld_server = match.group(1)
3186 patchset = int(match.group(3))
[email protected]e77ebbf2011-03-29 20:35:383187 patch_data = urllib2.urlopen(issue_arg).read()
[email protected]cc51cd02010-12-23 00:48:393188
[email protected]cc51cd02010-12-23 00:48:393189 # Switch up to the top-level directory, if necessary, in preparation for
3190 # applying the patch.
[email protected]8b0553c2014-02-11 00:33:373191 top = settings.GetRelativeRoot()
[email protected]cc51cd02010-12-23 00:48:393192 if top:
3193 os.chdir(top)
3194
[email protected]cc51cd02010-12-23 00:48:393195 # Git patches have a/ at the beginning of source paths. We strip that out
3196 # with a sed script rather than the -p flag to patch so we can feed either
3197 # Git or svn-style patches into the same apply command.
3198 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
[email protected]32f9f5e2011-09-14 13:41:473199 try:
3200 patch_data = subprocess2.check_output(
3201 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
3202 except subprocess2.CalledProcessError:
[email protected]cc51cd02010-12-23 00:48:393203 DieWithError('Git patch mungling failed.')
3204 logging.info(patch_data)
[email protected]82b91cd2013-07-09 06:33:413205
[email protected]cc51cd02010-12-23 00:48:393206 # We use "git apply" to apply the patch instead of "patch" so that we can
3207 # pick up file adds.
3208 # The --index flag means: also insert into the index (so we catch adds).
[email protected]82b91cd2013-07-09 06:33:413209 cmd = ['git', 'apply', '--index', '-p0']
[email protected]1ef44af2013-10-16 16:24:323210 if directory:
3211 cmd.extend(('--directory', directory))
[email protected]87b9bf02013-09-26 20:35:153212 if reject:
[email protected]cc51cd02010-12-23 00:48:393213 cmd.append('--reject')
[email protected]6a0b07c2013-07-10 01:29:193214 elif IsGitVersionAtLeast('1.7.12'):
3215 cmd.append('--3way')
[email protected]32f9f5e2011-09-14 13:41:473216 try:
[email protected]8b0553c2014-02-11 00:33:373217 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
[email protected]82b91cd2013-07-09 06:33:413218 stdin=patch_data, stdout=subprocess2.VOID)
[email protected]32f9f5e2011-09-14 13:41:473219 except subprocess2.CalledProcessError:
[email protected]a872e752015-04-28 23:42:183220 print 'Failed to apply the patch'
3221 return 1
[email protected]cc51cd02010-12-23 00:48:393222
3223 # If we had an issue, commit the current state and register the issue.
[email protected]87b9bf02013-09-26 20:35:153224 if not nocommit:
[email protected]5b3bebb2015-05-28 21:41:433225 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
3226 'patch from issue %(i)s at patchset '
[email protected]71284d92014-11-14 18:12:503227 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
3228 % {'i': issue, 'p': patchset})])
[email protected]cf6a5d22015-04-09 22:02:003229 cl = Changelist(auth_config=auth_config)
[email protected]cc51cd02010-12-23 00:48:393230 cl.SetIssue(issue)
[email protected]0281f522012-09-14 13:37:593231 cl.SetPatchset(patchset)
[email protected]98ca6622013-04-09 20:58:403232 print "Committed patch locally."
[email protected]cc51cd02010-12-23 00:48:393233 else:
3234 print "Patch applied to index."
3235 return 0
3236
3237
3238def CMDrebase(parser, args):
[email protected]d9c1b202013-07-24 23:52:113239 """Rebases current branch on top of svn repo."""
[email protected]cc51cd02010-12-23 00:48:393240 # Provide a wrapper for git svn rebase to help avoid accidental
3241 # git svn dcommit.
3242 # It's the only command that doesn't use parser at all since we just defer
3243 # execution to git-svn.
[email protected]82b91cd2013-07-09 06:33:413244
[email protected]8b0553c2014-02-11 00:33:373245 return RunGitWithCode(['svn', 'rebase'] + args)[1]
[email protected]cc51cd02010-12-23 00:48:393246
3247
[email protected]3ec0d542014-01-14 20:00:033248def GetTreeStatus(url=None):
[email protected]cc51cd02010-12-23 00:48:393249 """Fetches the tree status and returns either 'open', 'closed',
3250 'unknown' or 'unset'."""
[email protected]3ec0d542014-01-14 20:00:033251 url = url or settings.GetTreeStatusUrl(error_ok=True)
[email protected]cc51cd02010-12-23 00:48:393252 if url:
3253 status = urllib2.urlopen(url).read().lower()
3254 if status.find('closed') != -1 or status == '0':
3255 return 'closed'
3256 elif status.find('open') != -1 or status == '1':
3257 return 'open'
3258 return 'unknown'
[email protected]cc51cd02010-12-23 00:48:393259 return 'unset'
3260
[email protected]970c5222011-03-12 00:32:243261
[email protected]cc51cd02010-12-23 00:48:393262def GetTreeStatusReason():
3263 """Fetches the tree status from a json url and returns the message
3264 with the reason for the tree to be opened or closed."""
[email protected]bf1a7ba2011-02-01 16:21:463265 url = settings.GetTreeStatusUrl()
3266 json_url = urlparse.urljoin(url, '/current?format=json')
[email protected]cc51cd02010-12-23 00:48:393267 connection = urllib2.urlopen(json_url)
3268 status = json.loads(connection.read())
3269 connection.close()
3270 return status['message']
3271
[email protected]970c5222011-03-12 00:32:243272
[email protected]2b34d552014-08-14 22:18:423273def GetBuilderMaster(bot_list):
3274 """For a given builder, fetch the master from AE if available."""
3275 map_url = 'https://builders-map.appspot.com/'
3276 try:
3277 master_map = json.load(urllib2.urlopen(map_url))
3278 except urllib2.URLError as e:
3279 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
3280 (map_url, e))
3281 except ValueError as e:
3282 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
3283 if not master_map:
3284 return None, 'Failed to build master map.'
3285
3286 result_master = ''
3287 for bot in bot_list:
3288 builder = bot.split(':', 1)[0]
3289 master_list = master_map.get(builder, [])
3290 if not master_list:
3291 return None, ('No matching master for builder %s.' % builder)
3292 elif len(master_list) > 1:
3293 return None, ('The builder name %s exists in multiple masters %s.' %
3294 (builder, master_list))
3295 else:
3296 cur_master = master_list[0]
3297 if not result_master:
3298 result_master = cur_master
3299 elif result_master != cur_master:
3300 return None, 'The builders do not belong to the same master.'
3301 return result_master, None
3302
3303
[email protected]cc51cd02010-12-23 00:48:393304def CMDtree(parser, args):
[email protected]d9c1b202013-07-24 23:52:113305 """Shows the status of the tree."""
[email protected]97ae58e2011-03-18 00:29:203306 _, args = parser.parse_args(args)
[email protected]cc51cd02010-12-23 00:48:393307 status = GetTreeStatus()
3308 if 'unset' == status:
3309 print 'You must configure your tree status URL by running "git cl config".'
3310 return 2
3311
3312 print "The tree is %s" % status
3313 print
3314 print GetTreeStatusReason()
3315 if status != 'open':
3316 return 1
3317 return 0
3318
3319
[email protected]15192402012-09-06 12:38:293320def CMDtry(parser, args):
[email protected]db375572015-08-17 19:22:233321 """Triggers a try job through BuildBucket."""
[email protected]15192402012-09-06 12:38:293322 group = optparse.OptionGroup(parser, "Try job options")
3323 group.add_option(
3324 "-b", "--bot", action="append",
3325 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3326 "times to specify multiple builders. ex: "
[email protected]52914132015-01-22 10:37:093327 "'-b win_rel -b win_layout'. See "
[email protected]15192402012-09-06 12:38:293328 "the try server waterfall for the builders name and the tests "
[email protected]52914132015-01-22 10:37:093329 "available."))
[email protected]15192402012-09-06 12:38:293330 group.add_option(
[email protected]58a69cb2014-03-01 02:08:293331 "-m", "--master", default='',
[email protected]9e849272014-04-04 00:31:553332 help=("Specify a try master where to run the tries."))
[email protected]feb9e2a2015-09-25 19:11:093333 group.add_option( "--luci", action='store_true')
[email protected]58a69cb2014-03-01 02:08:293334 group.add_option(
[email protected]15192402012-09-06 12:38:293335 "-r", "--revision",
3336 help="Revision to use for the try job; default: the "
3337 "revision will be determined by the try server; see "
3338 "its waterfall for more info")
3339 group.add_option(
3340 "-c", "--clobber", action="store_true", default=False,
3341 help="Force a clobber before building; e.g. don't do an "
3342 "incremental build")
3343 group.add_option(
3344 "--project",
3345 help="Override which project to use. Projects are defined "
3346 "server-side to define what default bot set to use")
3347 group.add_option(
[email protected]45453142015-09-15 08:45:223348 "-p", "--property", dest="properties", action="append", default=[],
3349 help="Specify generic properties in the form -p key1=value1 -p "
3350 "key2=value2 etc (buildbucket only). The value will be treated as "
3351 "json if decodable, or as string otherwise.")
3352 group.add_option(
[email protected]15192402012-09-06 12:38:293353 "-n", "--name", help="Try job name; default to current branch name")
[email protected]6ebaf782015-05-12 19:17:543354 group.add_option(
[email protected]db375572015-08-17 19:22:233355 "--use-rietveld", action="store_true", default=False,
3356 help="Use Rietveld to trigger try jobs.")
3357 group.add_option(
3358 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3359 help="Host of buildbucket. The default host is %default.")
[email protected]15192402012-09-06 12:38:293360 parser.add_option_group(group)
[email protected]cf6a5d22015-04-09 22:02:003361 auth.add_auth_options(parser)
[email protected]15192402012-09-06 12:38:293362 options, args = parser.parse_args(args)
[email protected]cf6a5d22015-04-09 22:02:003363 auth_config = auth.extract_auth_config_from_options(options)
[email protected]15192402012-09-06 12:38:293364
[email protected]45453142015-09-15 08:45:223365 if options.use_rietveld and options.properties:
3366 parser.error('Properties can only be specified with buildbucket')
3367
3368 # Make sure that all properties are prop=value pairs.
3369 bad_params = [x for x in options.properties if '=' not in x]
3370 if bad_params:
3371 parser.error('Got properties with missing "=": %s' % bad_params)
3372
[email protected]15192402012-09-06 12:38:293373 if args:
3374 parser.error('Unknown arguments: %s' % args)
3375
[email protected]cf6a5d22015-04-09 22:02:003376 cl = Changelist(auth_config=auth_config)
[email protected]15192402012-09-06 12:38:293377 if not cl.GetIssue():
3378 parser.error('Need to upload first')
3379
[email protected]16f10f72014-06-24 22:14:363380 props = cl.GetIssueProperties()
[email protected]787e3062014-08-20 16:31:193381 if props.get('closed'):
3382 parser.error('Cannot send tryjobs for a closed CL')
3383
[email protected]16f10f72014-06-24 22:14:363384 if props.get('private'):
3385 parser.error('Cannot use trybots with private issue')
3386
[email protected]15192402012-09-06 12:38:293387 if not options.name:
3388 options.name = cl.GetBranch()
3389
[email protected]8da7f272014-03-14 01:28:393390 if options.bot and not options.master:
[email protected]2b34d552014-08-14 22:18:423391 options.master, err_msg = GetBuilderMaster(options.bot)
3392 if err_msg:
3393 parser.error('Tryserver master cannot be found because: %s\n'
3394 'Please manually specify the tryserver master'
3395 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
[email protected]8da7f272014-03-14 01:28:393396
[email protected]58a69cb2014-03-01 02:08:293397 def GetMasterMap():
[email protected]52914132015-01-22 10:37:093398 # Process --bot.
[email protected]58a69cb2014-03-01 02:08:293399 if not options.bot:
3400 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
[email protected]15192402012-09-06 12:38:293401
[email protected]58a69cb2014-03-01 02:08:293402 # Get try masters from PRESUBMIT.py files.
3403 masters = presubmit_support.DoGetTryMasters(
3404 change,
3405 change.LocalPaths(),
3406 settings.GetRoot(),
3407 None,
3408 None,
3409 options.verbose,
3410 sys.stdout)
3411 if masters:
3412 return masters
[email protected]43064fd2013-12-18 20:07:443413
[email protected]58a69cb2014-03-01 02:08:293414 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3415 options.bot = presubmit_support.DoGetTrySlaves(
3416 change,
3417 change.LocalPaths(),
3418 settings.GetRoot(),
3419 None,
3420 None,
3421 options.verbose,
3422 sys.stdout)
[email protected]71184c02016-01-13 15:18:443423
3424 if not options.bot:
3425 # Get try masters from cq.cfg if any.
3426 # TODO(tandrii): some (but very few) projects store cq.cfg in different
3427 # location.
3428 cq_cfg = os.path.join(change.RepositoryRoot(),
3429 'infra', 'config', 'cq.cfg')
3430 if os.path.exists(cq_cfg):
3431 masters = {}
[email protected]59994802016-01-14 10:10:333432 cq_masters = commit_queue.get_master_builder_map(
3433 cq_cfg, include_experimental=False, include_triggered=False)
[email protected]71184c02016-01-13 15:18:443434 for master, builders in cq_masters.iteritems():
3435 for builder in builders:
3436 # Skip presubmit builders, because these will fail without LGTM.
3437 if 'presubmit' not in builder.lower():
3438 masters.setdefault(master, {})[builder] = ['defaulttests']
3439 if masters:
3440 return masters
3441
[email protected]58a69cb2014-03-01 02:08:293442 if not options.bot:
3443 parser.error('No default try builder to try, use --bot')
[email protected]15192402012-09-06 12:38:293444
[email protected]58a69cb2014-03-01 02:08:293445 builders_and_tests = {}
3446 # TODO(machenbach): The old style command-line options don't support
3447 # multiple try masters yet.
3448 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3449 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3450
3451 for bot in old_style:
3452 if ':' in bot:
[email protected]52914132015-01-22 10:37:093453 parser.error('Specifying testfilter is no longer supported')
[email protected]58a69cb2014-03-01 02:08:293454 elif ',' in bot:
3455 parser.error('Specify one bot per --bot flag')
3456 else:
[email protected]3764fa22015-10-21 16:40:403457 builders_and_tests.setdefault(bot, [])
[email protected]58a69cb2014-03-01 02:08:293458
3459 for bot, tests in new_style:
3460 builders_and_tests.setdefault(bot, []).extend(tests)
3461
3462 # Return a master map with one master to be backwards compatible. The
3463 # master name defaults to an empty string, which will cause the master
3464 # not to be set on rietveld (deprecated).
3465 return {options.master: builders_and_tests}
3466
3467 masters = GetMasterMap()
[email protected]43064fd2013-12-18 20:07:443468
[email protected]58a69cb2014-03-01 02:08:293469 for builders in masters.itervalues():
3470 if any('triggered' in b for b in builders):
3471 print >> sys.stderr, (
3472 'ERROR You are trying to send a job to a triggered bot. This type of'
3473 ' bot requires an\ninitial job from a parent (usually a builder). '
3474 'Instead send your job to the parent.\n'
3475 'Bot list: %s' % builders)
3476 return 1
[email protected]f3b21232012-09-24 20:48:553477
[email protected]36e420b2013-08-06 23:21:123478 patchset = cl.GetMostRecentPatchset()
3479 if patchset and patchset != cl.GetPatchset():
3480 print(
3481 '\nWARNING Mismatch between local config and server. Did a previous '
3482 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3483 'Continuing using\npatchset %s.\n' % patchset)
[email protected]feb9e2a2015-09-25 19:11:093484 if options.luci:
3485 trigger_luci_job(cl, masters, options)
3486 elif not options.use_rietveld:
[email protected]6ebaf782015-05-12 19:17:543487 try:
3488 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3489 except BuildbucketResponseException as ex:
3490 print 'ERROR: %s' % ex
[email protected]d246c972013-12-21 22:47:383491 return 1
[email protected]6ebaf782015-05-12 19:17:543492 except Exception as e:
3493 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3494 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3495 e, stacktrace)
3496 return 1
3497 else:
3498 try:
3499 cl.RpcServer().trigger_distributed_try_jobs(
3500 cl.GetIssue(), patchset, options.name, options.clobber,
3501 options.revision, masters)
3502 except urllib2.HTTPError as e:
3503 if e.code == 404:
3504 print('404 from rietveld; '
3505 'did you mean to use "git try" instead of "git cl try"?')
3506 return 1
3507 print('Tried jobs on:')
[email protected]58a69cb2014-03-01 02:08:293508
[email protected]6ebaf782015-05-12 19:17:543509 for (master, builders) in sorted(masters.iteritems()):
3510 if master:
3511 print 'Master: %s' % master
3512 length = max(len(builder) for builder in builders)
3513 for builder in sorted(builders):
3514 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
[email protected]15192402012-09-06 12:38:293515 return 0
3516
3517
[email protected]b015fac2016-02-26 14:52:013518def CMDtry_results(parser, args):
3519 group = optparse.OptionGroup(parser, "Try job results options")
3520 group.add_option(
3521 "-p", "--patchset", type=int, help="patchset number if not current.")
3522 group.add_option(
3523 "--print-master", action='store_true', help="print master name as well")
3524 group.add_option(
3525 "--buildbucket-host", default='cr-buildbucket.appspot.com',
3526 help="Host of buildbucket. The default host is %default.")
3527 parser.add_option_group(group)
3528 auth.add_auth_options(parser)
3529 options, args = parser.parse_args(args)
3530 if args:
3531 parser.error('Unrecognized args: %s' % ' '.join(args))
3532
3533 auth_config = auth.extract_auth_config_from_options(options)
3534 cl = Changelist(auth_config=auth_config)
3535 if not cl.GetIssue():
3536 parser.error('Need to upload first')
3537
3538 if not options.patchset:
3539 options.patchset = cl.GetMostRecentPatchset()
3540 if options.patchset and options.patchset != cl.GetPatchset():
3541 print(
3542 '\nWARNING Mismatch between local config and server. Did a previous '
3543 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3544 'Continuing using\npatchset %s.\n' % options.patchset)
3545 try:
3546 jobs = fetch_try_jobs(auth_config, cl, options)
3547 except BuildbucketResponseException as ex:
3548 print 'Buildbucket error: %s' % ex
3549 return 1
3550 except Exception as e:
3551 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3552 print 'ERROR: Exception when trying to fetch tryjobs: %s\n%s' % (
3553 e, stacktrace)
3554 return 1
3555 print_tryjobs(options, jobs)
3556 return 0
3557
3558
[email protected]0633fb42013-08-16 20:06:143559@subcommand.usage('[new upstream branch]')
[email protected]cc51cd02010-12-23 00:48:393560def CMDupstream(parser, args):
[email protected]d9c1b202013-07-24 23:52:113561 """Prints or sets the name of the upstream branch, if any."""
[email protected]97ae58e2011-03-18 00:29:203562 _, args = parser.parse_args(args)
[email protected]ac0ba332012-08-09 23:42:533563 if len(args) > 1:
[email protected]27bb3872011-05-30 20:33:193564 parser.error('Unrecognized args: %s' % ' '.join(args))
[email protected]ac0ba332012-08-09 23:42:533565
[email protected]cc51cd02010-12-23 00:48:393566 cl = Changelist()
[email protected]ac0ba332012-08-09 23:42:533567 if args:
3568 # One arg means set upstream branch.
[email protected]c9cf90a2014-04-28 20:32:313569 branch = cl.GetBranch()
3570 RunGit(['branch', '--set-upstream', branch, args[0]])
[email protected]ac0ba332012-08-09 23:42:533571 cl = Changelist()
3572 print "Upstream branch set to " + cl.GetUpstreamBranch()
[email protected]c9cf90a2014-04-28 20:32:313573
3574 # Clear configured merge-base, if there is one.
3575 git_common.remove_merge_base(branch)
[email protected]ac0ba332012-08-09 23:42:533576 else:
3577 print cl.GetUpstreamBranch()
[email protected]cc51cd02010-12-23 00:48:393578 return 0
3579
3580
[email protected]00858c82013-12-02 23:08:033581def CMDweb(parser, args):
3582 """Opens the current CL in the web browser."""
3583 _, args = parser.parse_args(args)
3584 if args:
3585 parser.error('Unrecognized args: %s' % ' '.join(args))
3586
3587 issue_url = Changelist().GetIssueURL()
3588 if not issue_url:
3589 print >> sys.stderr, 'ERROR No issue to open'
3590 return 1
3591
3592 webbrowser.open(issue_url)
3593 return 0
3594
3595
[email protected]27bb3872011-05-30 20:33:193596def CMDset_commit(parser, args):
[email protected]d9c1b202013-07-24 23:52:113597 """Sets the commit bit to trigger the Commit Queue."""
[email protected]cf6a5d22015-04-09 22:02:003598 auth.add_auth_options(parser)
3599 options, args = parser.parse_args(args)
3600 auth_config = auth.extract_auth_config_from_options(options)
[email protected]27bb3872011-05-30 20:33:193601 if args:
3602 parser.error('Unrecognized args: %s' % ' '.join(args))
[email protected]cf6a5d22015-04-09 22:02:003603 cl = Changelist(auth_config=auth_config)
[email protected]16f10f72014-06-24 22:14:363604 props = cl.GetIssueProperties()
3605 if props.get('private'):
3606 parser.error('Cannot set commit on private issue')
[email protected]27bb3872011-05-30 20:33:193607 cl.SetFlag('commit', '1')
3608 return 0
3609
3610
[email protected]411034a2013-02-26 15:12:013611def CMDset_close(parser, args):
[email protected]d9c1b202013-07-24 23:52:113612 """Closes the issue."""
[email protected]cf6a5d22015-04-09 22:02:003613 auth.add_auth_options(parser)
3614 options, args = parser.parse_args(args)
3615 auth_config = auth.extract_auth_config_from_options(options)
[email protected]411034a2013-02-26 15:12:013616 if args:
3617 parser.error('Unrecognized args: %s' % ' '.join(args))
[email protected]cf6a5d22015-04-09 22:02:003618 cl = Changelist(auth_config=auth_config)
[email protected]411034a2013-02-26 15:12:013619 # Ensure there actually is an issue to close.
3620 cl.GetDescription()
3621 cl.CloseIssue()
3622 return 0
3623
3624
[email protected]87b9bf02013-09-26 20:35:153625def CMDdiff(parser, args):
[email protected]37b2ec02015-04-03 00:49:153626 """Shows differences between local tree and last upload."""
[email protected]cf6a5d22015-04-09 22:02:003627 auth.add_auth_options(parser)
3628 options, args = parser.parse_args(args)
3629 auth_config = auth.extract_auth_config_from_options(options)
3630 if args:
3631 parser.error('Unrecognized args: %s' % ' '.join(args))
[email protected]46309bf2015-04-03 21:04:493632
3633 # Uncommitted (staged and unstaged) changes will be destroyed by
3634 # "git reset --hard" if there are merging conflicts in PatchIssue().
3635 # Staged changes would be committed along with the patch from last
3636 # upload, hence counted toward the "last upload" side in the final
3637 # diff output, and this is not what we want.
[email protected]71437c02015-04-09 19:29:403638 if git_common.is_dirty_git_tree('diff'):
[email protected]46309bf2015-04-03 21:04:493639 return 1
3640
[email protected]cf6a5d22015-04-09 22:02:003641 cl = Changelist(auth_config=auth_config)
[email protected]78dc9842013-11-25 18:43:443642 issue = cl.GetIssue()
[email protected]87b9bf02013-09-26 20:35:153643 branch = cl.GetBranch()
[email protected]78dc9842013-11-25 18:43:443644 if not issue:
3645 DieWithError('No issue found for current branch (%s)' % branch)
[email protected]87b9bf02013-09-26 20:35:153646 TMP_BRANCH = 'git-cl-diff'
[email protected]8b0553c2014-02-11 00:33:373647 base_branch = cl.GetCommonAncestorWithUpstream()
[email protected]87b9bf02013-09-26 20:35:153648
3649 # Create a new branch based on the merge-base
3650 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3651 try:
3652 # Patch in the latest changes from rietveld.
[email protected]cf6a5d22015-04-09 22:02:003653 rtn = PatchIssue(issue, False, False, None, auth_config)
[email protected]87b9bf02013-09-26 20:35:153654 if rtn != 0:
[email protected]a872e752015-04-28 23:42:183655 RunGit(['reset', '--hard'])
[email protected]87b9bf02013-09-26 20:35:153656 return rtn
3657
[email protected]06928532015-02-03 02:11:293658 # Switch back to starting branch and diff against the temporary
[email protected]87b9bf02013-09-26 20:35:153659 # branch containing the latest rietveld patch.
[email protected]06928532015-02-03 02:11:293660 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
[email protected]87b9bf02013-09-26 20:35:153661 finally:
3662 RunGit(['checkout', '-q', branch])
3663 RunGit(['branch', '-D', TMP_BRANCH])
3664
3665 return 0
3666
3667
[email protected]faf3fdf2013-09-20 02:11:483668def CMDowners(parser, args):
[email protected]37b2ec02015-04-03 00:49:153669 """Interactively find the owners for reviewing."""
[email protected]faf3fdf2013-09-20 02:11:483670 parser.add_option(
3671 '--no-color',
3672 action='store_true',
3673 help='Use this option to disable color output')
[email protected]cf6a5d22015-04-09 22:02:003674 auth.add_auth_options(parser)
[email protected]faf3fdf2013-09-20 02:11:483675 options, args = parser.parse_args(args)
[email protected]cf6a5d22015-04-09 22:02:003676 auth_config = auth.extract_auth_config_from_options(options)
[email protected]faf3fdf2013-09-20 02:11:483677
3678 author = RunGit(['config', 'user.email']).strip() or None
3679
[email protected]cf6a5d22015-04-09 22:02:003680 cl = Changelist(auth_config=auth_config)
[email protected]faf3fdf2013-09-20 02:11:483681
3682 if args:
3683 if len(args) > 1:
3684 parser.error('Unknown args')
3685 base_branch = args[0]
3686 else:
3687 # Default to diffing against the common ancestor of the upstream branch.
[email protected]8b0553c2014-02-11 00:33:373688 base_branch = cl.GetCommonAncestorWithUpstream()
[email protected]faf3fdf2013-09-20 02:11:483689
3690 change = cl.GetChange(base_branch, None)
3691 return owners_finder.OwnersFinder(
3692 [f.LocalPath() for f in
3693 cl.GetChange(base_branch, None).AffectedFiles()],
3694 change.RepositoryRoot(), author,
3695 fopen=file, os_path=os.path, glob=glob.glob,
3696 disable_color=options.no_color).run()
3697
3698
[email protected]6f7fa5e2016-01-20 19:32:213699def BuildGitDiffCmd(diff_type, upstream_commit, args):
[email protected]e0a7c5d2015-02-23 20:30:083700 """Generates a diff command."""
3701 # Generate diff for the current branch's changes.
3702 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3703 upstream_commit, '--' ]
3704
3705 if args:
3706 for arg in args:
[email protected]6f7fa5e2016-01-20 19:32:213707 if os.path.isdir(arg) or os.path.isfile(arg):
[email protected]e0a7c5d2015-02-23 20:30:083708 diff_cmd.append(arg)
3709 else:
3710 DieWithError('Argument "%s" is not a file or a directory' % arg)
[email protected]e0a7c5d2015-02-23 20:30:083711
3712 return diff_cmd
3713
[email protected]6f7fa5e2016-01-20 19:32:213714def MatchingFileType(file_name, extensions):
3715 """Returns true if the file name ends with one of the given extensions."""
3716 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
[email protected]e0a7c5d2015-02-23 20:30:083717
[email protected]555cfe42014-01-29 18:21:393718@subcommand.usage('[files or directories to diff]')
[email protected]fab8f822013-05-06 17:43:093719def CMDformat(parser, args):
[email protected]9d0644d2015-06-05 23:16:543720 """Runs auto-formatting tools (clang-format etc.) on the diff."""
[email protected]9819b1b2014-12-09 21:21:533721 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
[email protected]8b61f112016-02-05 13:28:583722 GN_EXTS = ['.gn', '.gni']
[email protected]3b7e15c2014-01-21 17:44:473723 parser.add_option('--full', action='store_true',
3724 help='Reformat the full content of all touched files')
3725 parser.add_option('--dry-run', action='store_true',
3726 help='Don\'t modify any file on disk.')
[email protected]9d0644d2015-06-05 23:16:543727 parser.add_option('--python', action='store_true',
3728 help='Format python code with yapf (experimental).')
[email protected]04d5a222014-03-07 18:30:423729 parser.add_option('--diff', action='store_true',
3730 help='Print diff to stdout rather than modifying files.')
[email protected]fab8f822013-05-06 17:43:093731 opts, args = parser.parse_args(args)
[email protected]fab8f822013-05-06 17:43:093732
[email protected]ff7a1fb2013-12-10 19:21:413733 # git diff generates paths against the root of the repository. Change
3734 # to that directory so clang-format can find files even within subdirs.
[email protected]8b0553c2014-02-11 00:33:373735 rel_base_path = settings.GetRelativeRoot()
[email protected]ff7a1fb2013-12-10 19:21:413736 if rel_base_path:
3737 os.chdir(rel_base_path)
3738
[email protected]29e47272013-05-17 17:01:463739 # Grab the merge-base commit, i.e. the upstream commit of the current
3740 # branch when it was created or the last time it was rebased. This is
3741 # to cover the case where the user may have called "git fetch origin",
3742 # moving the origin branch to a newer commit, but hasn't rebased yet.
3743 upstream_commit = None
3744 cl = Changelist()
3745 upstream_branch = cl.GetUpstreamBranch()
3746 if upstream_branch:
3747 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3748 upstream_commit = upstream_commit.strip()
3749
3750 if not upstream_commit:
3751 DieWithError('Could not find base commit for this branch. '
3752 'Are you in detached state?')
3753
[email protected]6f7fa5e2016-01-20 19:32:213754 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
3755 diff_output = RunGit(changed_files_cmd)
3756 diff_files = diff_output.splitlines()
[email protected]ad21b922016-01-28 17:48:423757 # Filter out files deleted by this CL
3758 diff_files = [x for x in diff_files if os.path.isfile(x)]
[email protected]e0a7c5d2015-02-23 20:30:083759
[email protected]6f7fa5e2016-01-20 19:32:213760 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
3761 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
3762 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
[email protected]8b61f112016-02-05 13:28:583763 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
[email protected]29e47272013-05-17 17:01:463764
[email protected]3ac1c4e2014-01-16 02:44:423765 top_dir = os.path.normpath(
3766 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3767
3768 # Locate the clang-format binary in the checkout
3769 try:
3770 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3771 except clang_format.NotFoundError, e:
3772 DieWithError(e)
[email protected]c3b3dc02013-08-05 23:09:493773
[email protected]e0a7c5d2015-02-23 20:30:083774 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3775 # formatted. This is used to block during the presubmit.
3776 return_value = 0
3777
[email protected]0b35f5d2016-02-25 22:39:233778 if clang_diff_files:
3779 if opts.full:
[email protected]e0a7c5d2015-02-23 20:30:083780 cmd = [clang_format_tool]
3781 if not opts.dry_run and not opts.diff:
3782 cmd.append('-i')
[email protected]6f7fa5e2016-01-20 19:32:213783 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
[email protected]e0a7c5d2015-02-23 20:30:083784 if opts.diff:
3785 sys.stdout.write(stdout)
[email protected]0b35f5d2016-02-25 22:39:233786 else:
3787 env = os.environ.copy()
3788 env['PATH'] = str(os.path.dirname(clang_format_tool))
3789 try:
3790 script = clang_format.FindClangFormatScriptInChromiumTree(
3791 'clang-format-diff.py')
3792 except clang_format.NotFoundError, e:
3793 DieWithError(e)
[email protected]d6ddc1c2013-10-25 15:36:323794
[email protected]0b35f5d2016-02-25 22:39:233795 cmd = [sys.executable, script, '-p0']
3796 if not opts.dry_run and not opts.diff:
3797 cmd.append('-i')
[email protected]d6ddc1c2013-10-25 15:36:323798
[email protected]0b35f5d2016-02-25 22:39:233799 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
3800 diff_output = RunGit(diff_cmd)
[email protected]6f7fa5e2016-01-20 19:32:213801
[email protected]0b35f5d2016-02-25 22:39:233802 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
3803 if opts.diff:
3804 sys.stdout.write(stdout)
3805 if opts.dry_run and len(stdout) > 0:
3806 return_value = 2
[email protected]fab8f822013-05-06 17:43:093807
[email protected]9d0644d2015-06-05 23:16:543808 # Similar code to above, but using yapf on .py files rather than clang-format
3809 # on C/C++ files
3810 if opts.python:
[email protected]9d0644d2015-06-05 23:16:543811 yapf_tool = gclient_utils.FindExecutable('yapf')
3812 if yapf_tool is None:
3813 DieWithError('yapf not found in PATH')
3814
3815 if opts.full:
[email protected]6f7fa5e2016-01-20 19:32:213816 if python_diff_files:
[email protected]9d0644d2015-06-05 23:16:543817 cmd = [yapf_tool]
3818 if not opts.dry_run and not opts.diff:
3819 cmd.append('-i')
[email protected]6f7fa5e2016-01-20 19:32:213820 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
[email protected]9d0644d2015-06-05 23:16:543821 if opts.diff:
3822 sys.stdout.write(stdout)
3823 else:
3824 # TODO(sbc): yapf --lines mode still has some issues.
3825 # https://github.com/google/yapf/issues/154
3826 DieWithError('--python currently only works with --full')
3827
[email protected]6f7fa5e2016-01-20 19:32:213828 # Dart's formatter does not have the nice property of only operating on
3829 # modified chunks, so hard code full.
3830 if dart_diff_files:
[email protected]e0a7c5d2015-02-23 20:30:083831 try:
3832 command = [dart_format.FindDartFmtToolInChromiumTree()]
3833 if not opts.dry_run and not opts.diff:
3834 command.append('-w')
[email protected]6f7fa5e2016-01-20 19:32:213835 command.extend(dart_diff_files)
[email protected]e0a7c5d2015-02-23 20:30:083836
3837 stdout = RunCommand(command, cwd=top_dir, env=env)
3838 if opts.dry_run and stdout:
3839 return_value = 2
3840 except dart_format.NotFoundError as e:
[email protected]3e445022015-12-17 09:07:263841 print ('Warning: Unable to check Dart code formatting. Dart SDK not ' +
3842 'found in this checkout. Files in other languages are still ' +
3843 'formatted.')
[email protected]e0a7c5d2015-02-23 20:30:083844
[email protected]8b61f112016-02-05 13:28:583845 # Format GN build files. Always run on full build files for canonical form.
3846 if gn_diff_files:
3847 cmd = ['gn', 'format']
3848 if not opts.dry_run and not opts.diff:
3849 cmd.append('--in-place')
3850 for gn_diff_file in gn_diff_files:
3851 stdout = RunCommand(cmd + [gn_diff_file], cwd=top_dir)
3852 if opts.diff:
3853 sys.stdout.write(stdout)
3854
[email protected]e0a7c5d2015-02-23 20:30:083855 return return_value
[email protected]fab8f822013-05-06 17:43:093856
3857
[email protected]84a80c42015-09-22 20:40:373858@subcommand.usage('<codereview url or issue id>')
3859def CMDcheckout(parser, args):
3860 """Checks out a branch associated with a given Rietveld issue."""
3861 _, args = parser.parse_args(args)
3862
3863 if len(args) != 1:
3864 parser.print_help()
3865 return 1
3866
[email protected]fbed6562015-09-25 21:22:363867 target_issue = ParseIssueNum(args[0])
3868 if target_issue == None:
[email protected]84a80c42015-09-22 20:40:373869 parser.print_help()
3870 return 1
3871
3872 key_and_issues = [x.split() for x in RunGit(
3873 ['config', '--local', '--get-regexp', r'branch\..*\.rietveldissue'])
3874 .splitlines()]
3875 branches = []
3876 for key, issue in key_and_issues:
3877 if issue == target_issue:
3878 branches.append(re.sub(r'branch\.(.*)\.rietveldissue', r'\1', key))
3879
3880 if len(branches) == 0:
3881 print 'No branch found for issue %s.' % target_issue
3882 return 1
3883 if len(branches) == 1:
3884 RunGit(['checkout', branches[0]])
3885 else:
3886 print 'Multiple branches match issue %s:' % target_issue
3887 for i in range(len(branches)):
3888 print '%d: %s' % (i, branches[i])
3889 which = raw_input('Choose by index: ')
3890 try:
3891 RunGit(['checkout', branches[int(which)]])
3892 except (IndexError, ValueError):
3893 print 'Invalid selection, not checking out any branch.'
3894 return 1
3895
3896 return 0
3897
3898
[email protected]29404b52014-09-08 22:58:003899def CMDlol(parser, args):
3900 # This command is intentionally undocumented.
[email protected]3421c992014-11-02 02:20:323901 print zlib.decompress(base64.b64decode(
3902 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3903 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3904 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3905 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
[email protected]29404b52014-09-08 22:58:003906 return 0
3907
3908
[email protected]d9c1b202013-07-24 23:52:113909class OptionParser(optparse.OptionParser):
3910 """Creates the option parse and add --verbose support."""
3911 def __init__(self, *args, **kwargs):
[email protected]0633fb42013-08-16 20:06:143912 optparse.OptionParser.__init__(
3913 self, *args, prog='git cl', version=__version__, **kwargs)
[email protected]d9c1b202013-07-24 23:52:113914 self.add_option(
3915 '-v', '--verbose', action='count', default=0,
3916 help='Use 2 times for more debugging info')
3917
3918 def parse_args(self, args=None, values=None):
3919 options, args = optparse.OptionParser.parse_args(self, args, values)
3920 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3921 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3922 return options, args
3923
[email protected]d9c1b202013-07-24 23:52:113924
[email protected]cc51cd02010-12-23 00:48:393925def main(argv):
[email protected]82798cb2012-02-23 18:16:123926 if sys.hexversion < 0x02060000:
3927 print >> sys.stderr, (
3928 '\nYour python version %s is unsupported, please upgrade.\n' %
3929 sys.version.split(' ', 1)[0])
3930 return 2
[email protected]2e23ce32013-05-07 12:42:283931
[email protected]ddd59412011-11-30 14:20:383932 # Reload settings.
3933 global settings
3934 settings = Settings()
3935
[email protected]39c0b222013-08-17 16:57:013936 colorize_CMDstatus_doc()
[email protected]0633fb42013-08-16 20:06:143937 dispatcher = subcommand.CommandDispatcher(__name__)
3938 try:
3939 return dispatcher.execute(OptionParser(), argv)
[email protected]eed4df32015-04-10 21:30:203940 except auth.AuthenticationError as e:
3941 DieWithError(str(e))
[email protected]0633fb42013-08-16 20:06:143942 except urllib2.HTTPError, e:
3943 if e.code != 500:
3944 raise
3945 DieWithError(
3946 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3947 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
[email protected]013731e2015-02-26 18:28:433948 return 0
[email protected]cc51cd02010-12-23 00:48:393949
3950
3951if __name__ == '__main__':
[email protected]2e23ce32013-05-07 12:42:283952 # These affect sys.stdout so do it outside of main() to simplify mocks in
3953 # unit testing.
[email protected]6f09cd92011-04-01 16:38:123954 fix_encoding.fix_encoding()
[email protected]2e23ce32013-05-07 12:42:283955 colorama.init()
[email protected]013731e2015-02-26 18:28:433956 try:
3957 sys.exit(main(sys.argv[1:]))
3958 except KeyboardInterrupt:
3959 sys.stderr.write('interrupted\n')
3960 sys.exit(1)