blob: f3aa2ab5ae972872605c01d26bfa22e1f742a101 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000013import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000014import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000015import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import logging
17import optparse
18import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000019import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000021import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000023import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000025import time
26import traceback
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000028import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000029import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000030import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031
32try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000033 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034except ImportError:
35 pass
36
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000037from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000038from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000039from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000040import auth
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000042import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000043import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000044import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000045import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000046import git_common
piman@chromium.org336f9122014-09-04 02:16:55 +000047import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000048import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000049import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000050import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000051import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000052import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000053import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000054import watchlists
55
maruel@chromium.org0633fb42013-08-16 20:06:14 +000056__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000058DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000059POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000060DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000061GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000062CHANGE_ID = 'Change-Id:'
rmistry@google.comc68112d2015-03-03 12:48:06 +000063REFS_THAT_ALIAS_TO_OTHER_REFS = {
64 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
65 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
66}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067
sheyang@google.com6ebaf782015-05-12 19:17:54 +000068# Buildbucket-related constants
69BUILDBUCKET_HOST = 'cr-buildbucket.appspot.com'
70
thestig@chromium.org44202a22014-03-11 19:22:18 +000071# Valid extensions for files we want to lint.
72DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
73DEFAULT_LINT_IGNORE_REGEX = r"$^"
74
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000075# Shortcut since it quickly becomes redundant.
76Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000077
maruel@chromium.orgddd59412011-11-30 14:20:38 +000078# Initialized in main()
79settings = None
80
81
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000082def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000083 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000084 sys.exit(1)
85
86
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000087def GetNoGitPagerEnv():
88 env = os.environ.copy()
89 # 'cat' is a magical git string that disables pagers on all platforms.
90 env['GIT_PAGER'] = 'cat'
91 return env
92
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000093
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000094def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000095 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000096 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000097 except subprocess2.CalledProcessError as e:
98 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000099 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000100 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000101 'Command "%s" failed.\n%s' % (
102 ' '.join(args), error_message or e.stdout or ''))
103 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104
105
106def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000107 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000108 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000109
110
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000111def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000112 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000113 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000114 if suppress_stderr:
115 stderr = subprocess2.VOID
116 else:
117 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000118 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000119 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000120 stdout=subprocess2.PIPE,
121 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000122 return code, out[0]
123 except ValueError:
124 # When the subprocess fails, it returns None. That triggers a ValueError
125 # when trying to unpack the return value into (out, code).
126 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000127
128
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000129def RunGitSilent(args):
130 """Returns stdout, suppresses stderr and ingores the return code."""
131 return RunGitWithCode(args, suppress_stderr=True)[1]
132
133
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000134def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000135 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000136 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000137 return (version.startswith(prefix) and
138 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139
140
maruel@chromium.org90541732011-04-01 17:54:18 +0000141def ask_for_data(prompt):
142 try:
143 return raw_input(prompt)
144 except KeyboardInterrupt:
145 # Hide the exception.
146 sys.exit(1)
147
148
iannucci@chromium.org79540052012-10-19 23:15:26 +0000149def git_set_branch_value(key, value):
150 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000151 if not branch:
152 return
153
154 cmd = ['config']
155 if isinstance(value, int):
156 cmd.append('--int')
157 git_key = 'branch.%s.%s' % (branch, key)
158 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000159
160
161def git_get_branch_default(key, default):
162 branch = Changelist().GetBranch()
163 if branch:
164 git_key = 'branch.%s.%s' % (branch, key)
165 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
166 try:
167 return int(stdout.strip())
168 except ValueError:
169 pass
170 return default
171
172
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000173def add_git_similarity(parser):
174 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000175 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000176 help='Sets the percentage that a pair of files need to match in order to'
177 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000178 parser.add_option(
179 '--find-copies', action='store_true',
180 help='Allows git to look for copies.')
181 parser.add_option(
182 '--no-find-copies', action='store_false', dest='find_copies',
183 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000184
185 old_parser_args = parser.parse_args
186 def Parse(args):
187 options, args = old_parser_args(args)
188
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000189 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000190 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000191 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000192 print('Note: Saving similarity of %d%% in git config.'
193 % options.similarity)
194 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000195
iannucci@chromium.org79540052012-10-19 23:15:26 +0000196 options.similarity = max(0, min(options.similarity, 100))
197
198 if options.find_copies is None:
199 options.find_copies = bool(
200 git_get_branch_default('git-find-copies', True))
201 else:
202 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000203
204 print('Using %d%% similarity for rename/copy detection. '
205 'Override with --similarity.' % options.similarity)
206
207 return options, args
208 parser.parse_args = Parse
209
210
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000211def _prefix_master(master):
212 """Convert user-specified master name to full master name.
213
214 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
215 name, while the developers always use shortened master name
216 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
217 function does the conversion for buildbucket migration.
218 """
219 prefix = 'master.'
220 if master.startswith(prefix):
221 return master
222 return '%s%s' % (prefix, master)
223
224
machenbach@chromium.org79e43ff2015-05-15 05:56:13 +0000225def trigger_try_jobs(auth_config, changelist, options, masters, category,
226 override_properties=None):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000227 rietveld_url = settings.GetDefaultServerUrl()
228 rietveld_host = urlparse.urlparse(rietveld_url).hostname
229 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
230 http = authenticator.authorize(httplib2.Http())
231 http.force_exception_to_status_code = True
232 issue_props = changelist.GetIssueProperties()
233 issue = changelist.GetIssue()
234 patchset = changelist.GetMostRecentPatchset()
235
236 buildbucket_put_url = (
237 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
238 hostname=BUILDBUCKET_HOST))
239 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
240 hostname=rietveld_host,
241 issue=issue,
242 patch=patchset)
243
244 batch_req_body = {'builds': []}
245 print_text = []
246 print_text.append('Tried jobs on:')
247 for master, builders_and_tests in sorted(masters.iteritems()):
248 print_text.append('Master: %s' % master)
249 bucket = _prefix_master(master)
250 for builder, tests in sorted(builders_and_tests.iteritems()):
251 print_text.append(' %s: %s' % (builder, tests))
252 parameters = {
253 'builder_name': builder,
254 'changes': [
255 {'author': {'email': issue_props['owner_email']}},
256 ],
257 'properties': {
258 'category': category,
259 'issue': issue,
260 'master': master,
261 'patch_project': issue_props['project'],
262 'patch_storage': 'rietveld',
263 'patchset': patchset,
264 'reason': options.name,
265 'revision': options.revision,
266 'rietveld': rietveld_url,
267 'testfilter': tests,
268 },
269 }
machenbach@chromium.org79e43ff2015-05-15 05:56:13 +0000270 if override_properties:
271 parameters['properties'].update(override_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000272 if options.clobber:
273 parameters['properties']['clobber'] = True
274 batch_req_body['builds'].append(
275 {
276 'bucket': bucket,
277 'parameters_json': json.dumps(parameters),
278 'tags': ['builder:%s' % builder,
279 'buildset:%s' % buildset,
280 'master:%s' % master,
281 'user_agent:git_cl_try']
282 }
283 )
284
285 for try_count in xrange(3):
286 response, content = http.request(
287 buildbucket_put_url,
288 'PUT',
289 body=json.dumps(batch_req_body),
290 headers={'Content-Type': 'application/json'},
291 )
292 content_json = None
293 try:
294 content_json = json.loads(content)
295 except ValueError:
296 pass
297
298 # Buildbucket could return an error even if status==200.
299 if content_json and content_json.get('error'):
300 msg = 'Error in response. Code: %d. Reason: %s. Message: %s.' % (
301 content_json['error'].get('code', ''),
302 content_json['error'].get('reason', ''),
303 content_json['error'].get('message', ''))
304 raise BuildbucketResponseException(msg)
305
306 if response.status == 200:
307 if not content_json:
308 raise BuildbucketResponseException(
309 'Buildbucket returns invalid json content: %s.\n'
310 'Please file bugs at crbug.com, label "Infra-BuildBucket".' %
311 content)
312 break
313 if response.status < 500 or try_count >= 2:
314 raise httplib2.HttpLib2Error(content)
315
316 # status >= 500 means transient failures.
317 logging.debug('Transient errors when triggering tryjobs. Will retry.')
318 time.sleep(0.5 + 1.5*try_count)
319
320 print '\n'.join(print_text)
321
322
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000323def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
324 """Return the corresponding git ref if |base_url| together with |glob_spec|
325 matches the full |url|.
326
327 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
328 """
329 fetch_suburl, as_ref = glob_spec.split(':')
330 if allow_wildcards:
331 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
332 if glob_match:
333 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
334 # "branches/{472,597,648}/src:refs/remotes/svn/*".
335 branch_re = re.escape(base_url)
336 if glob_match.group(1):
337 branch_re += '/' + re.escape(glob_match.group(1))
338 wildcard = glob_match.group(2)
339 if wildcard == '*':
340 branch_re += '([^/]*)'
341 else:
342 # Escape and replace surrounding braces with parentheses and commas
343 # with pipe symbols.
344 wildcard = re.escape(wildcard)
345 wildcard = re.sub('^\\\\{', '(', wildcard)
346 wildcard = re.sub('\\\\,', '|', wildcard)
347 wildcard = re.sub('\\\\}$', ')', wildcard)
348 branch_re += wildcard
349 if glob_match.group(3):
350 branch_re += re.escape(glob_match.group(3))
351 match = re.match(branch_re, url)
352 if match:
353 return re.sub('\*$', match.group(1), as_ref)
354
355 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
356 if fetch_suburl:
357 full_url = base_url + '/' + fetch_suburl
358 else:
359 full_url = base_url
360 if full_url == url:
361 return as_ref
362 return None
363
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000364
iannucci@chromium.org79540052012-10-19 23:15:26 +0000365def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000366 """Prints statistics about the change to the user."""
367 # --no-ext-diff is broken in some versions of Git, so try to work around
368 # this by overriding the environment (but there is still a problem if the
369 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000370 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000371 if 'GIT_EXTERNAL_DIFF' in env:
372 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000373
374 if find_copies:
375 similarity_options = ['--find-copies-harder', '-l100000',
376 '-C%s' % similarity]
377 else:
378 similarity_options = ['-M%s' % similarity]
379
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000380 try:
381 stdout = sys.stdout.fileno()
382 except AttributeError:
383 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000384 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000385 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000386 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000387 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000388
389
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000390class BuildbucketResponseException(Exception):
391 pass
392
393
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000394class Settings(object):
395 def __init__(self):
396 self.default_server = None
397 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000398 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000399 self.is_git_svn = None
400 self.svn_branch = None
401 self.tree_status_url = None
402 self.viewvc_url = None
403 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000404 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000405 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000406 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000407 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000408 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000409
410 def LazyUpdateIfNeeded(self):
411 """Updates the settings from a codereview.settings file, if available."""
412 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000413 # The only value that actually changes the behavior is
414 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000415 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000416 error_ok=True
417 ).strip().lower()
418
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000419 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000420 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000421 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000422 # set updated to True to avoid infinite calling loop
423 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000424 self.updated = True
425 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000426 self.updated = True
427
428 def GetDefaultServerUrl(self, error_ok=False):
429 if not self.default_server:
430 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000431 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000432 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000433 if error_ok:
434 return self.default_server
435 if not self.default_server:
436 error_message = ('Could not find settings file. You must configure '
437 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000438 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000439 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000440 return self.default_server
441
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000442 @staticmethod
443 def GetRelativeRoot():
444 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000445
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000446 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000447 if self.root is None:
448 self.root = os.path.abspath(self.GetRelativeRoot())
449 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000450
451 def GetIsGitSvn(self):
452 """Return true if this repo looks like it's using git-svn."""
453 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000454 if self.GetPendingRefPrefix():
455 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
456 self.is_git_svn = False
457 else:
458 # If you have any "svn-remote.*" config keys, we think you're using svn.
459 self.is_git_svn = RunGitWithCode(
460 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000461 return self.is_git_svn
462
463 def GetSVNBranch(self):
464 if self.svn_branch is None:
465 if not self.GetIsGitSvn():
466 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
467
468 # Try to figure out which remote branch we're based on.
469 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000470 # 1) iterate through our branch history and find the svn URL.
471 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000472
473 # regexp matching the git-svn line that contains the URL.
474 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
475
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000476 # We don't want to go through all of history, so read a line from the
477 # pipe at a time.
478 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000479 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000480 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
481 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000482 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000483 for line in proc.stdout:
484 match = git_svn_re.match(line)
485 if match:
486 url = match.group(1)
487 proc.stdout.close() # Cut pipe.
488 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000489
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000490 if url:
491 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
492 remotes = RunGit(['config', '--get-regexp',
493 r'^svn-remote\..*\.url']).splitlines()
494 for remote in remotes:
495 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000496 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000497 remote = match.group(1)
498 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000499 rewrite_root = RunGit(
500 ['config', 'svn-remote.%s.rewriteRoot' % remote],
501 error_ok=True).strip()
502 if rewrite_root:
503 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000504 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000505 ['config', 'svn-remote.%s.fetch' % remote],
506 error_ok=True).strip()
507 if fetch_spec:
508 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
509 if self.svn_branch:
510 break
511 branch_spec = RunGit(
512 ['config', 'svn-remote.%s.branches' % remote],
513 error_ok=True).strip()
514 if branch_spec:
515 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
516 if self.svn_branch:
517 break
518 tag_spec = RunGit(
519 ['config', 'svn-remote.%s.tags' % remote],
520 error_ok=True).strip()
521 if tag_spec:
522 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
523 if self.svn_branch:
524 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000525
526 if not self.svn_branch:
527 DieWithError('Can\'t guess svn branch -- try specifying it on the '
528 'command line')
529
530 return self.svn_branch
531
532 def GetTreeStatusUrl(self, error_ok=False):
533 if not self.tree_status_url:
534 error_message = ('You must configure your tree status URL by running '
535 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000536 self.tree_status_url = self._GetRietveldConfig(
537 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000538 return self.tree_status_url
539
540 def GetViewVCUrl(self):
541 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000542 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000543 return self.viewvc_url
544
rmistry@google.com90752582014-01-14 21:04:50 +0000545 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000546 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000547
rmistry@google.com5626a922015-02-26 14:03:30 +0000548 def GetRunPostUploadHook(self):
549 run_post_upload_hook = self._GetRietveldConfig(
550 'run-post-upload-hook', error_ok=True)
551 return run_post_upload_hook == "True"
552
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000553 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000554 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000555
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000556 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000557 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000558
ukai@chromium.orge8077812012-02-03 03:41:46 +0000559 def GetIsGerrit(self):
560 """Return true if this repo is assosiated with gerrit code review system."""
561 if self.is_gerrit is None:
562 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
563 return self.is_gerrit
564
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000565 def GetGitEditor(self):
566 """Return the editor specified in the git config, or None if none is."""
567 if self.git_editor is None:
568 self.git_editor = self._GetConfig('core.editor', error_ok=True)
569 return self.git_editor or None
570
thestig@chromium.org44202a22014-03-11 19:22:18 +0000571 def GetLintRegex(self):
572 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
573 DEFAULT_LINT_REGEX)
574
575 def GetLintIgnoreRegex(self):
576 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
577 DEFAULT_LINT_IGNORE_REGEX)
578
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000579 def GetProject(self):
580 if not self.project:
581 self.project = self._GetRietveldConfig('project', error_ok=True)
582 return self.project
583
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000584 def GetForceHttpsCommitUrl(self):
585 if not self.force_https_commit_url:
586 self.force_https_commit_url = self._GetRietveldConfig(
587 'force-https-commit-url', error_ok=True)
588 return self.force_https_commit_url
589
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000590 def GetPendingRefPrefix(self):
591 if not self.pending_ref_prefix:
592 self.pending_ref_prefix = self._GetRietveldConfig(
593 'pending-ref-prefix', error_ok=True)
594 return self.pending_ref_prefix
595
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000596 def _GetRietveldConfig(self, param, **kwargs):
597 return self._GetConfig('rietveld.' + param, **kwargs)
598
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000599 def _GetConfig(self, param, **kwargs):
600 self.LazyUpdateIfNeeded()
601 return RunGit(['config', param], **kwargs).strip()
602
603
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000604def ShortBranchName(branch):
605 """Convert a name like 'refs/heads/foo' to just 'foo'."""
606 return branch.replace('refs/heads/', '')
607
608
609class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000610 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000611 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000612 global settings
613 if not settings:
614 # Happens when git_cl.py is used as a utility library.
615 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000616 settings.GetDefaultServerUrl()
617 self.branchref = branchref
618 if self.branchref:
619 self.branch = ShortBranchName(self.branchref)
620 else:
621 self.branch = None
622 self.rietveld_server = None
623 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000624 self.lookedup_issue = False
625 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000626 self.has_description = False
627 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000628 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000629 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000630 self.cc = None
631 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000632 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000633 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000634 self._remote = None
635 self._rpc_server = None
636
637 @property
638 def auth_config(self):
639 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000640
641 def GetCCList(self):
642 """Return the users cc'd on this CL.
643
644 Return is a string suitable for passing to gcl with the --cc flag.
645 """
646 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000647 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000648 more_cc = ','.join(self.watchers)
649 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
650 return self.cc
651
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000652 def GetCCListWithoutDefault(self):
653 """Return the users cc'd on this CL excluding default ones."""
654 if self.cc is None:
655 self.cc = ','.join(self.watchers)
656 return self.cc
657
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000658 def SetWatchers(self, watchers):
659 """Set the list of email addresses that should be cc'd based on the changed
660 files in this CL.
661 """
662 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000663
664 def GetBranch(self):
665 """Returns the short branch name, e.g. 'master'."""
666 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000667 branchref = RunGit(['symbolic-ref', 'HEAD'],
668 stderr=subprocess2.VOID, error_ok=True).strip()
669 if not branchref:
670 return None
671 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000672 self.branch = ShortBranchName(self.branchref)
673 return self.branch
674
675 def GetBranchRef(self):
676 """Returns the full branch name, e.g. 'refs/heads/master'."""
677 self.GetBranch() # Poke the lazy loader.
678 return self.branchref
679
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000680 @staticmethod
681 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000682 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000683 e.g. 'origin', 'refs/heads/master'
684 """
685 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000686 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
687 error_ok=True).strip()
688 if upstream_branch:
689 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
690 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000691 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
692 error_ok=True).strip()
693 if upstream_branch:
694 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000695 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000696 # Fall back on trying a git-svn upstream branch.
697 if settings.GetIsGitSvn():
698 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000699 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000700 # Else, try to guess the origin remote.
701 remote_branches = RunGit(['branch', '-r']).split()
702 if 'origin/master' in remote_branches:
703 # Fall back on origin/master if it exits.
704 remote = 'origin'
705 upstream_branch = 'refs/heads/master'
706 elif 'origin/trunk' in remote_branches:
707 # Fall back on origin/trunk if it exists. Generally a shared
708 # git-svn clone
709 remote = 'origin'
710 upstream_branch = 'refs/heads/trunk'
711 else:
712 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000713Either pass complete "git diff"-style arguments, like
714 git cl upload origin/master
715or verify this branch is set up to track another (via the --track argument to
716"git checkout -b ...").""")
717
718 return remote, upstream_branch
719
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000720 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000721 return git_common.get_or_create_merge_base(self.GetBranch(),
722 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000723
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000724 def GetUpstreamBranch(self):
725 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000726 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000727 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000728 upstream_branch = upstream_branch.replace('refs/heads/',
729 'refs/remotes/%s/' % remote)
730 upstream_branch = upstream_branch.replace('refs/branch-heads/',
731 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000732 self.upstream_branch = upstream_branch
733 return self.upstream_branch
734
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000735 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000736 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000737 remote, branch = None, self.GetBranch()
738 seen_branches = set()
739 while branch not in seen_branches:
740 seen_branches.add(branch)
741 remote, branch = self.FetchUpstreamTuple(branch)
742 branch = ShortBranchName(branch)
743 if remote != '.' or branch.startswith('refs/remotes'):
744 break
745 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000746 remotes = RunGit(['remote'], error_ok=True).split()
747 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000748 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000749 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000750 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000751 logging.warning('Could not determine which remote this change is '
752 'associated with, so defaulting to "%s". This may '
753 'not be what you want. You may prevent this message '
754 'by running "git svn info" as documented here: %s',
755 self._remote,
756 GIT_INSTRUCTIONS_URL)
757 else:
758 logging.warn('Could not determine which remote this change is '
759 'associated with. You may prevent this message by '
760 'running "git svn info" as documented here: %s',
761 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000762 branch = 'HEAD'
763 if branch.startswith('refs/remotes'):
764 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000765 elif branch.startswith('refs/branch-heads/'):
766 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000767 else:
768 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000769 return self._remote
770
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000771 def GitSanityChecks(self, upstream_git_obj):
772 """Checks git repo status and ensures diff is from local commits."""
773
sbc@chromium.org79706062015-01-14 21:18:12 +0000774 if upstream_git_obj is None:
775 if self.GetBranch() is None:
776 print >> sys.stderr, (
777 'ERROR: unable to dertermine current branch (detached HEAD?)')
778 else:
779 print >> sys.stderr, (
780 'ERROR: no upstream branch')
781 return False
782
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000783 # Verify the commit we're diffing against is in our current branch.
784 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
785 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
786 if upstream_sha != common_ancestor:
787 print >> sys.stderr, (
788 'ERROR: %s is not in the current branch. You may need to rebase '
789 'your tracking branch' % upstream_sha)
790 return False
791
792 # List the commits inside the diff, and verify they are all local.
793 commits_in_diff = RunGit(
794 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
795 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
796 remote_branch = remote_branch.strip()
797 if code != 0:
798 _, remote_branch = self.GetRemoteBranch()
799
800 commits_in_remote = RunGit(
801 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
802
803 common_commits = set(commits_in_diff) & set(commits_in_remote)
804 if common_commits:
805 print >> sys.stderr, (
806 'ERROR: Your diff contains %d commits already in %s.\n'
807 'Run "git log --oneline %s..HEAD" to get a list of commits in '
808 'the diff. If you are using a custom git flow, you can override'
809 ' the reference used for this check with "git config '
810 'gitcl.remotebranch <git-ref>".' % (
811 len(common_commits), remote_branch, upstream_git_obj))
812 return False
813 return True
814
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000815 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000816 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000817
818 Returns None if it is not set.
819 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000820 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
821 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000822
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000823 def GetGitSvnRemoteUrl(self):
824 """Return the configured git-svn remote URL parsed from git svn info.
825
826 Returns None if it is not set.
827 """
828 # URL is dependent on the current directory.
829 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
830 if data:
831 keys = dict(line.split(': ', 1) for line in data.splitlines()
832 if ': ' in line)
833 return keys.get('URL', None)
834 return None
835
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836 def GetRemoteUrl(self):
837 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
838
839 Returns None if there is no remote.
840 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000841 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000842 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
843
844 # If URL is pointing to a local directory, it is probably a git cache.
845 if os.path.isdir(url):
846 url = RunGit(['config', 'remote.%s.url' % remote],
847 error_ok=True,
848 cwd=url).strip()
849 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000850
851 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000852 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000853 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000854 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000855 self.issue = int(issue) or None if issue else None
856 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000857 return self.issue
858
859 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000860 if not self.rietveld_server:
861 # If we're on a branch then get the server potentially associated
862 # with that branch.
863 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000864 rietveld_server_config = self._RietveldServer()
865 if rietveld_server_config:
866 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
867 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000868 if not self.rietveld_server:
869 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000870 return self.rietveld_server
871
872 def GetIssueURL(self):
873 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000874 if not self.GetIssue():
875 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000876 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
877
878 def GetDescription(self, pretty=False):
879 if not self.has_description:
880 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000881 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000882 try:
883 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000884 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000885 if e.code == 404:
886 DieWithError(
887 ('\nWhile fetching the description for issue %d, received a '
888 '404 (not found)\n'
889 'error. It is likely that you deleted this '
890 'issue on the server. If this is the\n'
891 'case, please run\n\n'
892 ' git cl issue 0\n\n'
893 'to clear the association with the deleted issue. Then run '
894 'this command again.') % issue)
895 else:
896 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000897 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000898 except urllib2.URLError as e:
899 print >> sys.stderr, (
900 'Warning: Failed to retrieve CL description due to network '
901 'failure.')
902 self.description = ''
903
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000904 self.has_description = True
905 if pretty:
906 wrapper = textwrap.TextWrapper()
907 wrapper.initial_indent = wrapper.subsequent_indent = ' '
908 return wrapper.fill(self.description)
909 return self.description
910
911 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000912 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000913 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000914 patchset = RunGit(['config', self._PatchsetSetting()],
915 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000916 self.patchset = int(patchset) or None if patchset else None
917 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000918 return self.patchset
919
920 def SetPatchset(self, patchset):
921 """Set this branch's patchset. If patchset=0, clears the patchset."""
922 if patchset:
923 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000924 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000925 else:
926 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000927 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000928 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000929
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000930 def GetMostRecentPatchset(self):
931 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000932
933 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000934 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000935 '/download/issue%s_%s.diff' % (issue, patchset))
936
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000937 def GetIssueProperties(self):
938 if self._props is None:
939 issue = self.GetIssue()
940 if not issue:
941 self._props = {}
942 else:
943 self._props = self.RpcServer().get_issue_properties(issue, True)
944 return self._props
945
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000946 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000947 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000948
apavlov@chromium.orge4efd512014-11-05 09:05:29 +0000949 def AddComment(self, message):
950 return self.RpcServer().add_comment(self.GetIssue(), message)
951
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000952 def SetIssue(self, issue):
953 """Set this branch's issue. If issue=0, clears the issue."""
954 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000955 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000956 RunGit(['config', self._IssueSetting(), str(issue)])
957 if self.rietveld_server:
958 RunGit(['config', self._RietveldServer(), self.rietveld_server])
959 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000960 current_issue = self.GetIssue()
961 if current_issue:
962 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000963 self.issue = None
964 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000965
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000966 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000967 if not self.GitSanityChecks(upstream_branch):
968 DieWithError('\nGit sanity check failure')
969
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000970 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000971 if not root:
972 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000973 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000974
975 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000976 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000977 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000978 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000979 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000980 except subprocess2.CalledProcessError:
981 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000982 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000983 'This branch probably doesn\'t exist anymore. To reset the\n'
984 'tracking branch, please run\n'
985 ' git branch --set-upstream %s trunk\n'
986 'replacing trunk with origin/master or the relevant branch') %
987 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000988
maruel@chromium.org52424302012-08-29 15:14:30 +0000989 issue = self.GetIssue()
990 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000991 if issue:
992 description = self.GetDescription()
993 else:
994 # If the change was never uploaded, use the log messages of all commits
995 # up to the branch point, as git cl upload will prefill the description
996 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000997 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
998 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000999
1000 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001001 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001002 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001003 name,
1004 description,
1005 absroot,
1006 files,
1007 issue,
1008 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001009 author,
1010 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001011
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001012 def GetStatus(self):
1013 """Apply a rough heuristic to give a simple summary of an issue's review
1014 or CQ status, assuming adherence to a common workflow.
1015
1016 Returns None if no issue for this branch, or one of the following keywords:
1017 * 'error' - error from review tool (including deleted issues)
1018 * 'unsent' - not sent for review
1019 * 'waiting' - waiting for review
1020 * 'reply' - waiting for owner to reply to review
1021 * 'lgtm' - LGTM from at least one approved reviewer
1022 * 'commit' - in the commit queue
1023 * 'closed' - closed
1024 """
1025 if not self.GetIssue():
1026 return None
1027
1028 try:
1029 props = self.GetIssueProperties()
1030 except urllib2.HTTPError:
1031 return 'error'
1032
1033 if props.get('closed'):
1034 # Issue is closed.
1035 return 'closed'
1036 if props.get('commit'):
1037 # Issue is in the commit queue.
1038 return 'commit'
1039
1040 try:
1041 reviewers = self.GetApprovingReviewers()
1042 except urllib2.HTTPError:
1043 return 'error'
1044
1045 if reviewers:
1046 # Was LGTM'ed.
1047 return 'lgtm'
1048
1049 messages = props.get('messages') or []
1050
1051 if not messages:
1052 # No message was sent.
1053 return 'unsent'
1054 if messages[-1]['sender'] != props.get('owner_email'):
1055 # Non-LGTM reply from non-owner
1056 return 'reply'
1057 return 'waiting'
1058
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001059 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001060 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001061
1062 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001063 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001064 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001065 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001066 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001067 except presubmit_support.PresubmitFailure, e:
1068 DieWithError(
1069 ('%s\nMaybe your depot_tools is out of date?\n'
1070 'If all fails, contact maruel@') % e)
1071
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001072 def UpdateDescription(self, description):
1073 self.description = description
1074 return self.RpcServer().update_description(
1075 self.GetIssue(), self.description)
1076
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001077 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001078 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001079 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001080
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001081 def SetFlag(self, flag, value):
1082 """Patchset must match."""
1083 if not self.GetPatchset():
1084 DieWithError('The patchset needs to match. Send another patchset.')
1085 try:
1086 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001087 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001088 except urllib2.HTTPError, e:
1089 if e.code == 404:
1090 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1091 if e.code == 403:
1092 DieWithError(
1093 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1094 'match?') % (self.GetIssue(), self.GetPatchset()))
1095 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001096
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001097 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001098 """Returns an upload.RpcServer() to access this review's rietveld instance.
1099 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001100 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001101 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001102 self.GetRietveldServer(),
1103 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001104 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001105
1106 def _IssueSetting(self):
1107 """Return the git setting that stores this change's issue."""
1108 return 'branch.%s.rietveldissue' % self.GetBranch()
1109
1110 def _PatchsetSetting(self):
1111 """Return the git setting that stores this change's most recent patchset."""
1112 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1113
1114 def _RietveldServer(self):
1115 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001116 branch = self.GetBranch()
1117 if branch:
1118 return 'branch.%s.rietveldserver' % branch
1119 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120
1121
1122def GetCodereviewSettingsInteractively():
1123 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001124 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001125 server = settings.GetDefaultServerUrl(error_ok=True)
1126 prompt = 'Rietveld server (host[:port])'
1127 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001128 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129 if not server and not newserver:
1130 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001131 if newserver:
1132 newserver = gclient_utils.UpgradeToHttps(newserver)
1133 if newserver != server:
1134 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001135
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001136 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137 prompt = caption
1138 if initial:
1139 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001140 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141 if new_val == 'x':
1142 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001143 elif new_val:
1144 if is_url:
1145 new_val = gclient_utils.UpgradeToHttps(new_val)
1146 if new_val != initial:
1147 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001149 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001150 SetProperty(settings.GetDefaultPrivateFlag(),
1151 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001152 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001153 'tree-status-url', False)
1154 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001155 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001156 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1157 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001158
1159 # TODO: configure a default branch to diff against, rather than this
1160 # svn-based hackery.
1161
1162
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001163class ChangeDescription(object):
1164 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001165 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001166 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001167
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001168 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001169 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001170
agable@chromium.org42c20792013-09-12 17:34:49 +00001171 @property # www.logilab.org/ticket/89786
1172 def description(self): # pylint: disable=E0202
1173 return '\n'.join(self._description_lines)
1174
1175 def set_description(self, desc):
1176 if isinstance(desc, basestring):
1177 lines = desc.splitlines()
1178 else:
1179 lines = [line.rstrip() for line in desc]
1180 while lines and not lines[0]:
1181 lines.pop(0)
1182 while lines and not lines[-1]:
1183 lines.pop(-1)
1184 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001185
piman@chromium.org336f9122014-09-04 02:16:55 +00001186 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001187 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001188 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001189 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001190 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001191 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001192
agable@chromium.org42c20792013-09-12 17:34:49 +00001193 # Get the set of R= and TBR= lines and remove them from the desciption.
1194 regexp = re.compile(self.R_LINE)
1195 matches = [regexp.match(line) for line in self._description_lines]
1196 new_desc = [l for i, l in enumerate(self._description_lines)
1197 if not matches[i]]
1198 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001199
agable@chromium.org42c20792013-09-12 17:34:49 +00001200 # Construct new unified R= and TBR= lines.
1201 r_names = []
1202 tbr_names = []
1203 for match in matches:
1204 if not match:
1205 continue
1206 people = cleanup_list([match.group(2).strip()])
1207 if match.group(1) == 'TBR':
1208 tbr_names.extend(people)
1209 else:
1210 r_names.extend(people)
1211 for name in r_names:
1212 if name not in reviewers:
1213 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001214 if add_owners_tbr:
1215 owners_db = owners.Database(change.RepositoryRoot(),
1216 fopen=file, os_path=os.path, glob=glob.glob)
1217 all_reviewers = set(tbr_names + reviewers)
1218 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1219 all_reviewers)
1220 tbr_names.extend(owners_db.reviewers_for(missing_files,
1221 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001222 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1223 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1224
1225 # Put the new lines in the description where the old first R= line was.
1226 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1227 if 0 <= line_loc < len(self._description_lines):
1228 if new_tbr_line:
1229 self._description_lines.insert(line_loc, new_tbr_line)
1230 if new_r_line:
1231 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001232 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001233 if new_r_line:
1234 self.append_footer(new_r_line)
1235 if new_tbr_line:
1236 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001237
1238 def prompt(self):
1239 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001240 self.set_description([
1241 '# Enter a description of the change.',
1242 '# This will be displayed on the codereview site.',
1243 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001244 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001245 '--------------------',
1246 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001247
agable@chromium.org42c20792013-09-12 17:34:49 +00001248 regexp = re.compile(self.BUG_LINE)
1249 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001250 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001251 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001252 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001253 if not content:
1254 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001255 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001256
1257 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001258 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1259 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001260 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001261 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001262
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001263 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001264 if self._description_lines:
1265 # Add an empty line if either the last line or the new line isn't a tag.
1266 last_line = self._description_lines[-1]
1267 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1268 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1269 self._description_lines.append('')
1270 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001271
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001272 def get_reviewers(self):
1273 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001274 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1275 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001276 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001277
1278
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001279def get_approving_reviewers(props):
1280 """Retrieves the reviewers that approved a CL from the issue properties with
1281 messages.
1282
1283 Note that the list may contain reviewers that are not committer, thus are not
1284 considered by the CQ.
1285 """
1286 return sorted(
1287 set(
1288 message['sender']
1289 for message in props['messages']
1290 if message['approval'] and message['sender'] in props['reviewers']
1291 )
1292 )
1293
1294
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001295def FindCodereviewSettingsFile(filename='codereview.settings'):
1296 """Finds the given file starting in the cwd and going up.
1297
1298 Only looks up to the top of the repository unless an
1299 'inherit-review-settings-ok' file exists in the root of the repository.
1300 """
1301 inherit_ok_file = 'inherit-review-settings-ok'
1302 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001303 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001304 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1305 root = '/'
1306 while True:
1307 if filename in os.listdir(cwd):
1308 if os.path.isfile(os.path.join(cwd, filename)):
1309 return open(os.path.join(cwd, filename))
1310 if cwd == root:
1311 break
1312 cwd = os.path.dirname(cwd)
1313
1314
1315def LoadCodereviewSettingsFromFile(fileobj):
1316 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001317 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001318
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001319 def SetProperty(name, setting, unset_error_ok=False):
1320 fullname = 'rietveld.' + name
1321 if setting in keyvals:
1322 RunGit(['config', fullname, keyvals[setting]])
1323 else:
1324 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1325
1326 SetProperty('server', 'CODE_REVIEW_SERVER')
1327 # Only server setting is required. Other settings can be absent.
1328 # In that case, we ignore errors raised during option deletion attempt.
1329 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001330 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001331 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1332 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001333 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001334 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001335 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1336 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001337 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001338 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001339 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001340 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1341 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001342
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001343 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001344 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001345
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001346 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1347 #should be of the form
1348 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1349 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1350 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1351 keyvals['ORIGIN_URL_CONFIG']])
1352
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001353
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001354def urlretrieve(source, destination):
1355 """urllib is broken for SSL connections via a proxy therefore we
1356 can't use urllib.urlretrieve()."""
1357 with open(destination, 'w') as f:
1358 f.write(urllib2.urlopen(source).read())
1359
1360
ukai@chromium.org712d6102013-11-27 00:52:58 +00001361def hasSheBang(fname):
1362 """Checks fname is a #! script."""
1363 with open(fname) as f:
1364 return f.read(2).startswith('#!')
1365
1366
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001367def DownloadHooks(force):
1368 """downloads hooks
1369
1370 Args:
1371 force: True to update hooks. False to install hooks if not present.
1372 """
1373 if not settings.GetIsGerrit():
1374 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001375 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001376 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1377 if not os.access(dst, os.X_OK):
1378 if os.path.exists(dst):
1379 if not force:
1380 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001381 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001382 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001383 if not hasSheBang(dst):
1384 DieWithError('Not a script: %s\n'
1385 'You need to download from\n%s\n'
1386 'into .git/hooks/commit-msg and '
1387 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001388 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1389 except Exception:
1390 if os.path.exists(dst):
1391 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001392 DieWithError('\nFailed to download hooks.\n'
1393 'You need to download from\n%s\n'
1394 'into .git/hooks/commit-msg and '
1395 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001396
1397
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001398@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001400 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001401
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001402 parser.add_option('--activate-update', action='store_true',
1403 help='activate auto-updating [rietveld] section in '
1404 '.git/config')
1405 parser.add_option('--deactivate-update', action='store_true',
1406 help='deactivate auto-updating [rietveld] section in '
1407 '.git/config')
1408 options, args = parser.parse_args(args)
1409
1410 if options.deactivate_update:
1411 RunGit(['config', 'rietveld.autoupdate', 'false'])
1412 return
1413
1414 if options.activate_update:
1415 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1416 return
1417
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001418 if len(args) == 0:
1419 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001420 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001421 return 0
1422
1423 url = args[0]
1424 if not url.endswith('codereview.settings'):
1425 url = os.path.join(url, 'codereview.settings')
1426
1427 # Load code review settings and download hooks (if available).
1428 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001429 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001430 return 0
1431
1432
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001433def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001434 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001435 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1436 branch = ShortBranchName(branchref)
1437 _, args = parser.parse_args(args)
1438 if not args:
1439 print("Current base-url:")
1440 return RunGit(['config', 'branch.%s.base-url' % branch],
1441 error_ok=False).strip()
1442 else:
1443 print("Setting base-url to %s" % args[0])
1444 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1445 error_ok=False).strip()
1446
1447
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001448def color_for_status(status):
1449 """Maps a Changelist status to color, for CMDstatus and other tools."""
1450 return {
1451 'unsent': Fore.RED,
1452 'waiting': Fore.BLUE,
1453 'reply': Fore.YELLOW,
1454 'lgtm': Fore.GREEN,
1455 'commit': Fore.MAGENTA,
1456 'closed': Fore.CYAN,
1457 'error': Fore.WHITE,
1458 }.get(status, Fore.WHITE)
1459
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001460def fetch_cl_status(b, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001461 """Fetches information for an issue and returns (branch, issue, color)."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001462 c = Changelist(branchref=b, auth_config=auth_config)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001463 i = c.GetIssueURL()
1464 status = c.GetStatus()
1465 color = color_for_status(status)
1466
1467 if i and (not status or status == 'error'):
1468 # The issue probably doesn't exist anymore.
1469 i += ' (broken)'
1470
1471 return (b, i, color)
1472
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001473def get_cl_statuses(
1474 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001475 """Returns a blocking iterable of (branch, issue, color) for given branches.
1476
1477 If fine_grained is true, this will fetch CL statuses from the server.
1478 Otherwise, simply indicate if there's a matching url for the given branches.
1479
1480 If max_processes is specified, it is used as the maximum number of processes
1481 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1482 spawned.
1483 """
1484 # Silence upload.py otherwise it becomes unwieldly.
1485 upload.verbosity = 0
1486
1487 if fine_grained:
1488 # Process one branch synchronously to work through authentication, then
1489 # spawn processes to process all the other branches in parallel.
1490 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001491 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1492 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001493
1494 branches_to_fetch = branches[1:]
1495 pool = ThreadPool(
1496 min(max_processes, len(branches_to_fetch))
1497 if max_processes is not None
1498 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001499 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001500 yield x
1501 else:
1502 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1503 for b in branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001504 c = Changelist(branchref=b, auth_config=auth_config)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001505 url = c.GetIssueURL()
1506 yield (b, url, Fore.BLUE if url else Fore.WHITE)
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001507
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001508def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001509 """Show status of changelists.
1510
1511 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001512 - Red not sent for review or broken
1513 - Blue waiting for review
1514 - Yellow waiting for you to reply to review
1515 - Green LGTM'ed
1516 - Magenta in the commit queue
1517 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001518
1519 Also see 'git cl comments'.
1520 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001521 parser.add_option('--field',
1522 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001523 parser.add_option('-f', '--fast', action='store_true',
1524 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001525 parser.add_option(
1526 '-j', '--maxjobs', action='store', type=int,
1527 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001528
1529 auth.add_auth_options(parser)
1530 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001531 if args:
1532 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001533 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001534
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001535 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001536 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001537 if options.field.startswith('desc'):
1538 print cl.GetDescription()
1539 elif options.field == 'id':
1540 issueid = cl.GetIssue()
1541 if issueid:
1542 print issueid
1543 elif options.field == 'patch':
1544 patchset = cl.GetPatchset()
1545 if patchset:
1546 print patchset
1547 elif options.field == 'url':
1548 url = cl.GetIssueURL()
1549 if url:
1550 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001551 return 0
1552
1553 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1554 if not branches:
1555 print('No local branch found.')
1556 return 0
1557
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001558 changes = (
1559 Changelist(branchref=b, auth_config=auth_config)
1560 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001561 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001562 alignment = max(5, max(len(b) for b in branches))
1563 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001564 output = get_cl_statuses(branches,
1565 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001566 max_processes=options.maxjobs,
1567 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001568
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001569 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001570 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001571 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001572 while branch not in branch_statuses:
1573 b, i, color = output.next()
1574 branch_statuses[b] = (i, color)
1575 issue, color = branch_statuses.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001576 reset = Fore.RESET
1577 if not sys.stdout.isatty():
1578 color = ''
1579 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001580 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001581 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001582
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001583 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001584 print
1585 print 'Current branch:',
1586 if not cl.GetIssue():
1587 print 'no issue assigned.'
1588 return 0
1589 print cl.GetBranch()
1590 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001591 if not options.fast:
1592 print 'Issue description:'
1593 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001594 return 0
1595
1596
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001597def colorize_CMDstatus_doc():
1598 """To be called once in main() to add colors to git cl status help."""
1599 colors = [i for i in dir(Fore) if i[0].isupper()]
1600
1601 def colorize_line(line):
1602 for color in colors:
1603 if color in line.upper():
1604 # Extract whitespaces first and the leading '-'.
1605 indent = len(line) - len(line.lstrip(' ')) + 1
1606 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1607 return line
1608
1609 lines = CMDstatus.__doc__.splitlines()
1610 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1611
1612
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001613@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001614def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001615 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001616
1617 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001618 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001619 parser.add_option('-r', '--reverse', action='store_true',
1620 help='Lookup the branch(es) for the specified issues. If '
1621 'no issues are specified, all branches with mapped '
1622 'issues will be listed.')
1623 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001624
dnj@chromium.org406c4402015-03-03 17:22:28 +00001625 if options.reverse:
1626 branches = RunGit(['for-each-ref', 'refs/heads',
1627 '--format=%(refname:short)']).splitlines()
1628
1629 # Reverse issue lookup.
1630 issue_branch_map = {}
1631 for branch in branches:
1632 cl = Changelist(branchref=branch)
1633 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1634 if not args:
1635 args = sorted(issue_branch_map.iterkeys())
1636 for issue in args:
1637 if not issue:
1638 continue
1639 print 'Branch for issue number %s: %s' % (
1640 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1641 else:
1642 cl = Changelist()
1643 if len(args) > 0:
1644 try:
1645 issue = int(args[0])
1646 except ValueError:
1647 DieWithError('Pass a number to set the issue or none to list it.\n'
1648 'Maybe you want to run git cl status?')
1649 cl.SetIssue(issue)
1650 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001651 return 0
1652
1653
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001654def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001655 """Shows or posts review comments for any changelist."""
1656 parser.add_option('-a', '--add-comment', dest='comment',
1657 help='comment to add to an issue')
1658 parser.add_option('-i', dest='issue',
1659 help="review issue id (defaults to current issue)")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001660 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001661 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001662 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001663
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001664 issue = None
1665 if options.issue:
1666 try:
1667 issue = int(options.issue)
1668 except ValueError:
1669 DieWithError('A review issue id is expected to be a number')
1670
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001671 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001672
1673 if options.comment:
1674 cl.AddComment(options.comment)
1675 return 0
1676
1677 data = cl.GetIssueProperties()
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001678 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001679 if message['disapproval']:
1680 color = Fore.RED
1681 elif message['approval']:
1682 color = Fore.GREEN
1683 elif message['sender'] == data['owner_email']:
1684 color = Fore.MAGENTA
1685 else:
1686 color = Fore.BLUE
1687 print '\n%s%s %s%s' % (
1688 color, message['date'].split('.', 1)[0], message['sender'],
1689 Fore.RESET)
1690 if message['text'].strip():
1691 print '\n'.join(' ' + l for l in message['text'].splitlines())
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001692 return 0
1693
1694
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001695def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001696 """Brings up the editor for the current CL's description."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001697 auth.add_auth_options(parser)
1698 options, _ = parser.parse_args(args)
1699 auth_config = auth.extract_auth_config_from_options(options)
1700 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001701 if not cl.GetIssue():
1702 DieWithError('This branch has no associated changelist.')
1703 description = ChangeDescription(cl.GetDescription())
1704 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00001705 if cl.GetDescription() != description.description:
1706 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001707 return 0
1708
1709
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001710def CreateDescriptionFromLog(args):
1711 """Pulls out the commit log to use as a base for the CL description."""
1712 log_args = []
1713 if len(args) == 1 and not args[0].endswith('.'):
1714 log_args = [args[0] + '..']
1715 elif len(args) == 1 and args[0].endswith('...'):
1716 log_args = [args[0][:-1]]
1717 elif len(args) == 2:
1718 log_args = [args[0] + '..' + args[1]]
1719 else:
1720 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001721 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001722
1723
thestig@chromium.org44202a22014-03-11 19:22:18 +00001724def CMDlint(parser, args):
1725 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001726 parser.add_option('--filter', action='append', metavar='-x,+y',
1727 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001728 auth.add_auth_options(parser)
1729 options, args = parser.parse_args(args)
1730 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001731
1732 # Access to a protected member _XX of a client class
1733 # pylint: disable=W0212
1734 try:
1735 import cpplint
1736 import cpplint_chromium
1737 except ImportError:
1738 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1739 return 1
1740
1741 # Change the current working directory before calling lint so that it
1742 # shows the correct base.
1743 previous_cwd = os.getcwd()
1744 os.chdir(settings.GetRoot())
1745 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001746 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001747 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1748 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001749 if not files:
1750 print "Cannot lint an empty CL"
1751 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001752
1753 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001754 command = args + files
1755 if options.filter:
1756 command = ['--filter=' + ','.join(options.filter)] + command
1757 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001758
1759 white_regex = re.compile(settings.GetLintRegex())
1760 black_regex = re.compile(settings.GetLintIgnoreRegex())
1761 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1762 for filename in filenames:
1763 if white_regex.match(filename):
1764 if black_regex.match(filename):
1765 print "Ignoring file %s" % filename
1766 else:
1767 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1768 extra_check_functions)
1769 else:
1770 print "Skipping file %s" % filename
1771 finally:
1772 os.chdir(previous_cwd)
1773 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1774 if cpplint._cpplint_state.error_count != 0:
1775 return 1
1776 return 0
1777
1778
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001779def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001780 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001781 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001782 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001783 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001784 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001785 auth.add_auth_options(parser)
1786 options, args = parser.parse_args(args)
1787 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001788
sbc@chromium.org71437c02015-04-09 19:29:40 +00001789 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00001790 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001791 return 1
1792
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001793 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001794 if args:
1795 base_branch = args[0]
1796 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001797 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001798 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001799
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001800 cl.RunHook(
1801 committing=not options.upload,
1802 may_prompt=False,
1803 verbose=options.verbose,
1804 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001805 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001806
1807
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001808def AddChangeIdToCommitMessage(options, args):
1809 """Re-commits using the current message, assumes the commit hook is in
1810 place.
1811 """
1812 log_desc = options.message or CreateDescriptionFromLog(args)
1813 git_command = ['commit', '--amend', '-m', log_desc]
1814 RunGit(git_command)
1815 new_log_desc = CreateDescriptionFromLog(args)
1816 if CHANGE_ID in new_log_desc:
1817 print 'git-cl: Added Change-Id to commit message.'
1818 else:
1819 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1820
1821
piman@chromium.org336f9122014-09-04 02:16:55 +00001822def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001823 """upload the current branch to gerrit."""
1824 # We assume the remote called "origin" is the one we want.
1825 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001826 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00001827
1828 remote, remote_branch = cl.GetRemoteBranch()
1829 branch = GetTargetRef(remote, remote_branch, options.target_branch,
1830 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001831
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001832 change_desc = ChangeDescription(
1833 options.message or CreateDescriptionFromLog(args))
1834 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001835 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001836 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001837
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001838 if options.squash:
1839 # Try to get the message from a previous upload.
1840 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
1841 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
1842 if not message:
1843 if not options.force:
1844 change_desc.prompt()
1845
1846 if CHANGE_ID not in change_desc.description:
1847 # Run the commit-msg hook without modifying the head commit by writing
1848 # the commit message to a temporary file and running the hook over it,
1849 # then reading the file back in.
1850 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
1851 'commit-msg')
1852 file_handle, msg_file = tempfile.mkstemp(text=True,
1853 prefix='commit_msg')
1854 try:
1855 try:
1856 with os.fdopen(file_handle, 'w') as fileobj:
1857 fileobj.write(change_desc.description)
1858 finally:
1859 os.close(file_handle)
1860 RunCommand([commit_msg_hook, msg_file])
1861 change_desc.set_description(gclient_utils.FileRead(msg_file))
1862 finally:
1863 os.remove(msg_file)
1864
1865 if not change_desc.description:
1866 print "Description is empty; aborting."
1867 return 1
1868
1869 message = change_desc.description
1870
1871 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1872 if remote is '.':
1873 # If our upstream branch is local, we base our squashed commit on its
1874 # squashed version.
1875 parent = ('refs/heads/git_cl_uploads/' +
1876 scm.GIT.ShortBranchName(upstream_branch))
1877
1878 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
1879 # will create additional CLs when uploading.
1880 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
1881 RunGitSilent(['rev-parse', parent + ':'])):
1882 print 'Upload upstream branch ' + upstream_branch + ' first.'
1883 return 1
1884 else:
1885 parent = cl.GetCommonAncestorWithUpstream()
1886
1887 tree = RunGit(['rev-parse', 'HEAD:']).strip()
1888 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
1889 '-m', message]).strip()
1890 else:
1891 if CHANGE_ID not in change_desc.description:
1892 AddChangeIdToCommitMessage(options, args)
1893 ref_to_push = 'HEAD'
1894 parent = '%s/%s' % (gerrit_remote, branch)
1895
1896 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
1897 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001898 if len(commits) > 1:
1899 print('WARNING: This will upload %d commits. Run the following command '
1900 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001901 print('git log %s..%s' % (parent, ref_to_push))
1902 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001903 'commit.')
1904 ask_for_data('About to upload; enter to confirm.')
1905
piman@chromium.org336f9122014-09-04 02:16:55 +00001906 if options.reviewers or options.tbr_owners:
1907 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001908
ukai@chromium.orge8077812012-02-03 03:41:46 +00001909 receive_options = []
1910 cc = cl.GetCCList().split(',')
1911 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001912 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001913 cc = filter(None, cc)
1914 if cc:
1915 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001916 if change_desc.get_reviewers():
1917 receive_options.extend(
1918 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001919
ukai@chromium.orge8077812012-02-03 03:41:46 +00001920 git_command = ['push']
1921 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001922 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001923 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001924 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00001925 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001926
1927 if options.squash:
1928 head = RunGit(['rev-parse', 'HEAD']).strip()
1929 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
1930
ukai@chromium.orge8077812012-02-03 03:41:46 +00001931 # TODO(ukai): parse Change-Id: and set issue number?
1932 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001933
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001934
wittman@chromium.org455dc922015-01-26 20:15:50 +00001935def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
1936 """Computes the remote branch ref to use for the CL.
1937
1938 Args:
1939 remote (str): The git remote for the CL.
1940 remote_branch (str): The git remote branch for the CL.
1941 target_branch (str): The target branch specified by the user.
1942 pending_prefix (str): The pending prefix from the settings.
1943 """
1944 if not (remote and remote_branch):
1945 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001946
wittman@chromium.org455dc922015-01-26 20:15:50 +00001947 if target_branch:
1948 # Cannonicalize branch references to the equivalent local full symbolic
1949 # refs, which are then translated into the remote full symbolic refs
1950 # below.
1951 if '/' not in target_branch:
1952 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
1953 else:
1954 prefix_replacements = (
1955 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
1956 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
1957 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
1958 )
1959 match = None
1960 for regex, replacement in prefix_replacements:
1961 match = re.search(regex, target_branch)
1962 if match:
1963 remote_branch = target_branch.replace(match.group(0), replacement)
1964 break
1965 if not match:
1966 # This is a branch path but not one we recognize; use as-is.
1967 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00001968 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
1969 # Handle the refs that need to land in different refs.
1970 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001971
wittman@chromium.org455dc922015-01-26 20:15:50 +00001972 # Create the true path to the remote branch.
1973 # Does the following translation:
1974 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
1975 # * refs/remotes/origin/master -> refs/heads/master
1976 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
1977 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
1978 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
1979 elif remote_branch.startswith('refs/remotes/%s/' % remote):
1980 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
1981 'refs/heads/')
1982 elif remote_branch.startswith('refs/remotes/branch-heads'):
1983 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
1984 # If a pending prefix exists then replace refs/ with it.
1985 if pending_prefix:
1986 remote_branch = remote_branch.replace('refs/', pending_prefix)
1987 return remote_branch
1988
1989
piman@chromium.org336f9122014-09-04 02:16:55 +00001990def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001991 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001992 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1993 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001994 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001995 if options.emulate_svn_auto_props:
1996 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001997
1998 change_desc = None
1999
pgervais@chromium.org91141372014-01-09 23:27:20 +00002000 if options.email is not None:
2001 upload_args.extend(['--email', options.email])
2002
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002003 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002004 if options.title:
2005 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002006 if options.message:
2007 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002008 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002009 print ("This branch is associated with issue %s. "
2010 "Adding patch to that issue." % cl.GetIssue())
2011 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002012 if options.title:
2013 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002014 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002015 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002016 if options.reviewers or options.tbr_owners:
2017 change_desc.update_reviewers(options.reviewers,
2018 options.tbr_owners,
2019 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002020 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002021 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002022
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002023 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002024 print "Description is empty; aborting."
2025 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002026
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002027 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002028 if change_desc.get_reviewers():
2029 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002030 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002031 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002032 DieWithError("Must specify reviewers to send email.")
2033 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002034
2035 # We check this before applying rietveld.private assuming that in
2036 # rietveld.cc only addresses which we can send private CLs to are listed
2037 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2038 # --private is specified explicitly on the command line.
2039 if options.private:
2040 logging.warn('rietveld.cc is ignored since private flag is specified. '
2041 'You need to review and add them manually if necessary.')
2042 cc = cl.GetCCListWithoutDefault()
2043 else:
2044 cc = cl.GetCCList()
2045 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002046 if cc:
2047 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002048
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002049 if options.private or settings.GetDefaultPrivateFlag() == "True":
2050 upload_args.append('--private')
2051
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002052 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002053 if not options.find_copies:
2054 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002055
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002056 # Include the upstream repo's URL in the change -- this is useful for
2057 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002058 remote_url = cl.GetGitBaseUrlFromConfig()
2059 if not remote_url:
2060 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002061 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002062 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002063 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2064 remote_url = (cl.GetRemoteUrl() + '@'
2065 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002066 if remote_url:
2067 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002068 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002069 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2070 settings.GetPendingRefPrefix())
2071 if target_ref:
2072 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002073
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002074 project = settings.GetProject()
2075 if project:
2076 upload_args.extend(['--project', project])
2077
rmistry@google.comef966222015-04-07 11:15:01 +00002078 if options.cq_dry_run:
2079 upload_args.extend(['--cq_dry_run'])
2080
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002081 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002082 upload_args = ['upload'] + upload_args + args
2083 logging.info('upload.RealMain(%s)', upload_args)
2084 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002085 issue = int(issue)
2086 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002087 except KeyboardInterrupt:
2088 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002089 except:
2090 # If we got an exception after the user typed a description for their
2091 # change, back up the description before re-raising.
2092 if change_desc:
2093 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2094 print '\nGot exception while uploading -- saving description to %s\n' \
2095 % backup_path
2096 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002097 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002098 backup_file.close()
2099 raise
2100
2101 if not cl.GetIssue():
2102 cl.SetIssue(issue)
2103 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002104
2105 if options.use_commit_queue:
2106 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002107 return 0
2108
2109
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002110def cleanup_list(l):
2111 """Fixes a list so that comma separated items are put as individual items.
2112
2113 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2114 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2115 """
2116 items = sum((i.split(',') for i in l), [])
2117 stripped_items = (i.strip() for i in items)
2118 return sorted(filter(None, stripped_items))
2119
2120
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002121@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002122def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002123 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00002124 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2125 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002126 parser.add_option('--bypass-watchlists', action='store_true',
2127 dest='bypass_watchlists',
2128 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002129 parser.add_option('-f', action='store_true', dest='force',
2130 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002131 parser.add_option('-m', dest='message', help='message for patchset')
2132 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002133 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002134 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002135 help='reviewer email addresses')
2136 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002137 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002138 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002139 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002140 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002141 parser.add_option('--emulate_svn_auto_props',
2142 '--emulate-svn-auto-props',
2143 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002144 dest="emulate_svn_auto_props",
2145 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002146 parser.add_option('-c', '--use-commit-queue', action='store_true',
2147 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002148 parser.add_option('--private', action='store_true',
2149 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002150 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002151 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002152 metavar='TARGET',
2153 help='Apply CL to remote ref TARGET. ' +
2154 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002155 parser.add_option('--squash', action='store_true',
2156 help='Squash multiple commits into one (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002157 parser.add_option('--email', default=None,
2158 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002159 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2160 help='add a set of OWNERS to TBR')
rmistry@google.comef966222015-04-07 11:15:01 +00002161 parser.add_option('--cq-dry-run', dest='cq_dry_run', action='store_true',
2162 help='Send the patchset to do a CQ dry run right after '
2163 'upload.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002164
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002165 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002166 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002167 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002168 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002169
sbc@chromium.org71437c02015-04-09 19:29:40 +00002170 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002171 return 1
2172
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002173 options.reviewers = cleanup_list(options.reviewers)
2174 options.cc = cleanup_list(options.cc)
2175
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002176 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002177 if args:
2178 # TODO(ukai): is it ok for gerrit case?
2179 base_branch = args[0]
2180 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002181 if cl.GetBranch() is None:
2182 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2183
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002184 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002185 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002186 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002187
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002188 # Make sure authenticated to Rietveld before running expensive hooks. It is
2189 # a fast, best efforts check. Rietveld still can reject the authentication
2190 # during the actual upload.
2191 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2192 authenticator = auth.get_authenticator_for_host(
2193 cl.GetRietveldServer(), auth_config)
2194 if not authenticator.has_cached_credentials():
2195 raise auth.LoginRequiredError(cl.GetRietveldServer())
2196
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002197 # Apply watchlists on upload.
2198 change = cl.GetChange(base_branch, None)
2199 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2200 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002201 if not options.bypass_watchlists:
2202 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002203
ukai@chromium.orge8077812012-02-03 03:41:46 +00002204 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002205 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002206 # Set the reviewer list now so that presubmit checks can access it.
2207 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002208 change_description.update_reviewers(options.reviewers,
2209 options.tbr_owners,
2210 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002211 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002212 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002213 may_prompt=not options.force,
2214 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002215 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002216 if not hook_results.should_continue():
2217 return 1
2218 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002219 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002220
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002221 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002222 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002223 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002224 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002225 print ('The last upload made from this repository was patchset #%d but '
2226 'the most recent patchset on the server is #%d.'
2227 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002228 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2229 'from another machine or branch the patch you\'re uploading now '
2230 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002231 ask_for_data('About to upload; enter to confirm.')
2232
iannucci@chromium.org79540052012-10-19 23:15:26 +00002233 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002234 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00002235 return GerritUpload(options, args, cl, change)
2236 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002237 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002238 git_set_branch_value('last-upload-hash',
2239 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002240 # Run post upload hooks, if specified.
2241 if settings.GetRunPostUploadHook():
2242 presubmit_support.DoPostUploadExecuter(
2243 change,
2244 cl,
2245 settings.GetRoot(),
2246 options.verbose,
2247 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002248
2249 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002250
2251
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002252def IsSubmoduleMergeCommit(ref):
2253 # When submodules are added to the repo, we expect there to be a single
2254 # non-git-svn merge commit at remote HEAD with a signature comment.
2255 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002256 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002257 return RunGit(cmd) != ''
2258
2259
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002260def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002261 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002262
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002263 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002264 Updates changelog with metadata (e.g. pointer to review).
2265 Pushes/dcommits the code upstream.
2266 Updates review and closes.
2267 """
2268 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2269 help='bypass upload presubmit hook')
2270 parser.add_option('-m', dest='message',
2271 help="override review description")
2272 parser.add_option('-f', action='store_true', dest='force',
2273 help="force yes to questions (don't prompt)")
2274 parser.add_option('-c', dest='contributor',
2275 help="external contributor for patch (appended to " +
2276 "description and used as author for git). Should be " +
2277 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002278 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002279 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002280 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002281 auth_config = auth.extract_auth_config_from_options(options)
2282
2283 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002284
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002285 current = cl.GetBranch()
2286 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2287 if not settings.GetIsGitSvn() and remote == '.':
2288 print
2289 print 'Attempting to push branch %r into another local branch!' % current
2290 print
2291 print 'Either reparent this branch on top of origin/master:'
2292 print ' git reparent-branch --root'
2293 print
2294 print 'OR run `git rebase-update` if you think the parent branch is already'
2295 print 'committed.'
2296 print
2297 print ' Current parent: %r' % upstream_branch
2298 return 1
2299
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002300 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002301 # Default to merging against our best guess of the upstream branch.
2302 args = [cl.GetUpstreamBranch()]
2303
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002304 if options.contributor:
2305 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2306 print "Please provide contibutor as 'First Last <email@example.com>'"
2307 return 1
2308
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002309 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002310 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002311
sbc@chromium.org71437c02015-04-09 19:29:40 +00002312 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002313 return 1
2314
2315 # This rev-list syntax means "show all commits not in my branch that
2316 # are in base_branch".
2317 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2318 base_branch]).splitlines()
2319 if upstream_commits:
2320 print ('Base branch "%s" has %d commits '
2321 'not in this branch.' % (base_branch, len(upstream_commits)))
2322 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2323 return 1
2324
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002325 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002326 svn_head = None
2327 if cmd == 'dcommit' or base_has_submodules:
2328 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2329 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002330
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002331 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002332 # If the base_head is a submodule merge commit, the first parent of the
2333 # base_head should be a git-svn commit, which is what we're interested in.
2334 base_svn_head = base_branch
2335 if base_has_submodules:
2336 base_svn_head += '^1'
2337
2338 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002339 if extra_commits:
2340 print ('This branch has %d additional commits not upstreamed yet.'
2341 % len(extra_commits.splitlines()))
2342 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2343 'before attempting to %s.' % (base_branch, cmd))
2344 return 1
2345
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002346 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002347 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002348 author = None
2349 if options.contributor:
2350 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002351 hook_results = cl.RunHook(
2352 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002353 may_prompt=not options.force,
2354 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002355 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002356 if not hook_results.should_continue():
2357 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002358
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002359 # Check the tree status if the tree status URL is set.
2360 status = GetTreeStatus()
2361 if 'closed' == status:
2362 print('The tree is closed. Please wait for it to reopen. Use '
2363 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2364 return 1
2365 elif 'unknown' == status:
2366 print('Unable to determine tree status. Please verify manually and '
2367 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2368 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002369 else:
2370 breakpad.SendStack(
2371 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002372 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2373 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002374 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002375
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002376 change_desc = ChangeDescription(options.message)
2377 if not change_desc.description and cl.GetIssue():
2378 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002379
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002380 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002381 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002382 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002383 else:
2384 print 'No description set.'
2385 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2386 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002387
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002388 # Keep a separate copy for the commit message, because the commit message
2389 # contains the link to the Rietveld issue, while the Rietveld message contains
2390 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002391 # Keep a separate copy for the commit message.
2392 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002393 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002394
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002395 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002396 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002397 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002398 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002399 commit_desc.append_footer('Patch from %s.' % options.contributor)
2400
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002401 print('Description:')
2402 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002403
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002404 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002405 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002406 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002407
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002408 # We want to squash all this branch's commits into one commit with the proper
2409 # description. We do this by doing a "reset --soft" to the base branch (which
2410 # keeps the working copy the same), then dcommitting that. If origin/master
2411 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2412 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002413 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002414 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2415 # Delete the branches if they exist.
2416 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2417 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2418 result = RunGitWithCode(showref_cmd)
2419 if result[0] == 0:
2420 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002421
2422 # We might be in a directory that's present in this branch but not in the
2423 # trunk. Move up to the top of the tree so that git commands that expect a
2424 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002425 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002426 if rel_base_path:
2427 os.chdir(rel_base_path)
2428
2429 # Stuff our change into the merge branch.
2430 # We wrap in a try...finally block so if anything goes wrong,
2431 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002432 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002433 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002434 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002435 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002436 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002437 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002438 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002439 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002440 RunGit(
2441 [
2442 'commit', '--author', options.contributor,
2443 '-m', commit_desc.description,
2444 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002445 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002446 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002447 if base_has_submodules:
2448 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2449 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2450 RunGit(['checkout', CHERRY_PICK_BRANCH])
2451 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002452 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002453 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002454 pending_prefix = settings.GetPendingRefPrefix()
2455 if not pending_prefix or branch.startswith(pending_prefix):
2456 # If not using refs/pending/heads/* at all, or target ref is already set
2457 # to pending, then push to the target ref directly.
2458 retcode, output = RunGitWithCode(
2459 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002460 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002461 else:
2462 # Cherry-pick the change on top of pending ref and then push it.
2463 assert branch.startswith('refs/'), branch
2464 assert pending_prefix[-1] == '/', pending_prefix
2465 pending_ref = pending_prefix + branch[len('refs/'):]
2466 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002467 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002468 if retcode == 0:
2469 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002470 else:
2471 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002472 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002473 'svn', 'dcommit',
2474 '-C%s' % options.similarity,
2475 '--no-rebase', '--rmdir',
2476 ]
2477 if settings.GetForceHttpsCommitUrl():
2478 # Allow forcing https commit URLs for some projects that don't allow
2479 # committing to http URLs (like Google Code).
2480 remote_url = cl.GetGitSvnRemoteUrl()
2481 if urlparse.urlparse(remote_url).scheme == 'http':
2482 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002483 cmd_args.append('--commit-url=%s' % remote_url)
2484 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002485 if 'Committed r' in output:
2486 revision = re.match(
2487 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2488 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002489 finally:
2490 # And then swap back to the original branch and clean up.
2491 RunGit(['checkout', '-q', cl.GetBranch()])
2492 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002493 if base_has_submodules:
2494 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002495
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002496 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002497 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002498 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002499
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002500 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002501 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002502 try:
2503 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2504 # We set pushed_to_pending to False, since it made it all the way to the
2505 # real ref.
2506 pushed_to_pending = False
2507 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002508 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002509
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002510 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002511 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002512 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002513 if not to_pending:
2514 if viewvc_url and revision:
2515 change_desc.append_footer(
2516 'Committed: %s%s' % (viewvc_url, revision))
2517 elif revision:
2518 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002519 print ('Closing issue '
2520 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002521 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002522 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002523 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002524 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002525 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002526 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002527 if options.bypass_hooks:
2528 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2529 else:
2530 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002531 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002532 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002533
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002534 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002535 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2536 print 'The commit is in the pending queue (%s).' % pending_ref
2537 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002538 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002539 'footer.' % branch)
2540
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002541 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2542 if os.path.isfile(hook):
2543 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002544
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002545 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002546
2547
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002548def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2549 print
2550 print 'Waiting for commit to be landed on %s...' % real_ref
2551 print '(If you are impatient, you may Ctrl-C once without harm)'
2552 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2553 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2554
2555 loop = 0
2556 while True:
2557 sys.stdout.write('fetching (%d)... \r' % loop)
2558 sys.stdout.flush()
2559 loop += 1
2560
2561 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2562 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2563 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2564 for commit in commits.splitlines():
2565 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2566 print 'Found commit on %s' % real_ref
2567 return commit
2568
2569 current_rev = to_rev
2570
2571
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002572def PushToGitPending(remote, pending_ref, upstream_ref):
2573 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2574
2575 Returns:
2576 (retcode of last operation, output log of last operation).
2577 """
2578 assert pending_ref.startswith('refs/'), pending_ref
2579 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2580 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2581 code = 0
2582 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002583 max_attempts = 3
2584 attempts_left = max_attempts
2585 while attempts_left:
2586 if attempts_left != max_attempts:
2587 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2588 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002589
2590 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002591 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002592 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002593 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002594 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002595 print 'Fetch failed with exit code %d.' % code
2596 if out.strip():
2597 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002598 continue
2599
2600 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002601 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002602 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002603 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002604 if code:
2605 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002606 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2607 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002608 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2609 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002610 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002611 return code, out
2612
2613 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002614 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002615 code, out = RunGitWithCode(
2616 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2617 if code == 0:
2618 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002619 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002620 return code, out
2621
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002622 print 'Push failed with exit code %d.' % code
2623 if out.strip():
2624 print out.strip()
2625 if IsFatalPushFailure(out):
2626 print (
2627 'Fatal push error. Make sure your .netrc credentials and git '
2628 'user.email are correct and you have push access to the repo.')
2629 return code, out
2630
2631 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002632 return code, out
2633
2634
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002635def IsFatalPushFailure(push_stdout):
2636 """True if retrying push won't help."""
2637 return '(prohibited by Gerrit)' in push_stdout
2638
2639
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002640@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002641def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002642 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002643 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002644 message = """This doesn't appear to be an SVN repository.
2645If your project has a git mirror with an upstream SVN master, you probably need
2646to run 'git svn init', see your project's git mirror documentation.
2647If your project has a true writeable upstream repository, you probably want
mark@chromium.org671c7a32014-07-31 17:09:36 +00002648to run 'git cl land' instead.
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002649Choose wisely, if you get this wrong, your commit might appear to succeed but
2650will instead be silently ignored."""
2651 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002652 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002653 return SendUpstream(parser, args, 'dcommit')
2654
2655
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002656@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002657def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002658 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002659 if settings.GetIsGitSvn():
2660 print('This appears to be an SVN repository.')
2661 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002662 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002663 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002664
2665
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002666@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002667def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002668 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002669 parser.add_option('-b', dest='newbranch',
2670 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002671 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002672 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002673 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2674 help='Change to the directory DIR immediately, '
2675 'before doing anything else.')
2676 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002677 help='failed patches spew .rej files rather than '
2678 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002679 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2680 help="don't commit after patch applies")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002681 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002682 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002683 auth_config = auth.extract_auth_config_from_options(options)
2684
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002685 if len(args) != 1:
2686 parser.print_help()
2687 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002688 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002689
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002690 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002691 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002692 return 1
2693
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002694 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002695 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002696
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002697 if options.newbranch:
2698 if options.force:
2699 RunGit(['branch', '-D', options.newbranch],
2700 stderr=subprocess2.PIPE, error_ok=True)
2701 RunGit(['checkout', '-b', options.newbranch,
2702 Changelist().GetUpstreamBranch()])
2703
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002704 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002705 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002706
2707
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002708def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00002709 # PatchIssue should never be called with a dirty tree. It is up to the
2710 # caller to check this, but just in case we assert here since the
2711 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002712 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002713
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002714 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002715 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002716 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002717 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002718 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002719 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002720 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002721 # Assume it's a URL to the patch. Default to https.
2722 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002723 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002724 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002725 DieWithError('Must pass an issue ID or full URL for '
2726 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002727 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002728 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002729 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002730
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002731 # Switch up to the top-level directory, if necessary, in preparation for
2732 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002733 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002734 if top:
2735 os.chdir(top)
2736
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002737 # Git patches have a/ at the beginning of source paths. We strip that out
2738 # with a sed script rather than the -p flag to patch so we can feed either
2739 # Git or svn-style patches into the same apply command.
2740 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002741 try:
2742 patch_data = subprocess2.check_output(
2743 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2744 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002745 DieWithError('Git patch mungling failed.')
2746 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002747
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002748 # We use "git apply" to apply the patch instead of "patch" so that we can
2749 # pick up file adds.
2750 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002751 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002752 if directory:
2753 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002754 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002755 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002756 elif IsGitVersionAtLeast('1.7.12'):
2757 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002758 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002759 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002760 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002761 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00002762 print 'Failed to apply the patch'
2763 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002764
2765 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002766 if not nocommit:
carlosk@chromium.org71284d92014-11-14 18:12:50 +00002767 RunGit(['commit', '-m', ('patch from issue %(i)s at patchset '
2768 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2769 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002770 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002771 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002772 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002773 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002774 else:
2775 print "Patch applied to index."
2776 return 0
2777
2778
2779def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002780 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002781 # Provide a wrapper for git svn rebase to help avoid accidental
2782 # git svn dcommit.
2783 # It's the only command that doesn't use parser at all since we just defer
2784 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002785
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002786 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002787
2788
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002789def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002790 """Fetches the tree status and returns either 'open', 'closed',
2791 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002792 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002793 if url:
2794 status = urllib2.urlopen(url).read().lower()
2795 if status.find('closed') != -1 or status == '0':
2796 return 'closed'
2797 elif status.find('open') != -1 or status == '1':
2798 return 'open'
2799 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002800 return 'unset'
2801
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002802
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002803def GetTreeStatusReason():
2804 """Fetches the tree status from a json url and returns the message
2805 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002806 url = settings.GetTreeStatusUrl()
2807 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002808 connection = urllib2.urlopen(json_url)
2809 status = json.loads(connection.read())
2810 connection.close()
2811 return status['message']
2812
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002813
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002814def GetBuilderMaster(bot_list):
2815 """For a given builder, fetch the master from AE if available."""
2816 map_url = 'https://builders-map.appspot.com/'
2817 try:
2818 master_map = json.load(urllib2.urlopen(map_url))
2819 except urllib2.URLError as e:
2820 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2821 (map_url, e))
2822 except ValueError as e:
2823 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2824 if not master_map:
2825 return None, 'Failed to build master map.'
2826
2827 result_master = ''
2828 for bot in bot_list:
2829 builder = bot.split(':', 1)[0]
2830 master_list = master_map.get(builder, [])
2831 if not master_list:
2832 return None, ('No matching master for builder %s.' % builder)
2833 elif len(master_list) > 1:
2834 return None, ('The builder name %s exists in multiple masters %s.' %
2835 (builder, master_list))
2836 else:
2837 cur_master = master_list[0]
2838 if not result_master:
2839 result_master = cur_master
2840 elif result_master != cur_master:
2841 return None, 'The builders do not belong to the same master.'
2842 return result_master, None
2843
2844
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002845def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002846 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002847 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002848 status = GetTreeStatus()
2849 if 'unset' == status:
2850 print 'You must configure your tree status URL by running "git cl config".'
2851 return 2
2852
2853 print "The tree is %s" % status
2854 print
2855 print GetTreeStatusReason()
2856 if status != 'open':
2857 return 1
2858 return 0
2859
2860
maruel@chromium.org15192402012-09-06 12:38:29 +00002861def CMDtry(parser, args):
2862 """Triggers a try job through Rietveld."""
2863 group = optparse.OptionGroup(parser, "Try job options")
2864 group.add_option(
2865 "-b", "--bot", action="append",
2866 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2867 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002868 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002869 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002870 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00002871 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002872 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002873 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002874 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002875 "-r", "--revision",
2876 help="Revision to use for the try job; default: the "
2877 "revision will be determined by the try server; see "
2878 "its waterfall for more info")
2879 group.add_option(
2880 "-c", "--clobber", action="store_true", default=False,
2881 help="Force a clobber before building; e.g. don't do an "
2882 "incremental build")
2883 group.add_option(
2884 "--project",
2885 help="Override which project to use. Projects are defined "
2886 "server-side to define what default bot set to use")
2887 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002888 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00002889 group.add_option(
2890 "--use-buildbucket", action="store_true", default=False,
2891 help="Use buildbucket to trigger try jobs.")
maruel@chromium.org15192402012-09-06 12:38:29 +00002892 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002893 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00002894 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002895 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00002896
2897 if args:
2898 parser.error('Unknown arguments: %s' % args)
2899
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002900 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00002901 if not cl.GetIssue():
2902 parser.error('Need to upload first')
2903
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002904 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002905 if props.get('closed'):
2906 parser.error('Cannot send tryjobs for a closed CL')
2907
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002908 if props.get('private'):
2909 parser.error('Cannot use trybots with private issue')
2910
maruel@chromium.org15192402012-09-06 12:38:29 +00002911 if not options.name:
2912 options.name = cl.GetBranch()
2913
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002914 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002915 options.master, err_msg = GetBuilderMaster(options.bot)
2916 if err_msg:
2917 parser.error('Tryserver master cannot be found because: %s\n'
2918 'Please manually specify the tryserver master'
2919 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002920
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002921 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002922 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002923 if not options.bot:
2924 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002925
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002926 # Get try masters from PRESUBMIT.py files.
2927 masters = presubmit_support.DoGetTryMasters(
2928 change,
2929 change.LocalPaths(),
2930 settings.GetRoot(),
2931 None,
2932 None,
2933 options.verbose,
2934 sys.stdout)
2935 if masters:
2936 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002937
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002938 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2939 options.bot = presubmit_support.DoGetTrySlaves(
2940 change,
2941 change.LocalPaths(),
2942 settings.GetRoot(),
2943 None,
2944 None,
2945 options.verbose,
2946 sys.stdout)
2947 if not options.bot:
2948 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002949
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002950 builders_and_tests = {}
2951 # TODO(machenbach): The old style command-line options don't support
2952 # multiple try masters yet.
2953 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2954 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2955
2956 for bot in old_style:
2957 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002958 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002959 elif ',' in bot:
2960 parser.error('Specify one bot per --bot flag')
2961 else:
2962 builders_and_tests.setdefault(bot, []).append('defaulttests')
2963
2964 for bot, tests in new_style:
2965 builders_and_tests.setdefault(bot, []).extend(tests)
2966
2967 # Return a master map with one master to be backwards compatible. The
2968 # master name defaults to an empty string, which will cause the master
2969 # not to be set on rietveld (deprecated).
2970 return {options.master: builders_and_tests}
2971
2972 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002973
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002974 for builders in masters.itervalues():
2975 if any('triggered' in b for b in builders):
2976 print >> sys.stderr, (
2977 'ERROR You are trying to send a job to a triggered bot. This type of'
2978 ' bot requires an\ninitial job from a parent (usually a builder). '
2979 'Instead send your job to the parent.\n'
2980 'Bot list: %s' % builders)
2981 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002982
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002983 patchset = cl.GetMostRecentPatchset()
2984 if patchset and patchset != cl.GetPatchset():
2985 print(
2986 '\nWARNING Mismatch between local config and server. Did a previous '
2987 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2988 'Continuing using\npatchset %s.\n' % patchset)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00002989 if options.use_buildbucket:
2990 try:
2991 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
2992 except BuildbucketResponseException as ex:
2993 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002994 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00002995 except Exception as e:
2996 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
2997 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
2998 e, stacktrace)
2999 return 1
3000 else:
3001 try:
3002 cl.RpcServer().trigger_distributed_try_jobs(
3003 cl.GetIssue(), patchset, options.name, options.clobber,
3004 options.revision, masters)
3005 except urllib2.HTTPError as e:
3006 if e.code == 404:
3007 print('404 from rietveld; '
3008 'did you mean to use "git try" instead of "git cl try"?')
3009 return 1
3010 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003011
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003012 for (master, builders) in sorted(masters.iteritems()):
3013 if master:
3014 print 'Master: %s' % master
3015 length = max(len(builder) for builder in builders)
3016 for builder in sorted(builders):
3017 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003018 return 0
3019
3020
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003021@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003022def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003023 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003024 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003025 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003026 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003027
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003028 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003029 if args:
3030 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003031 branch = cl.GetBranch()
3032 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003033 cl = Changelist()
3034 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003035
3036 # Clear configured merge-base, if there is one.
3037 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003038 else:
3039 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003040 return 0
3041
3042
thestig@chromium.org00858c82013-12-02 23:08:03 +00003043def CMDweb(parser, args):
3044 """Opens the current CL in the web browser."""
3045 _, args = parser.parse_args(args)
3046 if args:
3047 parser.error('Unrecognized args: %s' % ' '.join(args))
3048
3049 issue_url = Changelist().GetIssueURL()
3050 if not issue_url:
3051 print >> sys.stderr, 'ERROR No issue to open'
3052 return 1
3053
3054 webbrowser.open(issue_url)
3055 return 0
3056
3057
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003058def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003059 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003060 auth.add_auth_options(parser)
3061 options, args = parser.parse_args(args)
3062 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003063 if args:
3064 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003065 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003066 props = cl.GetIssueProperties()
3067 if props.get('private'):
3068 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003069 cl.SetFlag('commit', '1')
3070 return 0
3071
3072
groby@chromium.org411034a2013-02-26 15:12:01 +00003073def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003074 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003075 auth.add_auth_options(parser)
3076 options, args = parser.parse_args(args)
3077 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003078 if args:
3079 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003080 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003081 # Ensure there actually is an issue to close.
3082 cl.GetDescription()
3083 cl.CloseIssue()
3084 return 0
3085
3086
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003087def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003088 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003089 auth.add_auth_options(parser)
3090 options, args = parser.parse_args(args)
3091 auth_config = auth.extract_auth_config_from_options(options)
3092 if args:
3093 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003094
3095 # Uncommitted (staged and unstaged) changes will be destroyed by
3096 # "git reset --hard" if there are merging conflicts in PatchIssue().
3097 # Staged changes would be committed along with the patch from last
3098 # upload, hence counted toward the "last upload" side in the final
3099 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003100 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003101 return 1
3102
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003103 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003104 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003105 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003106 if not issue:
3107 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003108 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003109 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003110
3111 # Create a new branch based on the merge-base
3112 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3113 try:
3114 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003115 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003116 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003117 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003118 return rtn
3119
wychen@chromium.org06928532015-02-03 02:11:29 +00003120 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003121 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003122 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003123 finally:
3124 RunGit(['checkout', '-q', branch])
3125 RunGit(['branch', '-D', TMP_BRANCH])
3126
3127 return 0
3128
3129
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003130def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003131 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003132 parser.add_option(
3133 '--no-color',
3134 action='store_true',
3135 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003136 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003137 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003138 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003139
3140 author = RunGit(['config', 'user.email']).strip() or None
3141
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003142 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003143
3144 if args:
3145 if len(args) > 1:
3146 parser.error('Unknown args')
3147 base_branch = args[0]
3148 else:
3149 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003150 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003151
3152 change = cl.GetChange(base_branch, None)
3153 return owners_finder.OwnersFinder(
3154 [f.LocalPath() for f in
3155 cl.GetChange(base_branch, None).AffectedFiles()],
3156 change.RepositoryRoot(), author,
3157 fopen=file, os_path=os.path, glob=glob.glob,
3158 disable_color=options.no_color).run()
3159
3160
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003161def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
3162 """Generates a diff command."""
3163 # Generate diff for the current branch's changes.
3164 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3165 upstream_commit, '--' ]
3166
3167 if args:
3168 for arg in args:
3169 if os.path.isdir(arg):
3170 diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
3171 elif os.path.isfile(arg):
3172 diff_cmd.append(arg)
3173 else:
3174 DieWithError('Argument "%s" is not a file or a directory' % arg)
3175 else:
3176 diff_cmd.extend('*' + ext for ext in extensions)
3177
3178 return diff_cmd
3179
3180
enne@chromium.org555cfe42014-01-29 18:21:39 +00003181@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003182def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003183 """Runs clang-format on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003184 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003185 parser.add_option('--full', action='store_true',
3186 help='Reformat the full content of all touched files')
3187 parser.add_option('--dry-run', action='store_true',
3188 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003189 parser.add_option('--diff', action='store_true',
3190 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003191 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003192
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003193 # git diff generates paths against the root of the repository. Change
3194 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003195 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003196 if rel_base_path:
3197 os.chdir(rel_base_path)
3198
digit@chromium.org29e47272013-05-17 17:01:46 +00003199 # Grab the merge-base commit, i.e. the upstream commit of the current
3200 # branch when it was created or the last time it was rebased. This is
3201 # to cover the case where the user may have called "git fetch origin",
3202 # moving the origin branch to a newer commit, but hasn't rebased yet.
3203 upstream_commit = None
3204 cl = Changelist()
3205 upstream_branch = cl.GetUpstreamBranch()
3206 if upstream_branch:
3207 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3208 upstream_commit = upstream_commit.strip()
3209
3210 if not upstream_commit:
3211 DieWithError('Could not find base commit for this branch. '
3212 'Are you in detached state?')
3213
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003214 if opts.full:
3215 # Only list the names of modified files.
3216 clang_diff_type = '--name-only'
enne@chromium.org555cfe42014-01-29 18:21:39 +00003217 else:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003218 # Only generate context-less patches.
3219 clang_diff_type = '-U0'
3220
3221 diff_cmd = BuildGitDiffCmd(clang_diff_type, upstream_commit, args, CLANG_EXTS)
digit@chromium.org29e47272013-05-17 17:01:46 +00003222 diff_output = RunGit(diff_cmd)
3223
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003224 top_dir = os.path.normpath(
3225 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3226
3227 # Locate the clang-format binary in the checkout
3228 try:
3229 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3230 except clang_format.NotFoundError, e:
3231 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003232
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003233 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3234 # formatted. This is used to block during the presubmit.
3235 return_value = 0
3236
digit@chromium.org29e47272013-05-17 17:01:46 +00003237 if opts.full:
3238 # diff_output is a list of files to send to clang-format.
3239 files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003240 if files:
3241 cmd = [clang_format_tool]
3242 if not opts.dry_run and not opts.diff:
3243 cmd.append('-i')
3244 stdout = RunCommand(cmd + files, cwd=top_dir)
3245 if opts.diff:
3246 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003247 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003248 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003249 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00003250 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003251 try:
3252 script = clang_format.FindClangFormatScriptInChromiumTree(
3253 'clang-format-diff.py')
3254 except clang_format.NotFoundError, e:
3255 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003256
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003257 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003258 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003259 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003260
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003261 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003262 if opts.diff:
3263 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003264 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003265 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003266
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003267 # Build a diff command that only operates on dart files. dart's formatter
3268 # does not have the nice property of only operating on modified chunks, so
3269 # hard code full.
3270 dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3271 args, ['.dart'])
3272 dart_diff_output = RunGit(dart_diff_cmd)
3273 if dart_diff_output:
3274 try:
3275 command = [dart_format.FindDartFmtToolInChromiumTree()]
3276 if not opts.dry_run and not opts.diff:
3277 command.append('-w')
3278 command.extend(dart_diff_output.splitlines())
3279
3280 stdout = RunCommand(command, cwd=top_dir, env=env)
3281 if opts.dry_run and stdout:
3282 return_value = 2
3283 except dart_format.NotFoundError as e:
3284 print ('Unable to check dart code formatting. Dart SDK is not in ' +
3285 'this checkout.')
3286
3287 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003288
3289
maruel@chromium.org29404b52014-09-08 22:58:00 +00003290def CMDlol(parser, args):
3291 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003292 print zlib.decompress(base64.b64decode(
3293 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3294 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3295 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3296 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003297 return 0
3298
3299
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003300class OptionParser(optparse.OptionParser):
3301 """Creates the option parse and add --verbose support."""
3302 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003303 optparse.OptionParser.__init__(
3304 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003305 self.add_option(
3306 '-v', '--verbose', action='count', default=0,
3307 help='Use 2 times for more debugging info')
3308
3309 def parse_args(self, args=None, values=None):
3310 options, args = optparse.OptionParser.parse_args(self, args, values)
3311 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3312 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3313 return options, args
3314
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003315
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003316def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003317 if sys.hexversion < 0x02060000:
3318 print >> sys.stderr, (
3319 '\nYour python version %s is unsupported, please upgrade.\n' %
3320 sys.version.split(' ', 1)[0])
3321 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003322
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003323 # Reload settings.
3324 global settings
3325 settings = Settings()
3326
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003327 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003328 dispatcher = subcommand.CommandDispatcher(__name__)
3329 try:
3330 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003331 except auth.AuthenticationError as e:
3332 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003333 except urllib2.HTTPError, e:
3334 if e.code != 500:
3335 raise
3336 DieWithError(
3337 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3338 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003339 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003340
3341
3342if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003343 # These affect sys.stdout so do it outside of main() to simplify mocks in
3344 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003345 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003346 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003347 try:
3348 sys.exit(main(sys.argv[1:]))
3349 except KeyboardInterrupt:
3350 sys.stderr.write('interrupted\n')
3351 sys.exit(1)