blob: b6810e8b6b50f44e4c00594c46152fb9df379bd8 [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)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000321
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000322
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
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001460def fetch_cl_status(branch, auth_config=None):
1461 """Fetches information for an issue and returns (branch, issue, status)."""
1462 cl = Changelist(branchref=branch, auth_config=auth_config)
1463 url = cl.GetIssueURL()
1464 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001465
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001466 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001467 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001468 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001469
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001470 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001471
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001472def get_cl_statuses(
1473 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001474 """Returns a blocking iterable of (branch, issue, color) for given branches.
1475
1476 If fine_grained is true, this will fetch CL statuses from the server.
1477 Otherwise, simply indicate if there's a matching url for the given branches.
1478
1479 If max_processes is specified, it is used as the maximum number of processes
1480 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1481 spawned.
1482 """
1483 # Silence upload.py otherwise it becomes unwieldly.
1484 upload.verbosity = 0
1485
1486 if fine_grained:
1487 # Process one branch synchronously to work through authentication, then
1488 # spawn processes to process all the other branches in parallel.
1489 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001490 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1491 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001492
1493 branches_to_fetch = branches[1:]
1494 pool = ThreadPool(
1495 min(max_processes, len(branches_to_fetch))
1496 if max_processes is not None
1497 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001498 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001499 yield x
1500 else:
1501 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1502 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001503 cl = Changelist(branchref=b, auth_config=auth_config)
1504 url = cl.GetIssueURL()
1505 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001506
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001507def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001508 """Show status of changelists.
1509
1510 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001511 - Red not sent for review or broken
1512 - Blue waiting for review
1513 - Yellow waiting for you to reply to review
1514 - Green LGTM'ed
1515 - Magenta in the commit queue
1516 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001517
1518 Also see 'git cl comments'.
1519 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001520 parser.add_option('--field',
1521 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001522 parser.add_option('-f', '--fast', action='store_true',
1523 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001524 parser.add_option(
1525 '-j', '--maxjobs', action='store', type=int,
1526 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001527
1528 auth.add_auth_options(parser)
1529 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001530 if args:
1531 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001532 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001533
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001534 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001535 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001536 if options.field.startswith('desc'):
1537 print cl.GetDescription()
1538 elif options.field == 'id':
1539 issueid = cl.GetIssue()
1540 if issueid:
1541 print issueid
1542 elif options.field == 'patch':
1543 patchset = cl.GetPatchset()
1544 if patchset:
1545 print patchset
1546 elif options.field == 'url':
1547 url = cl.GetIssueURL()
1548 if url:
1549 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001550 return 0
1551
1552 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1553 if not branches:
1554 print('No local branch found.')
1555 return 0
1556
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001557 changes = (
1558 Changelist(branchref=b, auth_config=auth_config)
1559 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001560 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001561 alignment = max(5, max(len(b) for b in branches))
1562 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001563 output = get_cl_statuses(branches,
1564 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001565 max_processes=options.maxjobs,
1566 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001567
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001568 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001569 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001570 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001571 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001572 b, i, status = output.next()
1573 branch_statuses[b] = (i, status)
1574 issue_url, status = branch_statuses.pop(branch)
1575 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001576 reset = Fore.RESET
1577 if not sys.stdout.isatty():
1578 color = ''
1579 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001580 status_str = '(%s)' % status if status else ''
1581 print ' %*s : %s%s %s%s' % (
1582 alignment, ShortBranchName(branch), color, issue_url, status_str,
1583 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001584
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001585 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001586 print
1587 print 'Current branch:',
1588 if not cl.GetIssue():
1589 print 'no issue assigned.'
1590 return 0
1591 print cl.GetBranch()
1592 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001593 if not options.fast:
1594 print 'Issue description:'
1595 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001596 return 0
1597
1598
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001599def colorize_CMDstatus_doc():
1600 """To be called once in main() to add colors to git cl status help."""
1601 colors = [i for i in dir(Fore) if i[0].isupper()]
1602
1603 def colorize_line(line):
1604 for color in colors:
1605 if color in line.upper():
1606 # Extract whitespaces first and the leading '-'.
1607 indent = len(line) - len(line.lstrip(' ')) + 1
1608 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1609 return line
1610
1611 lines = CMDstatus.__doc__.splitlines()
1612 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1613
1614
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001615@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001616def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001617 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001618
1619 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001620 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001621 parser.add_option('-r', '--reverse', action='store_true',
1622 help='Lookup the branch(es) for the specified issues. If '
1623 'no issues are specified, all branches with mapped '
1624 'issues will be listed.')
1625 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001626
dnj@chromium.org406c4402015-03-03 17:22:28 +00001627 if options.reverse:
1628 branches = RunGit(['for-each-ref', 'refs/heads',
1629 '--format=%(refname:short)']).splitlines()
1630
1631 # Reverse issue lookup.
1632 issue_branch_map = {}
1633 for branch in branches:
1634 cl = Changelist(branchref=branch)
1635 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1636 if not args:
1637 args = sorted(issue_branch_map.iterkeys())
1638 for issue in args:
1639 if not issue:
1640 continue
1641 print 'Branch for issue number %s: %s' % (
1642 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1643 else:
1644 cl = Changelist()
1645 if len(args) > 0:
1646 try:
1647 issue = int(args[0])
1648 except ValueError:
1649 DieWithError('Pass a number to set the issue or none to list it.\n'
1650 'Maybe you want to run git cl status?')
1651 cl.SetIssue(issue)
1652 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001653 return 0
1654
1655
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001656def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001657 """Shows or posts review comments for any changelist."""
1658 parser.add_option('-a', '--add-comment', dest='comment',
1659 help='comment to add to an issue')
1660 parser.add_option('-i', dest='issue',
1661 help="review issue id (defaults to current issue)")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001662 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001663 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001664 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001665
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001666 issue = None
1667 if options.issue:
1668 try:
1669 issue = int(options.issue)
1670 except ValueError:
1671 DieWithError('A review issue id is expected to be a number')
1672
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001673 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001674
1675 if options.comment:
1676 cl.AddComment(options.comment)
1677 return 0
1678
1679 data = cl.GetIssueProperties()
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001680 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001681 if message['disapproval']:
1682 color = Fore.RED
1683 elif message['approval']:
1684 color = Fore.GREEN
1685 elif message['sender'] == data['owner_email']:
1686 color = Fore.MAGENTA
1687 else:
1688 color = Fore.BLUE
1689 print '\n%s%s %s%s' % (
1690 color, message['date'].split('.', 1)[0], message['sender'],
1691 Fore.RESET)
1692 if message['text'].strip():
1693 print '\n'.join(' ' + l for l in message['text'].splitlines())
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001694 return 0
1695
1696
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001697def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001698 """Brings up the editor for the current CL's description."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001699 auth.add_auth_options(parser)
1700 options, _ = parser.parse_args(args)
1701 auth_config = auth.extract_auth_config_from_options(options)
1702 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001703 if not cl.GetIssue():
1704 DieWithError('This branch has no associated changelist.')
1705 description = ChangeDescription(cl.GetDescription())
1706 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00001707 if cl.GetDescription() != description.description:
1708 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001709 return 0
1710
1711
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001712def CreateDescriptionFromLog(args):
1713 """Pulls out the commit log to use as a base for the CL description."""
1714 log_args = []
1715 if len(args) == 1 and not args[0].endswith('.'):
1716 log_args = [args[0] + '..']
1717 elif len(args) == 1 and args[0].endswith('...'):
1718 log_args = [args[0][:-1]]
1719 elif len(args) == 2:
1720 log_args = [args[0] + '..' + args[1]]
1721 else:
1722 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001723 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001724
1725
thestig@chromium.org44202a22014-03-11 19:22:18 +00001726def CMDlint(parser, args):
1727 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001728 parser.add_option('--filter', action='append', metavar='-x,+y',
1729 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001730 auth.add_auth_options(parser)
1731 options, args = parser.parse_args(args)
1732 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001733
1734 # Access to a protected member _XX of a client class
1735 # pylint: disable=W0212
1736 try:
1737 import cpplint
1738 import cpplint_chromium
1739 except ImportError:
1740 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1741 return 1
1742
1743 # Change the current working directory before calling lint so that it
1744 # shows the correct base.
1745 previous_cwd = os.getcwd()
1746 os.chdir(settings.GetRoot())
1747 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001748 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001749 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1750 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001751 if not files:
1752 print "Cannot lint an empty CL"
1753 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001754
1755 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001756 command = args + files
1757 if options.filter:
1758 command = ['--filter=' + ','.join(options.filter)] + command
1759 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001760
1761 white_regex = re.compile(settings.GetLintRegex())
1762 black_regex = re.compile(settings.GetLintIgnoreRegex())
1763 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1764 for filename in filenames:
1765 if white_regex.match(filename):
1766 if black_regex.match(filename):
1767 print "Ignoring file %s" % filename
1768 else:
1769 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1770 extra_check_functions)
1771 else:
1772 print "Skipping file %s" % filename
1773 finally:
1774 os.chdir(previous_cwd)
1775 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1776 if cpplint._cpplint_state.error_count != 0:
1777 return 1
1778 return 0
1779
1780
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001781def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001782 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001783 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001784 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001785 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001786 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001787 auth.add_auth_options(parser)
1788 options, args = parser.parse_args(args)
1789 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001790
sbc@chromium.org71437c02015-04-09 19:29:40 +00001791 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00001792 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001793 return 1
1794
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001795 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001796 if args:
1797 base_branch = args[0]
1798 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001799 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001800 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001801
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001802 cl.RunHook(
1803 committing=not options.upload,
1804 may_prompt=False,
1805 verbose=options.verbose,
1806 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001807 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001808
1809
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001810def AddChangeIdToCommitMessage(options, args):
1811 """Re-commits using the current message, assumes the commit hook is in
1812 place.
1813 """
1814 log_desc = options.message or CreateDescriptionFromLog(args)
1815 git_command = ['commit', '--amend', '-m', log_desc]
1816 RunGit(git_command)
1817 new_log_desc = CreateDescriptionFromLog(args)
1818 if CHANGE_ID in new_log_desc:
1819 print 'git-cl: Added Change-Id to commit message.'
1820 else:
1821 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1822
1823
piman@chromium.org336f9122014-09-04 02:16:55 +00001824def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001825 """upload the current branch to gerrit."""
1826 # We assume the remote called "origin" is the one we want.
1827 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001828 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00001829
1830 remote, remote_branch = cl.GetRemoteBranch()
1831 branch = GetTargetRef(remote, remote_branch, options.target_branch,
1832 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001833
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001834 change_desc = ChangeDescription(
1835 options.message or CreateDescriptionFromLog(args))
1836 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001837 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001838 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001839
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001840 if options.squash:
1841 # Try to get the message from a previous upload.
1842 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
1843 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
1844 if not message:
1845 if not options.force:
1846 change_desc.prompt()
1847
1848 if CHANGE_ID not in change_desc.description:
1849 # Run the commit-msg hook without modifying the head commit by writing
1850 # the commit message to a temporary file and running the hook over it,
1851 # then reading the file back in.
1852 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
1853 'commit-msg')
1854 file_handle, msg_file = tempfile.mkstemp(text=True,
1855 prefix='commit_msg')
1856 try:
1857 try:
1858 with os.fdopen(file_handle, 'w') as fileobj:
1859 fileobj.write(change_desc.description)
1860 finally:
1861 os.close(file_handle)
1862 RunCommand([commit_msg_hook, msg_file])
1863 change_desc.set_description(gclient_utils.FileRead(msg_file))
1864 finally:
1865 os.remove(msg_file)
1866
1867 if not change_desc.description:
1868 print "Description is empty; aborting."
1869 return 1
1870
1871 message = change_desc.description
1872
1873 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1874 if remote is '.':
1875 # If our upstream branch is local, we base our squashed commit on its
1876 # squashed version.
1877 parent = ('refs/heads/git_cl_uploads/' +
1878 scm.GIT.ShortBranchName(upstream_branch))
1879
1880 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
1881 # will create additional CLs when uploading.
1882 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
1883 RunGitSilent(['rev-parse', parent + ':'])):
1884 print 'Upload upstream branch ' + upstream_branch + ' first.'
1885 return 1
1886 else:
1887 parent = cl.GetCommonAncestorWithUpstream()
1888
1889 tree = RunGit(['rev-parse', 'HEAD:']).strip()
1890 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
1891 '-m', message]).strip()
1892 else:
1893 if CHANGE_ID not in change_desc.description:
1894 AddChangeIdToCommitMessage(options, args)
1895 ref_to_push = 'HEAD'
1896 parent = '%s/%s' % (gerrit_remote, branch)
1897
1898 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
1899 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001900 if len(commits) > 1:
1901 print('WARNING: This will upload %d commits. Run the following command '
1902 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001903 print('git log %s..%s' % (parent, ref_to_push))
1904 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001905 'commit.')
1906 ask_for_data('About to upload; enter to confirm.')
1907
piman@chromium.org336f9122014-09-04 02:16:55 +00001908 if options.reviewers or options.tbr_owners:
1909 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001910
ukai@chromium.orge8077812012-02-03 03:41:46 +00001911 receive_options = []
1912 cc = cl.GetCCList().split(',')
1913 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001914 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001915 cc = filter(None, cc)
1916 if cc:
1917 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001918 if change_desc.get_reviewers():
1919 receive_options.extend(
1920 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001921
ukai@chromium.orge8077812012-02-03 03:41:46 +00001922 git_command = ['push']
1923 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001924 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001925 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001926 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00001927 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001928
1929 if options.squash:
1930 head = RunGit(['rev-parse', 'HEAD']).strip()
1931 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
1932
ukai@chromium.orge8077812012-02-03 03:41:46 +00001933 # TODO(ukai): parse Change-Id: and set issue number?
1934 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001935
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001936
wittman@chromium.org455dc922015-01-26 20:15:50 +00001937def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
1938 """Computes the remote branch ref to use for the CL.
1939
1940 Args:
1941 remote (str): The git remote for the CL.
1942 remote_branch (str): The git remote branch for the CL.
1943 target_branch (str): The target branch specified by the user.
1944 pending_prefix (str): The pending prefix from the settings.
1945 """
1946 if not (remote and remote_branch):
1947 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001948
wittman@chromium.org455dc922015-01-26 20:15:50 +00001949 if target_branch:
1950 # Cannonicalize branch references to the equivalent local full symbolic
1951 # refs, which are then translated into the remote full symbolic refs
1952 # below.
1953 if '/' not in target_branch:
1954 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
1955 else:
1956 prefix_replacements = (
1957 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
1958 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
1959 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
1960 )
1961 match = None
1962 for regex, replacement in prefix_replacements:
1963 match = re.search(regex, target_branch)
1964 if match:
1965 remote_branch = target_branch.replace(match.group(0), replacement)
1966 break
1967 if not match:
1968 # This is a branch path but not one we recognize; use as-is.
1969 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00001970 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
1971 # Handle the refs that need to land in different refs.
1972 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001973
wittman@chromium.org455dc922015-01-26 20:15:50 +00001974 # Create the true path to the remote branch.
1975 # Does the following translation:
1976 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
1977 # * refs/remotes/origin/master -> refs/heads/master
1978 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
1979 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
1980 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
1981 elif remote_branch.startswith('refs/remotes/%s/' % remote):
1982 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
1983 'refs/heads/')
1984 elif remote_branch.startswith('refs/remotes/branch-heads'):
1985 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
1986 # If a pending prefix exists then replace refs/ with it.
1987 if pending_prefix:
1988 remote_branch = remote_branch.replace('refs/', pending_prefix)
1989 return remote_branch
1990
1991
piman@chromium.org336f9122014-09-04 02:16:55 +00001992def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001993 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001994 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1995 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001996 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001997 if options.emulate_svn_auto_props:
1998 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001999
2000 change_desc = None
2001
pgervais@chromium.org91141372014-01-09 23:27:20 +00002002 if options.email is not None:
2003 upload_args.extend(['--email', options.email])
2004
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002005 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002006 if options.title:
2007 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002008 if options.message:
2009 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002010 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002011 print ("This branch is associated with issue %s. "
2012 "Adding patch to that issue." % cl.GetIssue())
2013 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002014 if options.title:
2015 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002016 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002017 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002018 if options.reviewers or options.tbr_owners:
2019 change_desc.update_reviewers(options.reviewers,
2020 options.tbr_owners,
2021 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002022 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002023 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002024
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002025 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002026 print "Description is empty; aborting."
2027 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002028
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002029 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002030 if change_desc.get_reviewers():
2031 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002032 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002033 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002034 DieWithError("Must specify reviewers to send email.")
2035 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002036
2037 # We check this before applying rietveld.private assuming that in
2038 # rietveld.cc only addresses which we can send private CLs to are listed
2039 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2040 # --private is specified explicitly on the command line.
2041 if options.private:
2042 logging.warn('rietveld.cc is ignored since private flag is specified. '
2043 'You need to review and add them manually if necessary.')
2044 cc = cl.GetCCListWithoutDefault()
2045 else:
2046 cc = cl.GetCCList()
2047 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002048 if cc:
2049 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002050
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002051 if options.private or settings.GetDefaultPrivateFlag() == "True":
2052 upload_args.append('--private')
2053
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002054 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002055 if not options.find_copies:
2056 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002057
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002058 # Include the upstream repo's URL in the change -- this is useful for
2059 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002060 remote_url = cl.GetGitBaseUrlFromConfig()
2061 if not remote_url:
2062 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002063 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002064 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002065 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2066 remote_url = (cl.GetRemoteUrl() + '@'
2067 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002068 if remote_url:
2069 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002070 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002071 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2072 settings.GetPendingRefPrefix())
2073 if target_ref:
2074 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002075
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002076 project = settings.GetProject()
2077 if project:
2078 upload_args.extend(['--project', project])
2079
rmistry@google.comef966222015-04-07 11:15:01 +00002080 if options.cq_dry_run:
2081 upload_args.extend(['--cq_dry_run'])
2082
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002083 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002084 upload_args = ['upload'] + upload_args + args
2085 logging.info('upload.RealMain(%s)', upload_args)
2086 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002087 issue = int(issue)
2088 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002089 except KeyboardInterrupt:
2090 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002091 except:
2092 # If we got an exception after the user typed a description for their
2093 # change, back up the description before re-raising.
2094 if change_desc:
2095 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2096 print '\nGot exception while uploading -- saving description to %s\n' \
2097 % backup_path
2098 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002099 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002100 backup_file.close()
2101 raise
2102
2103 if not cl.GetIssue():
2104 cl.SetIssue(issue)
2105 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002106
2107 if options.use_commit_queue:
2108 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002109 return 0
2110
2111
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002112def cleanup_list(l):
2113 """Fixes a list so that comma separated items are put as individual items.
2114
2115 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2116 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2117 """
2118 items = sum((i.split(',') for i in l), [])
2119 stripped_items = (i.strip() for i in items)
2120 return sorted(filter(None, stripped_items))
2121
2122
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002123@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002124def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002125 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00002126 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2127 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002128 parser.add_option('--bypass-watchlists', action='store_true',
2129 dest='bypass_watchlists',
2130 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002131 parser.add_option('-f', action='store_true', dest='force',
2132 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002133 parser.add_option('-m', dest='message', help='message for patchset')
2134 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002135 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002136 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002137 help='reviewer email addresses')
2138 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002139 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002140 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002141 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002142 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002143 parser.add_option('--emulate_svn_auto_props',
2144 '--emulate-svn-auto-props',
2145 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002146 dest="emulate_svn_auto_props",
2147 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002148 parser.add_option('-c', '--use-commit-queue', action='store_true',
2149 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002150 parser.add_option('--private', action='store_true',
2151 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002152 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002153 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002154 metavar='TARGET',
2155 help='Apply CL to remote ref TARGET. ' +
2156 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002157 parser.add_option('--squash', action='store_true',
2158 help='Squash multiple commits into one (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002159 parser.add_option('--email', default=None,
2160 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002161 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2162 help='add a set of OWNERS to TBR')
rmistry@google.comef966222015-04-07 11:15:01 +00002163 parser.add_option('--cq-dry-run', dest='cq_dry_run', action='store_true',
2164 help='Send the patchset to do a CQ dry run right after '
2165 'upload.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002166
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002167 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002168 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002169 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002170 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002171
sbc@chromium.org71437c02015-04-09 19:29:40 +00002172 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002173 return 1
2174
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002175 options.reviewers = cleanup_list(options.reviewers)
2176 options.cc = cleanup_list(options.cc)
2177
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002178 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002179 if args:
2180 # TODO(ukai): is it ok for gerrit case?
2181 base_branch = args[0]
2182 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002183 if cl.GetBranch() is None:
2184 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2185
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002186 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002187 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002188 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002189
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002190 # Make sure authenticated to Rietveld before running expensive hooks. It is
2191 # a fast, best efforts check. Rietveld still can reject the authentication
2192 # during the actual upload.
2193 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2194 authenticator = auth.get_authenticator_for_host(
2195 cl.GetRietveldServer(), auth_config)
2196 if not authenticator.has_cached_credentials():
2197 raise auth.LoginRequiredError(cl.GetRietveldServer())
2198
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002199 # Apply watchlists on upload.
2200 change = cl.GetChange(base_branch, None)
2201 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2202 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002203 if not options.bypass_watchlists:
2204 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002205
ukai@chromium.orge8077812012-02-03 03:41:46 +00002206 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002207 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002208 # Set the reviewer list now so that presubmit checks can access it.
2209 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002210 change_description.update_reviewers(options.reviewers,
2211 options.tbr_owners,
2212 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002213 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002214 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002215 may_prompt=not options.force,
2216 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002217 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002218 if not hook_results.should_continue():
2219 return 1
2220 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002221 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002222
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002223 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002224 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002225 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002226 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002227 print ('The last upload made from this repository was patchset #%d but '
2228 'the most recent patchset on the server is #%d.'
2229 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002230 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2231 'from another machine or branch the patch you\'re uploading now '
2232 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002233 ask_for_data('About to upload; enter to confirm.')
2234
iannucci@chromium.org79540052012-10-19 23:15:26 +00002235 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002236 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00002237 return GerritUpload(options, args, cl, change)
2238 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002239 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002240 git_set_branch_value('last-upload-hash',
2241 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002242 # Run post upload hooks, if specified.
2243 if settings.GetRunPostUploadHook():
2244 presubmit_support.DoPostUploadExecuter(
2245 change,
2246 cl,
2247 settings.GetRoot(),
2248 options.verbose,
2249 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002250
2251 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002252
2253
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002254def IsSubmoduleMergeCommit(ref):
2255 # When submodules are added to the repo, we expect there to be a single
2256 # non-git-svn merge commit at remote HEAD with a signature comment.
2257 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002258 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002259 return RunGit(cmd) != ''
2260
2261
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002262def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002263 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002264
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002265 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002266 Updates changelog with metadata (e.g. pointer to review).
2267 Pushes/dcommits the code upstream.
2268 Updates review and closes.
2269 """
2270 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2271 help='bypass upload presubmit hook')
2272 parser.add_option('-m', dest='message',
2273 help="override review description")
2274 parser.add_option('-f', action='store_true', dest='force',
2275 help="force yes to questions (don't prompt)")
2276 parser.add_option('-c', dest='contributor',
2277 help="external contributor for patch (appended to " +
2278 "description and used as author for git). Should be " +
2279 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002280 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002281 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002282 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002283 auth_config = auth.extract_auth_config_from_options(options)
2284
2285 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002286
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002287 current = cl.GetBranch()
2288 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2289 if not settings.GetIsGitSvn() and remote == '.':
2290 print
2291 print 'Attempting to push branch %r into another local branch!' % current
2292 print
2293 print 'Either reparent this branch on top of origin/master:'
2294 print ' git reparent-branch --root'
2295 print
2296 print 'OR run `git rebase-update` if you think the parent branch is already'
2297 print 'committed.'
2298 print
2299 print ' Current parent: %r' % upstream_branch
2300 return 1
2301
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002302 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002303 # Default to merging against our best guess of the upstream branch.
2304 args = [cl.GetUpstreamBranch()]
2305
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002306 if options.contributor:
2307 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2308 print "Please provide contibutor as 'First Last <email@example.com>'"
2309 return 1
2310
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002311 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002312 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002313
sbc@chromium.org71437c02015-04-09 19:29:40 +00002314 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002315 return 1
2316
2317 # This rev-list syntax means "show all commits not in my branch that
2318 # are in base_branch".
2319 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2320 base_branch]).splitlines()
2321 if upstream_commits:
2322 print ('Base branch "%s" has %d commits '
2323 'not in this branch.' % (base_branch, len(upstream_commits)))
2324 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2325 return 1
2326
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002327 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002328 svn_head = None
2329 if cmd == 'dcommit' or base_has_submodules:
2330 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2331 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002332
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002333 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002334 # If the base_head is a submodule merge commit, the first parent of the
2335 # base_head should be a git-svn commit, which is what we're interested in.
2336 base_svn_head = base_branch
2337 if base_has_submodules:
2338 base_svn_head += '^1'
2339
2340 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002341 if extra_commits:
2342 print ('This branch has %d additional commits not upstreamed yet.'
2343 % len(extra_commits.splitlines()))
2344 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2345 'before attempting to %s.' % (base_branch, cmd))
2346 return 1
2347
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002348 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002349 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002350 author = None
2351 if options.contributor:
2352 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002353 hook_results = cl.RunHook(
2354 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002355 may_prompt=not options.force,
2356 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002357 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002358 if not hook_results.should_continue():
2359 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002360
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002361 # Check the tree status if the tree status URL is set.
2362 status = GetTreeStatus()
2363 if 'closed' == status:
2364 print('The tree is closed. Please wait for it to reopen. Use '
2365 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2366 return 1
2367 elif 'unknown' == status:
2368 print('Unable to determine tree status. Please verify manually and '
2369 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2370 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002371 else:
2372 breakpad.SendStack(
2373 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002374 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2375 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002376 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002377
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002378 change_desc = ChangeDescription(options.message)
2379 if not change_desc.description and cl.GetIssue():
2380 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002381
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002382 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002383 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002384 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002385 else:
2386 print 'No description set.'
2387 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2388 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002389
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002390 # Keep a separate copy for the commit message, because the commit message
2391 # contains the link to the Rietveld issue, while the Rietveld message contains
2392 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002393 # Keep a separate copy for the commit message.
2394 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002395 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002396
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002397 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002398 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002399 # Xcode won't linkify this URL unless there is a non-whitespace character
2400 # after it. Add a period on a new line to circumvent this.
2401 commit_desc.append_footer('Review URL: %s.' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002402 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002403 commit_desc.append_footer('Patch from %s.' % options.contributor)
2404
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002405 print('Description:')
2406 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002407
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002408 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002409 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002410 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002411
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002412 # We want to squash all this branch's commits into one commit with the proper
2413 # description. We do this by doing a "reset --soft" to the base branch (which
2414 # keeps the working copy the same), then dcommitting that. If origin/master
2415 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2416 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002417 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002418 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2419 # Delete the branches if they exist.
2420 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2421 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2422 result = RunGitWithCode(showref_cmd)
2423 if result[0] == 0:
2424 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002425
2426 # We might be in a directory that's present in this branch but not in the
2427 # trunk. Move up to the top of the tree so that git commands that expect a
2428 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002429 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002430 if rel_base_path:
2431 os.chdir(rel_base_path)
2432
2433 # Stuff our change into the merge branch.
2434 # We wrap in a try...finally block so if anything goes wrong,
2435 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002436 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002437 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002438 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002439 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002440 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002441 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002442 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002443 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002444 RunGit(
2445 [
2446 'commit', '--author', options.contributor,
2447 '-m', commit_desc.description,
2448 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002449 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002450 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002451 if base_has_submodules:
2452 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2453 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2454 RunGit(['checkout', CHERRY_PICK_BRANCH])
2455 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002456 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002457 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002458 pending_prefix = settings.GetPendingRefPrefix()
2459 if not pending_prefix or branch.startswith(pending_prefix):
2460 # If not using refs/pending/heads/* at all, or target ref is already set
2461 # to pending, then push to the target ref directly.
2462 retcode, output = RunGitWithCode(
2463 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002464 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002465 else:
2466 # Cherry-pick the change on top of pending ref and then push it.
2467 assert branch.startswith('refs/'), branch
2468 assert pending_prefix[-1] == '/', pending_prefix
2469 pending_ref = pending_prefix + branch[len('refs/'):]
2470 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002471 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002472 if retcode == 0:
2473 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002474 else:
2475 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002476 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002477 'svn', 'dcommit',
2478 '-C%s' % options.similarity,
2479 '--no-rebase', '--rmdir',
2480 ]
2481 if settings.GetForceHttpsCommitUrl():
2482 # Allow forcing https commit URLs for some projects that don't allow
2483 # committing to http URLs (like Google Code).
2484 remote_url = cl.GetGitSvnRemoteUrl()
2485 if urlparse.urlparse(remote_url).scheme == 'http':
2486 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002487 cmd_args.append('--commit-url=%s' % remote_url)
2488 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002489 if 'Committed r' in output:
2490 revision = re.match(
2491 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2492 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002493 finally:
2494 # And then swap back to the original branch and clean up.
2495 RunGit(['checkout', '-q', cl.GetBranch()])
2496 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002497 if base_has_submodules:
2498 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002499
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002500 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002501 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002502 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002503
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002504 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002505 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002506 try:
2507 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2508 # We set pushed_to_pending to False, since it made it all the way to the
2509 # real ref.
2510 pushed_to_pending = False
2511 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002512 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002513
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002514 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002515 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002516 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002517 if not to_pending:
2518 if viewvc_url and revision:
2519 change_desc.append_footer(
2520 'Committed: %s%s' % (viewvc_url, revision))
2521 elif revision:
2522 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002523 print ('Closing issue '
2524 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002525 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002526 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002527 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002528 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002529 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002530 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002531 if options.bypass_hooks:
2532 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2533 else:
2534 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002535 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002536 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002537
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002538 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002539 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2540 print 'The commit is in the pending queue (%s).' % pending_ref
2541 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002542 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002543 'footer.' % branch)
2544
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002545 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2546 if os.path.isfile(hook):
2547 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002548
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002549 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002550
2551
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002552def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2553 print
2554 print 'Waiting for commit to be landed on %s...' % real_ref
2555 print '(If you are impatient, you may Ctrl-C once without harm)'
2556 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2557 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2558
2559 loop = 0
2560 while True:
2561 sys.stdout.write('fetching (%d)... \r' % loop)
2562 sys.stdout.flush()
2563 loop += 1
2564
2565 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2566 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2567 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2568 for commit in commits.splitlines():
2569 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2570 print 'Found commit on %s' % real_ref
2571 return commit
2572
2573 current_rev = to_rev
2574
2575
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002576def PushToGitPending(remote, pending_ref, upstream_ref):
2577 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2578
2579 Returns:
2580 (retcode of last operation, output log of last operation).
2581 """
2582 assert pending_ref.startswith('refs/'), pending_ref
2583 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2584 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2585 code = 0
2586 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002587 max_attempts = 3
2588 attempts_left = max_attempts
2589 while attempts_left:
2590 if attempts_left != max_attempts:
2591 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2592 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002593
2594 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002595 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002596 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002597 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002598 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002599 print 'Fetch failed with exit code %d.' % code
2600 if out.strip():
2601 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002602 continue
2603
2604 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002605 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002606 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002607 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002608 if code:
2609 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002610 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2611 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002612 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2613 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002614 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002615 return code, out
2616
2617 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002618 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002619 code, out = RunGitWithCode(
2620 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2621 if code == 0:
2622 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002623 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002624 return code, out
2625
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002626 print 'Push failed with exit code %d.' % code
2627 if out.strip():
2628 print out.strip()
2629 if IsFatalPushFailure(out):
2630 print (
2631 'Fatal push error. Make sure your .netrc credentials and git '
2632 'user.email are correct and you have push access to the repo.')
2633 return code, out
2634
2635 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002636 return code, out
2637
2638
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002639def IsFatalPushFailure(push_stdout):
2640 """True if retrying push won't help."""
2641 return '(prohibited by Gerrit)' in push_stdout
2642
2643
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002644@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002645def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002646 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002647 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002648 message = """This doesn't appear to be an SVN repository.
2649If your project has a git mirror with an upstream SVN master, you probably need
2650to run 'git svn init', see your project's git mirror documentation.
2651If your project has a true writeable upstream repository, you probably want
mark@chromium.org671c7a32014-07-31 17:09:36 +00002652to run 'git cl land' instead.
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002653Choose wisely, if you get this wrong, your commit might appear to succeed but
2654will instead be silently ignored."""
2655 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002656 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002657 return SendUpstream(parser, args, 'dcommit')
2658
2659
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002660@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002661def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002662 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002663 if settings.GetIsGitSvn():
2664 print('This appears to be an SVN repository.')
2665 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002666 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002667 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002668
2669
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002670@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002671def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002672 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002673 parser.add_option('-b', dest='newbranch',
2674 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002675 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002676 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002677 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2678 help='Change to the directory DIR immediately, '
2679 'before doing anything else.')
2680 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002681 help='failed patches spew .rej files rather than '
2682 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002683 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2684 help="don't commit after patch applies")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002685 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002686 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002687 auth_config = auth.extract_auth_config_from_options(options)
2688
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002689 if len(args) != 1:
2690 parser.print_help()
2691 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002692 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002693
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002694 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002695 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002696 return 1
2697
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002698 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002699 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002700
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002701 if options.newbranch:
2702 if options.force:
2703 RunGit(['branch', '-D', options.newbranch],
2704 stderr=subprocess2.PIPE, error_ok=True)
2705 RunGit(['checkout', '-b', options.newbranch,
2706 Changelist().GetUpstreamBranch()])
2707
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002708 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002709 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002710
2711
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002712def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00002713 # PatchIssue should never be called with a dirty tree. It is up to the
2714 # caller to check this, but just in case we assert here since the
2715 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002716 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002717
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002718 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002719 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002720 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002721 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002722 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002723 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002724 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002725 # Assume it's a URL to the patch. Default to https.
2726 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00002727 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002728 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002729 DieWithError('Must pass an issue ID or full URL for '
2730 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00002731 issue = int(match.group(2))
2732 cl = Changelist(issue=issue, auth_config=auth_config)
2733 cl.rietveld_server = match.group(1)
2734 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002735 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002736
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002737 # Switch up to the top-level directory, if necessary, in preparation for
2738 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002739 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002740 if top:
2741 os.chdir(top)
2742
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002743 # Git patches have a/ at the beginning of source paths. We strip that out
2744 # with a sed script rather than the -p flag to patch so we can feed either
2745 # Git or svn-style patches into the same apply command.
2746 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002747 try:
2748 patch_data = subprocess2.check_output(
2749 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2750 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002751 DieWithError('Git patch mungling failed.')
2752 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002753
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002754 # We use "git apply" to apply the patch instead of "patch" so that we can
2755 # pick up file adds.
2756 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002757 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002758 if directory:
2759 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002760 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002761 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002762 elif IsGitVersionAtLeast('1.7.12'):
2763 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002764 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002765 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002766 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002767 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00002768 print 'Failed to apply the patch'
2769 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002770
2771 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002772 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00002773 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
2774 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00002775 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2776 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002777 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002778 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002779 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002780 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002781 else:
2782 print "Patch applied to index."
2783 return 0
2784
2785
2786def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002787 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002788 # Provide a wrapper for git svn rebase to help avoid accidental
2789 # git svn dcommit.
2790 # It's the only command that doesn't use parser at all since we just defer
2791 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002792
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002793 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002794
2795
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002796def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002797 """Fetches the tree status and returns either 'open', 'closed',
2798 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002799 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002800 if url:
2801 status = urllib2.urlopen(url).read().lower()
2802 if status.find('closed') != -1 or status == '0':
2803 return 'closed'
2804 elif status.find('open') != -1 or status == '1':
2805 return 'open'
2806 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002807 return 'unset'
2808
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002809
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002810def GetTreeStatusReason():
2811 """Fetches the tree status from a json url and returns the message
2812 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002813 url = settings.GetTreeStatusUrl()
2814 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002815 connection = urllib2.urlopen(json_url)
2816 status = json.loads(connection.read())
2817 connection.close()
2818 return status['message']
2819
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002820
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002821def GetBuilderMaster(bot_list):
2822 """For a given builder, fetch the master from AE if available."""
2823 map_url = 'https://builders-map.appspot.com/'
2824 try:
2825 master_map = json.load(urllib2.urlopen(map_url))
2826 except urllib2.URLError as e:
2827 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2828 (map_url, e))
2829 except ValueError as e:
2830 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2831 if not master_map:
2832 return None, 'Failed to build master map.'
2833
2834 result_master = ''
2835 for bot in bot_list:
2836 builder = bot.split(':', 1)[0]
2837 master_list = master_map.get(builder, [])
2838 if not master_list:
2839 return None, ('No matching master for builder %s.' % builder)
2840 elif len(master_list) > 1:
2841 return None, ('The builder name %s exists in multiple masters %s.' %
2842 (builder, master_list))
2843 else:
2844 cur_master = master_list[0]
2845 if not result_master:
2846 result_master = cur_master
2847 elif result_master != cur_master:
2848 return None, 'The builders do not belong to the same master.'
2849 return result_master, None
2850
2851
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002852def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002853 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002854 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002855 status = GetTreeStatus()
2856 if 'unset' == status:
2857 print 'You must configure your tree status URL by running "git cl config".'
2858 return 2
2859
2860 print "The tree is %s" % status
2861 print
2862 print GetTreeStatusReason()
2863 if status != 'open':
2864 return 1
2865 return 0
2866
2867
maruel@chromium.org15192402012-09-06 12:38:29 +00002868def CMDtry(parser, args):
2869 """Triggers a try job through Rietveld."""
2870 group = optparse.OptionGroup(parser, "Try job options")
2871 group.add_option(
2872 "-b", "--bot", action="append",
2873 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2874 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002875 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002876 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002877 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00002878 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002879 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002880 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002881 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002882 "-r", "--revision",
2883 help="Revision to use for the try job; default: the "
2884 "revision will be determined by the try server; see "
2885 "its waterfall for more info")
2886 group.add_option(
2887 "-c", "--clobber", action="store_true", default=False,
2888 help="Force a clobber before building; e.g. don't do an "
2889 "incremental build")
2890 group.add_option(
2891 "--project",
2892 help="Override which project to use. Projects are defined "
2893 "server-side to define what default bot set to use")
2894 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002895 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00002896 group.add_option(
2897 "--use-buildbucket", action="store_true", default=False,
2898 help="Use buildbucket to trigger try jobs.")
maruel@chromium.org15192402012-09-06 12:38:29 +00002899 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002900 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00002901 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002902 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00002903
2904 if args:
2905 parser.error('Unknown arguments: %s' % args)
2906
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002907 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00002908 if not cl.GetIssue():
2909 parser.error('Need to upload first')
2910
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002911 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002912 if props.get('closed'):
2913 parser.error('Cannot send tryjobs for a closed CL')
2914
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002915 if props.get('private'):
2916 parser.error('Cannot use trybots with private issue')
2917
maruel@chromium.org15192402012-09-06 12:38:29 +00002918 if not options.name:
2919 options.name = cl.GetBranch()
2920
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002921 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002922 options.master, err_msg = GetBuilderMaster(options.bot)
2923 if err_msg:
2924 parser.error('Tryserver master cannot be found because: %s\n'
2925 'Please manually specify the tryserver master'
2926 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002927
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002928 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002929 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002930 if not options.bot:
2931 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002932
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002933 # Get try masters from PRESUBMIT.py files.
2934 masters = presubmit_support.DoGetTryMasters(
2935 change,
2936 change.LocalPaths(),
2937 settings.GetRoot(),
2938 None,
2939 None,
2940 options.verbose,
2941 sys.stdout)
2942 if masters:
2943 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002944
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002945 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2946 options.bot = presubmit_support.DoGetTrySlaves(
2947 change,
2948 change.LocalPaths(),
2949 settings.GetRoot(),
2950 None,
2951 None,
2952 options.verbose,
2953 sys.stdout)
2954 if not options.bot:
2955 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002956
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002957 builders_and_tests = {}
2958 # TODO(machenbach): The old style command-line options don't support
2959 # multiple try masters yet.
2960 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2961 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2962
2963 for bot in old_style:
2964 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002965 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002966 elif ',' in bot:
2967 parser.error('Specify one bot per --bot flag')
2968 else:
2969 builders_and_tests.setdefault(bot, []).append('defaulttests')
2970
2971 for bot, tests in new_style:
2972 builders_and_tests.setdefault(bot, []).extend(tests)
2973
2974 # Return a master map with one master to be backwards compatible. The
2975 # master name defaults to an empty string, which will cause the master
2976 # not to be set on rietveld (deprecated).
2977 return {options.master: builders_and_tests}
2978
2979 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002980
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002981 for builders in masters.itervalues():
2982 if any('triggered' in b for b in builders):
2983 print >> sys.stderr, (
2984 'ERROR You are trying to send a job to a triggered bot. This type of'
2985 ' bot requires an\ninitial job from a parent (usually a builder). '
2986 'Instead send your job to the parent.\n'
2987 'Bot list: %s' % builders)
2988 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002989
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002990 patchset = cl.GetMostRecentPatchset()
2991 if patchset and patchset != cl.GetPatchset():
2992 print(
2993 '\nWARNING Mismatch between local config and server. Did a previous '
2994 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2995 'Continuing using\npatchset %s.\n' % patchset)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00002996 if options.use_buildbucket:
2997 try:
2998 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
2999 except BuildbucketResponseException as ex:
3000 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003001 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003002 except Exception as e:
3003 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3004 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3005 e, stacktrace)
3006 return 1
3007 else:
3008 try:
3009 cl.RpcServer().trigger_distributed_try_jobs(
3010 cl.GetIssue(), patchset, options.name, options.clobber,
3011 options.revision, masters)
3012 except urllib2.HTTPError as e:
3013 if e.code == 404:
3014 print('404 from rietveld; '
3015 'did you mean to use "git try" instead of "git cl try"?')
3016 return 1
3017 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003018
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003019 for (master, builders) in sorted(masters.iteritems()):
3020 if master:
3021 print 'Master: %s' % master
3022 length = max(len(builder) for builder in builders)
3023 for builder in sorted(builders):
3024 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003025 return 0
3026
3027
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003028@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003029def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003030 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003031 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003032 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003033 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003034
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003035 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003036 if args:
3037 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003038 branch = cl.GetBranch()
3039 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003040 cl = Changelist()
3041 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003042
3043 # Clear configured merge-base, if there is one.
3044 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003045 else:
3046 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003047 return 0
3048
3049
thestig@chromium.org00858c82013-12-02 23:08:03 +00003050def CMDweb(parser, args):
3051 """Opens the current CL in the web browser."""
3052 _, args = parser.parse_args(args)
3053 if args:
3054 parser.error('Unrecognized args: %s' % ' '.join(args))
3055
3056 issue_url = Changelist().GetIssueURL()
3057 if not issue_url:
3058 print >> sys.stderr, 'ERROR No issue to open'
3059 return 1
3060
3061 webbrowser.open(issue_url)
3062 return 0
3063
3064
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003065def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003066 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003067 auth.add_auth_options(parser)
3068 options, args = parser.parse_args(args)
3069 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003070 if args:
3071 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003072 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003073 props = cl.GetIssueProperties()
3074 if props.get('private'):
3075 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003076 cl.SetFlag('commit', '1')
3077 return 0
3078
3079
groby@chromium.org411034a2013-02-26 15:12:01 +00003080def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003081 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003082 auth.add_auth_options(parser)
3083 options, args = parser.parse_args(args)
3084 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003085 if args:
3086 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003087 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003088 # Ensure there actually is an issue to close.
3089 cl.GetDescription()
3090 cl.CloseIssue()
3091 return 0
3092
3093
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003094def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003095 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003096 auth.add_auth_options(parser)
3097 options, args = parser.parse_args(args)
3098 auth_config = auth.extract_auth_config_from_options(options)
3099 if args:
3100 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003101
3102 # Uncommitted (staged and unstaged) changes will be destroyed by
3103 # "git reset --hard" if there are merging conflicts in PatchIssue().
3104 # Staged changes would be committed along with the patch from last
3105 # upload, hence counted toward the "last upload" side in the final
3106 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003107 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003108 return 1
3109
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003110 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003111 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003112 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003113 if not issue:
3114 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003115 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003116 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003117
3118 # Create a new branch based on the merge-base
3119 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3120 try:
3121 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003122 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003123 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003124 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003125 return rtn
3126
wychen@chromium.org06928532015-02-03 02:11:29 +00003127 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003128 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003129 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003130 finally:
3131 RunGit(['checkout', '-q', branch])
3132 RunGit(['branch', '-D', TMP_BRANCH])
3133
3134 return 0
3135
3136
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003137def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003138 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003139 parser.add_option(
3140 '--no-color',
3141 action='store_true',
3142 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003143 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003144 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003145 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003146
3147 author = RunGit(['config', 'user.email']).strip() or None
3148
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003149 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003150
3151 if args:
3152 if len(args) > 1:
3153 parser.error('Unknown args')
3154 base_branch = args[0]
3155 else:
3156 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003157 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003158
3159 change = cl.GetChange(base_branch, None)
3160 return owners_finder.OwnersFinder(
3161 [f.LocalPath() for f in
3162 cl.GetChange(base_branch, None).AffectedFiles()],
3163 change.RepositoryRoot(), author,
3164 fopen=file, os_path=os.path, glob=glob.glob,
3165 disable_color=options.no_color).run()
3166
3167
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003168def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
3169 """Generates a diff command."""
3170 # Generate diff for the current branch's changes.
3171 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3172 upstream_commit, '--' ]
3173
3174 if args:
3175 for arg in args:
3176 if os.path.isdir(arg):
3177 diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
3178 elif os.path.isfile(arg):
3179 diff_cmd.append(arg)
3180 else:
3181 DieWithError('Argument "%s" is not a file or a directory' % arg)
3182 else:
3183 diff_cmd.extend('*' + ext for ext in extensions)
3184
3185 return diff_cmd
3186
3187
enne@chromium.org555cfe42014-01-29 18:21:39 +00003188@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003189def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003190 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003191 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003192 parser.add_option('--full', action='store_true',
3193 help='Reformat the full content of all touched files')
3194 parser.add_option('--dry-run', action='store_true',
3195 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003196 parser.add_option('--python', action='store_true',
3197 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003198 parser.add_option('--diff', action='store_true',
3199 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003200 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003201
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003202 # git diff generates paths against the root of the repository. Change
3203 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003204 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003205 if rel_base_path:
3206 os.chdir(rel_base_path)
3207
digit@chromium.org29e47272013-05-17 17:01:46 +00003208 # Grab the merge-base commit, i.e. the upstream commit of the current
3209 # branch when it was created or the last time it was rebased. This is
3210 # to cover the case where the user may have called "git fetch origin",
3211 # moving the origin branch to a newer commit, but hasn't rebased yet.
3212 upstream_commit = None
3213 cl = Changelist()
3214 upstream_branch = cl.GetUpstreamBranch()
3215 if upstream_branch:
3216 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3217 upstream_commit = upstream_commit.strip()
3218
3219 if not upstream_commit:
3220 DieWithError('Could not find base commit for this branch. '
3221 'Are you in detached state?')
3222
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003223 if opts.full:
3224 # Only list the names of modified files.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003225 diff_type = '--name-only'
enne@chromium.org555cfe42014-01-29 18:21:39 +00003226 else:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003227 # Only generate context-less patches.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003228 diff_type = '-U0'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003229
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003230 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, CLANG_EXTS)
digit@chromium.org29e47272013-05-17 17:01:46 +00003231 diff_output = RunGit(diff_cmd)
3232
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003233 top_dir = os.path.normpath(
3234 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3235
3236 # Locate the clang-format binary in the checkout
3237 try:
3238 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3239 except clang_format.NotFoundError, e:
3240 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003241
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003242 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3243 # formatted. This is used to block during the presubmit.
3244 return_value = 0
3245
digit@chromium.org29e47272013-05-17 17:01:46 +00003246 if opts.full:
3247 # diff_output is a list of files to send to clang-format.
3248 files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003249 if files:
3250 cmd = [clang_format_tool]
3251 if not opts.dry_run and not opts.diff:
3252 cmd.append('-i')
3253 stdout = RunCommand(cmd + files, cwd=top_dir)
3254 if opts.diff:
3255 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003256 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003257 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003258 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00003259 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003260 try:
3261 script = clang_format.FindClangFormatScriptInChromiumTree(
3262 'clang-format-diff.py')
3263 except clang_format.NotFoundError, e:
3264 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003265
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003266 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003267 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003268 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003269
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003270 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003271 if opts.diff:
3272 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003273 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003274 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003275
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003276 # Similar code to above, but using yapf on .py files rather than clang-format
3277 # on C/C++ files
3278 if opts.python:
3279 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, ['.py'])
3280 diff_output = RunGit(diff_cmd)
3281 yapf_tool = gclient_utils.FindExecutable('yapf')
3282 if yapf_tool is None:
3283 DieWithError('yapf not found in PATH')
3284
3285 if opts.full:
3286 files = diff_output.splitlines()
3287 if files:
3288 cmd = [yapf_tool]
3289 if not opts.dry_run and not opts.diff:
3290 cmd.append('-i')
3291 stdout = RunCommand(cmd + files, cwd=top_dir)
3292 if opts.diff:
3293 sys.stdout.write(stdout)
3294 else:
3295 # TODO(sbc): yapf --lines mode still has some issues.
3296 # https://github.com/google/yapf/issues/154
3297 DieWithError('--python currently only works with --full')
3298
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003299 # Build a diff command that only operates on dart files. dart's formatter
3300 # does not have the nice property of only operating on modified chunks, so
3301 # hard code full.
3302 dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3303 args, ['.dart'])
3304 dart_diff_output = RunGit(dart_diff_cmd)
3305 if dart_diff_output:
3306 try:
3307 command = [dart_format.FindDartFmtToolInChromiumTree()]
3308 if not opts.dry_run and not opts.diff:
3309 command.append('-w')
3310 command.extend(dart_diff_output.splitlines())
3311
3312 stdout = RunCommand(command, cwd=top_dir, env=env)
3313 if opts.dry_run and stdout:
3314 return_value = 2
3315 except dart_format.NotFoundError as e:
3316 print ('Unable to check dart code formatting. Dart SDK is not in ' +
3317 'this checkout.')
3318
3319 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003320
3321
maruel@chromium.org29404b52014-09-08 22:58:00 +00003322def CMDlol(parser, args):
3323 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003324 print zlib.decompress(base64.b64decode(
3325 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3326 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3327 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3328 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003329 return 0
3330
3331
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003332class OptionParser(optparse.OptionParser):
3333 """Creates the option parse and add --verbose support."""
3334 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003335 optparse.OptionParser.__init__(
3336 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003337 self.add_option(
3338 '-v', '--verbose', action='count', default=0,
3339 help='Use 2 times for more debugging info')
3340
3341 def parse_args(self, args=None, values=None):
3342 options, args = optparse.OptionParser.parse_args(self, args, values)
3343 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3344 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3345 return options, args
3346
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003347
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003348def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003349 if sys.hexversion < 0x02060000:
3350 print >> sys.stderr, (
3351 '\nYour python version %s is unsupported, please upgrade.\n' %
3352 sys.version.split(' ', 1)[0])
3353 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003354
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003355 # Reload settings.
3356 global settings
3357 settings = Settings()
3358
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003359 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003360 dispatcher = subcommand.CommandDispatcher(__name__)
3361 try:
3362 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003363 except auth.AuthenticationError as e:
3364 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003365 except urllib2.HTTPError, e:
3366 if e.code != 500:
3367 raise
3368 DieWithError(
3369 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3370 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003371 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003372
3373
3374if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003375 # These affect sys.stdout so do it outside of main() to simplify mocks in
3376 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003377 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003378 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003379 try:
3380 sys.exit(main(sys.argv[1:]))
3381 except KeyboardInterrupt:
3382 sys.stderr.write('interrupted\n')
3383 sys.exit(1)