blob: 5ff11fc152c0d6e12e2124944ad8f152968001a5 [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
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001460def fetch_cl_status(b, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001461 """Fetches information for an issue and returns (branch, issue, color)."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001462 c = Changelist(branchref=b, auth_config=auth_config)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001463 i = c.GetIssueURL()
1464 status = c.GetStatus()
1465 color = color_for_status(status)
1466
1467 if i and (not status or status == 'error'):
1468 # The issue probably doesn't exist anymore.
1469 i += ' (broken)'
1470
1471 return (b, i, color)
1472
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001473def get_cl_statuses(
1474 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001475 """Returns a blocking iterable of (branch, issue, color) for given branches.
1476
1477 If fine_grained is true, this will fetch CL statuses from the server.
1478 Otherwise, simply indicate if there's a matching url for the given branches.
1479
1480 If max_processes is specified, it is used as the maximum number of processes
1481 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1482 spawned.
1483 """
1484 # Silence upload.py otherwise it becomes unwieldly.
1485 upload.verbosity = 0
1486
1487 if fine_grained:
1488 # Process one branch synchronously to work through authentication, then
1489 # spawn processes to process all the other branches in parallel.
1490 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001491 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1492 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001493
1494 branches_to_fetch = branches[1:]
1495 pool = ThreadPool(
1496 min(max_processes, len(branches_to_fetch))
1497 if max_processes is not None
1498 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001499 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001500 yield x
1501 else:
1502 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1503 for b in branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001504 c = Changelist(branchref=b, auth_config=auth_config)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001505 url = c.GetIssueURL()
1506 yield (b, url, Fore.BLUE if url else Fore.WHITE)
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001507
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001508def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001509 """Show status of changelists.
1510
1511 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001512 - Red not sent for review or broken
1513 - Blue waiting for review
1514 - Yellow waiting for you to reply to review
1515 - Green LGTM'ed
1516 - Magenta in the commit queue
1517 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001518
1519 Also see 'git cl comments'.
1520 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001521 parser.add_option('--field',
1522 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001523 parser.add_option('-f', '--fast', action='store_true',
1524 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001525 parser.add_option(
1526 '-j', '--maxjobs', action='store', type=int,
1527 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001528
1529 auth.add_auth_options(parser)
1530 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001531 if args:
1532 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001533 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001534
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001535 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001536 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001537 if options.field.startswith('desc'):
1538 print cl.GetDescription()
1539 elif options.field == 'id':
1540 issueid = cl.GetIssue()
1541 if issueid:
1542 print issueid
1543 elif options.field == 'patch':
1544 patchset = cl.GetPatchset()
1545 if patchset:
1546 print patchset
1547 elif options.field == 'url':
1548 url = cl.GetIssueURL()
1549 if url:
1550 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001551 return 0
1552
1553 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1554 if not branches:
1555 print('No local branch found.')
1556 return 0
1557
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001558 changes = (
1559 Changelist(branchref=b, auth_config=auth_config)
1560 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001561 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001562 alignment = max(5, max(len(b) for b in branches))
1563 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001564 output = get_cl_statuses(branches,
1565 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001566 max_processes=options.maxjobs,
1567 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001568
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001569 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001570 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001571 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001572 while branch not in branch_statuses:
1573 b, i, color = output.next()
1574 branch_statuses[b] = (i, color)
1575 issue, color = branch_statuses.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001576 reset = Fore.RESET
1577 if not sys.stdout.isatty():
1578 color = ''
1579 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001580 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001581 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001582
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001583 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001584 print
1585 print 'Current branch:',
1586 if not cl.GetIssue():
1587 print 'no issue assigned.'
1588 return 0
1589 print cl.GetBranch()
1590 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001591 if not options.fast:
1592 print 'Issue description:'
1593 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001594 return 0
1595
1596
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001597def colorize_CMDstatus_doc():
1598 """To be called once in main() to add colors to git cl status help."""
1599 colors = [i for i in dir(Fore) if i[0].isupper()]
1600
1601 def colorize_line(line):
1602 for color in colors:
1603 if color in line.upper():
1604 # Extract whitespaces first and the leading '-'.
1605 indent = len(line) - len(line.lstrip(' ')) + 1
1606 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1607 return line
1608
1609 lines = CMDstatus.__doc__.splitlines()
1610 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1611
1612
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001613@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001614def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001615 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001616
1617 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001618 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001619 parser.add_option('-r', '--reverse', action='store_true',
1620 help='Lookup the branch(es) for the specified issues. If '
1621 'no issues are specified, all branches with mapped '
1622 'issues will be listed.')
1623 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001624
dnj@chromium.org406c4402015-03-03 17:22:28 +00001625 if options.reverse:
1626 branches = RunGit(['for-each-ref', 'refs/heads',
1627 '--format=%(refname:short)']).splitlines()
1628
1629 # Reverse issue lookup.
1630 issue_branch_map = {}
1631 for branch in branches:
1632 cl = Changelist(branchref=branch)
1633 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1634 if not args:
1635 args = sorted(issue_branch_map.iterkeys())
1636 for issue in args:
1637 if not issue:
1638 continue
1639 print 'Branch for issue number %s: %s' % (
1640 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1641 else:
1642 cl = Changelist()
1643 if len(args) > 0:
1644 try:
1645 issue = int(args[0])
1646 except ValueError:
1647 DieWithError('Pass a number to set the issue or none to list it.\n'
1648 'Maybe you want to run git cl status?')
1649 cl.SetIssue(issue)
1650 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001651 return 0
1652
1653
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001654def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001655 """Shows or posts review comments for any changelist."""
1656 parser.add_option('-a', '--add-comment', dest='comment',
1657 help='comment to add to an issue')
1658 parser.add_option('-i', dest='issue',
1659 help="review issue id (defaults to current issue)")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001660 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001661 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001662 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001663
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001664 issue = None
1665 if options.issue:
1666 try:
1667 issue = int(options.issue)
1668 except ValueError:
1669 DieWithError('A review issue id is expected to be a number')
1670
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001671 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001672
1673 if options.comment:
1674 cl.AddComment(options.comment)
1675 return 0
1676
1677 data = cl.GetIssueProperties()
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001678 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001679 if message['disapproval']:
1680 color = Fore.RED
1681 elif message['approval']:
1682 color = Fore.GREEN
1683 elif message['sender'] == data['owner_email']:
1684 color = Fore.MAGENTA
1685 else:
1686 color = Fore.BLUE
1687 print '\n%s%s %s%s' % (
1688 color, message['date'].split('.', 1)[0], message['sender'],
1689 Fore.RESET)
1690 if message['text'].strip():
1691 print '\n'.join(' ' + l for l in message['text'].splitlines())
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001692 return 0
1693
1694
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001695def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001696 """Brings up the editor for the current CL's description."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001697 auth.add_auth_options(parser)
1698 options, _ = parser.parse_args(args)
1699 auth_config = auth.extract_auth_config_from_options(options)
1700 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001701 if not cl.GetIssue():
1702 DieWithError('This branch has no associated changelist.')
1703 description = ChangeDescription(cl.GetDescription())
1704 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00001705 if cl.GetDescription() != description.description:
1706 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001707 return 0
1708
1709
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001710def CreateDescriptionFromLog(args):
1711 """Pulls out the commit log to use as a base for the CL description."""
1712 log_args = []
1713 if len(args) == 1 and not args[0].endswith('.'):
1714 log_args = [args[0] + '..']
1715 elif len(args) == 1 and args[0].endswith('...'):
1716 log_args = [args[0][:-1]]
1717 elif len(args) == 2:
1718 log_args = [args[0] + '..' + args[1]]
1719 else:
1720 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001721 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001722
1723
thestig@chromium.org44202a22014-03-11 19:22:18 +00001724def CMDlint(parser, args):
1725 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001726 parser.add_option('--filter', action='append', metavar='-x,+y',
1727 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001728 auth.add_auth_options(parser)
1729 options, args = parser.parse_args(args)
1730 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001731
1732 # Access to a protected member _XX of a client class
1733 # pylint: disable=W0212
1734 try:
1735 import cpplint
1736 import cpplint_chromium
1737 except ImportError:
1738 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1739 return 1
1740
1741 # Change the current working directory before calling lint so that it
1742 # shows the correct base.
1743 previous_cwd = os.getcwd()
1744 os.chdir(settings.GetRoot())
1745 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001746 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001747 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1748 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001749 if not files:
1750 print "Cannot lint an empty CL"
1751 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001752
1753 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001754 command = args + files
1755 if options.filter:
1756 command = ['--filter=' + ','.join(options.filter)] + command
1757 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001758
1759 white_regex = re.compile(settings.GetLintRegex())
1760 black_regex = re.compile(settings.GetLintIgnoreRegex())
1761 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1762 for filename in filenames:
1763 if white_regex.match(filename):
1764 if black_regex.match(filename):
1765 print "Ignoring file %s" % filename
1766 else:
1767 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1768 extra_check_functions)
1769 else:
1770 print "Skipping file %s" % filename
1771 finally:
1772 os.chdir(previous_cwd)
1773 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1774 if cpplint._cpplint_state.error_count != 0:
1775 return 1
1776 return 0
1777
1778
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001779def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001780 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001781 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001782 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001783 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001784 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001785 auth.add_auth_options(parser)
1786 options, args = parser.parse_args(args)
1787 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001788
sbc@chromium.org71437c02015-04-09 19:29:40 +00001789 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00001790 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001791 return 1
1792
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001793 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001794 if args:
1795 base_branch = args[0]
1796 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001797 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001798 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001799
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001800 cl.RunHook(
1801 committing=not options.upload,
1802 may_prompt=False,
1803 verbose=options.verbose,
1804 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001805 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001806
1807
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001808def AddChangeIdToCommitMessage(options, args):
1809 """Re-commits using the current message, assumes the commit hook is in
1810 place.
1811 """
1812 log_desc = options.message or CreateDescriptionFromLog(args)
1813 git_command = ['commit', '--amend', '-m', log_desc]
1814 RunGit(git_command)
1815 new_log_desc = CreateDescriptionFromLog(args)
1816 if CHANGE_ID in new_log_desc:
1817 print 'git-cl: Added Change-Id to commit message.'
1818 else:
1819 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1820
1821
piman@chromium.org336f9122014-09-04 02:16:55 +00001822def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001823 """upload the current branch to gerrit."""
1824 # We assume the remote called "origin" is the one we want.
1825 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001826 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00001827
1828 remote, remote_branch = cl.GetRemoteBranch()
1829 branch = GetTargetRef(remote, remote_branch, options.target_branch,
1830 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001831
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001832 change_desc = ChangeDescription(
1833 options.message or CreateDescriptionFromLog(args))
1834 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001835 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001836 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001837
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001838 if options.squash:
1839 # Try to get the message from a previous upload.
1840 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
1841 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
1842 if not message:
1843 if not options.force:
1844 change_desc.prompt()
1845
1846 if CHANGE_ID not in change_desc.description:
1847 # Run the commit-msg hook without modifying the head commit by writing
1848 # the commit message to a temporary file and running the hook over it,
1849 # then reading the file back in.
1850 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
1851 'commit-msg')
1852 file_handle, msg_file = tempfile.mkstemp(text=True,
1853 prefix='commit_msg')
1854 try:
1855 try:
1856 with os.fdopen(file_handle, 'w') as fileobj:
1857 fileobj.write(change_desc.description)
1858 finally:
1859 os.close(file_handle)
1860 RunCommand([commit_msg_hook, msg_file])
1861 change_desc.set_description(gclient_utils.FileRead(msg_file))
1862 finally:
1863 os.remove(msg_file)
1864
1865 if not change_desc.description:
1866 print "Description is empty; aborting."
1867 return 1
1868
1869 message = change_desc.description
1870
1871 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1872 if remote is '.':
1873 # If our upstream branch is local, we base our squashed commit on its
1874 # squashed version.
1875 parent = ('refs/heads/git_cl_uploads/' +
1876 scm.GIT.ShortBranchName(upstream_branch))
1877
1878 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
1879 # will create additional CLs when uploading.
1880 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
1881 RunGitSilent(['rev-parse', parent + ':'])):
1882 print 'Upload upstream branch ' + upstream_branch + ' first.'
1883 return 1
1884 else:
1885 parent = cl.GetCommonAncestorWithUpstream()
1886
1887 tree = RunGit(['rev-parse', 'HEAD:']).strip()
1888 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
1889 '-m', message]).strip()
1890 else:
1891 if CHANGE_ID not in change_desc.description:
1892 AddChangeIdToCommitMessage(options, args)
1893 ref_to_push = 'HEAD'
1894 parent = '%s/%s' % (gerrit_remote, branch)
1895
1896 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
1897 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001898 if len(commits) > 1:
1899 print('WARNING: This will upload %d commits. Run the following command '
1900 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001901 print('git log %s..%s' % (parent, ref_to_push))
1902 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001903 'commit.')
1904 ask_for_data('About to upload; enter to confirm.')
1905
piman@chromium.org336f9122014-09-04 02:16:55 +00001906 if options.reviewers or options.tbr_owners:
1907 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001908
ukai@chromium.orge8077812012-02-03 03:41:46 +00001909 receive_options = []
1910 cc = cl.GetCCList().split(',')
1911 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001912 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001913 cc = filter(None, cc)
1914 if cc:
1915 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001916 if change_desc.get_reviewers():
1917 receive_options.extend(
1918 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001919
ukai@chromium.orge8077812012-02-03 03:41:46 +00001920 git_command = ['push']
1921 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001922 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001923 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001924 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00001925 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001926
1927 if options.squash:
1928 head = RunGit(['rev-parse', 'HEAD']).strip()
1929 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
1930
ukai@chromium.orge8077812012-02-03 03:41:46 +00001931 # TODO(ukai): parse Change-Id: and set issue number?
1932 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001933
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001934
wittman@chromium.org455dc922015-01-26 20:15:50 +00001935def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
1936 """Computes the remote branch ref to use for the CL.
1937
1938 Args:
1939 remote (str): The git remote for the CL.
1940 remote_branch (str): The git remote branch for the CL.
1941 target_branch (str): The target branch specified by the user.
1942 pending_prefix (str): The pending prefix from the settings.
1943 """
1944 if not (remote and remote_branch):
1945 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001946
wittman@chromium.org455dc922015-01-26 20:15:50 +00001947 if target_branch:
1948 # Cannonicalize branch references to the equivalent local full symbolic
1949 # refs, which are then translated into the remote full symbolic refs
1950 # below.
1951 if '/' not in target_branch:
1952 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
1953 else:
1954 prefix_replacements = (
1955 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
1956 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
1957 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
1958 )
1959 match = None
1960 for regex, replacement in prefix_replacements:
1961 match = re.search(regex, target_branch)
1962 if match:
1963 remote_branch = target_branch.replace(match.group(0), replacement)
1964 break
1965 if not match:
1966 # This is a branch path but not one we recognize; use as-is.
1967 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00001968 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
1969 # Handle the refs that need to land in different refs.
1970 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001971
wittman@chromium.org455dc922015-01-26 20:15:50 +00001972 # Create the true path to the remote branch.
1973 # Does the following translation:
1974 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
1975 # * refs/remotes/origin/master -> refs/heads/master
1976 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
1977 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
1978 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
1979 elif remote_branch.startswith('refs/remotes/%s/' % remote):
1980 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
1981 'refs/heads/')
1982 elif remote_branch.startswith('refs/remotes/branch-heads'):
1983 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
1984 # If a pending prefix exists then replace refs/ with it.
1985 if pending_prefix:
1986 remote_branch = remote_branch.replace('refs/', pending_prefix)
1987 return remote_branch
1988
1989
piman@chromium.org336f9122014-09-04 02:16:55 +00001990def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001991 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001992 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1993 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001994 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001995 if options.emulate_svn_auto_props:
1996 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001997
1998 change_desc = None
1999
pgervais@chromium.org91141372014-01-09 23:27:20 +00002000 if options.email is not None:
2001 upload_args.extend(['--email', options.email])
2002
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002003 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002004 if options.title:
2005 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002006 if options.message:
2007 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002008 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002009 print ("This branch is associated with issue %s. "
2010 "Adding patch to that issue." % cl.GetIssue())
2011 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002012 if options.title:
2013 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002014 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002015 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002016 if options.reviewers or options.tbr_owners:
2017 change_desc.update_reviewers(options.reviewers,
2018 options.tbr_owners,
2019 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002020 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002021 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002022
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002023 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002024 print "Description is empty; aborting."
2025 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002026
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002027 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002028 if change_desc.get_reviewers():
2029 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002030 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002031 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002032 DieWithError("Must specify reviewers to send email.")
2033 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002034
2035 # We check this before applying rietveld.private assuming that in
2036 # rietveld.cc only addresses which we can send private CLs to are listed
2037 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2038 # --private is specified explicitly on the command line.
2039 if options.private:
2040 logging.warn('rietveld.cc is ignored since private flag is specified. '
2041 'You need to review and add them manually if necessary.')
2042 cc = cl.GetCCListWithoutDefault()
2043 else:
2044 cc = cl.GetCCList()
2045 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002046 if cc:
2047 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002048
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002049 if options.private or settings.GetDefaultPrivateFlag() == "True":
2050 upload_args.append('--private')
2051
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002052 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002053 if not options.find_copies:
2054 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002055
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002056 # Include the upstream repo's URL in the change -- this is useful for
2057 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002058 remote_url = cl.GetGitBaseUrlFromConfig()
2059 if not remote_url:
2060 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002061 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002062 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002063 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2064 remote_url = (cl.GetRemoteUrl() + '@'
2065 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002066 if remote_url:
2067 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002068 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002069 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2070 settings.GetPendingRefPrefix())
2071 if target_ref:
2072 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002073
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002074 project = settings.GetProject()
2075 if project:
2076 upload_args.extend(['--project', project])
2077
rmistry@google.comef966222015-04-07 11:15:01 +00002078 if options.cq_dry_run:
2079 upload_args.extend(['--cq_dry_run'])
2080
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002081 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002082 upload_args = ['upload'] + upload_args + args
2083 logging.info('upload.RealMain(%s)', upload_args)
2084 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002085 issue = int(issue)
2086 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002087 except KeyboardInterrupt:
2088 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002089 except:
2090 # If we got an exception after the user typed a description for their
2091 # change, back up the description before re-raising.
2092 if change_desc:
2093 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2094 print '\nGot exception while uploading -- saving description to %s\n' \
2095 % backup_path
2096 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002097 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002098 backup_file.close()
2099 raise
2100
2101 if not cl.GetIssue():
2102 cl.SetIssue(issue)
2103 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002104
2105 if options.use_commit_queue:
2106 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002107 return 0
2108
2109
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002110def cleanup_list(l):
2111 """Fixes a list so that comma separated items are put as individual items.
2112
2113 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2114 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2115 """
2116 items = sum((i.split(',') for i in l), [])
2117 stripped_items = (i.strip() for i in items)
2118 return sorted(filter(None, stripped_items))
2119
2120
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002121@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002122def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002123 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00002124 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2125 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002126 parser.add_option('--bypass-watchlists', action='store_true',
2127 dest='bypass_watchlists',
2128 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002129 parser.add_option('-f', action='store_true', dest='force',
2130 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002131 parser.add_option('-m', dest='message', help='message for patchset')
2132 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002133 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002134 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002135 help='reviewer email addresses')
2136 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002137 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002138 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002139 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002140 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002141 parser.add_option('--emulate_svn_auto_props',
2142 '--emulate-svn-auto-props',
2143 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002144 dest="emulate_svn_auto_props",
2145 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002146 parser.add_option('-c', '--use-commit-queue', action='store_true',
2147 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002148 parser.add_option('--private', action='store_true',
2149 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002150 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002151 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002152 metavar='TARGET',
2153 help='Apply CL to remote ref TARGET. ' +
2154 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002155 parser.add_option('--squash', action='store_true',
2156 help='Squash multiple commits into one (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002157 parser.add_option('--email', default=None,
2158 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002159 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2160 help='add a set of OWNERS to TBR')
rmistry@google.comef966222015-04-07 11:15:01 +00002161 parser.add_option('--cq-dry-run', dest='cq_dry_run', action='store_true',
2162 help='Send the patchset to do a CQ dry run right after '
2163 'upload.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002164
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002165 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002166 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002167 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002168 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002169
sbc@chromium.org71437c02015-04-09 19:29:40 +00002170 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002171 return 1
2172
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002173 options.reviewers = cleanup_list(options.reviewers)
2174 options.cc = cleanup_list(options.cc)
2175
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002176 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002177 if args:
2178 # TODO(ukai): is it ok for gerrit case?
2179 base_branch = args[0]
2180 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002181 if cl.GetBranch() is None:
2182 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2183
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002184 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002185 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002186 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002187
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002188 # Make sure authenticated to Rietveld before running expensive hooks. It is
2189 # a fast, best efforts check. Rietveld still can reject the authentication
2190 # during the actual upload.
2191 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2192 authenticator = auth.get_authenticator_for_host(
2193 cl.GetRietveldServer(), auth_config)
2194 if not authenticator.has_cached_credentials():
2195 raise auth.LoginRequiredError(cl.GetRietveldServer())
2196
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002197 # Apply watchlists on upload.
2198 change = cl.GetChange(base_branch, None)
2199 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2200 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002201 if not options.bypass_watchlists:
2202 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002203
ukai@chromium.orge8077812012-02-03 03:41:46 +00002204 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002205 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002206 # Set the reviewer list now so that presubmit checks can access it.
2207 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002208 change_description.update_reviewers(options.reviewers,
2209 options.tbr_owners,
2210 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002211 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002212 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002213 may_prompt=not options.force,
2214 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002215 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002216 if not hook_results.should_continue():
2217 return 1
2218 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002219 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002220
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002221 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002222 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002223 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002224 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002225 print ('The last upload made from this repository was patchset #%d but '
2226 'the most recent patchset on the server is #%d.'
2227 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002228 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2229 'from another machine or branch the patch you\'re uploading now '
2230 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002231 ask_for_data('About to upload; enter to confirm.')
2232
iannucci@chromium.org79540052012-10-19 23:15:26 +00002233 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002234 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00002235 return GerritUpload(options, args, cl, change)
2236 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002237 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002238 git_set_branch_value('last-upload-hash',
2239 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002240 # Run post upload hooks, if specified.
2241 if settings.GetRunPostUploadHook():
2242 presubmit_support.DoPostUploadExecuter(
2243 change,
2244 cl,
2245 settings.GetRoot(),
2246 options.verbose,
2247 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002248
2249 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002250
2251
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002252def IsSubmoduleMergeCommit(ref):
2253 # When submodules are added to the repo, we expect there to be a single
2254 # non-git-svn merge commit at remote HEAD with a signature comment.
2255 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002256 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002257 return RunGit(cmd) != ''
2258
2259
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002260def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002261 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002262
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002263 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002264 Updates changelog with metadata (e.g. pointer to review).
2265 Pushes/dcommits the code upstream.
2266 Updates review and closes.
2267 """
2268 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2269 help='bypass upload presubmit hook')
2270 parser.add_option('-m', dest='message',
2271 help="override review description")
2272 parser.add_option('-f', action='store_true', dest='force',
2273 help="force yes to questions (don't prompt)")
2274 parser.add_option('-c', dest='contributor',
2275 help="external contributor for patch (appended to " +
2276 "description and used as author for git). Should be " +
2277 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002278 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002279 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002280 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002281 auth_config = auth.extract_auth_config_from_options(options)
2282
2283 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002284
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002285 current = cl.GetBranch()
2286 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2287 if not settings.GetIsGitSvn() and remote == '.':
2288 print
2289 print 'Attempting to push branch %r into another local branch!' % current
2290 print
2291 print 'Either reparent this branch on top of origin/master:'
2292 print ' git reparent-branch --root'
2293 print
2294 print 'OR run `git rebase-update` if you think the parent branch is already'
2295 print 'committed.'
2296 print
2297 print ' Current parent: %r' % upstream_branch
2298 return 1
2299
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002300 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002301 # Default to merging against our best guess of the upstream branch.
2302 args = [cl.GetUpstreamBranch()]
2303
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002304 if options.contributor:
2305 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2306 print "Please provide contibutor as 'First Last <email@example.com>'"
2307 return 1
2308
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002309 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002310 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002311
sbc@chromium.org71437c02015-04-09 19:29:40 +00002312 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002313 return 1
2314
2315 # This rev-list syntax means "show all commits not in my branch that
2316 # are in base_branch".
2317 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2318 base_branch]).splitlines()
2319 if upstream_commits:
2320 print ('Base branch "%s" has %d commits '
2321 'not in this branch.' % (base_branch, len(upstream_commits)))
2322 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2323 return 1
2324
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002325 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002326 svn_head = None
2327 if cmd == 'dcommit' or base_has_submodules:
2328 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2329 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002330
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002331 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002332 # If the base_head is a submodule merge commit, the first parent of the
2333 # base_head should be a git-svn commit, which is what we're interested in.
2334 base_svn_head = base_branch
2335 if base_has_submodules:
2336 base_svn_head += '^1'
2337
2338 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002339 if extra_commits:
2340 print ('This branch has %d additional commits not upstreamed yet.'
2341 % len(extra_commits.splitlines()))
2342 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2343 'before attempting to %s.' % (base_branch, cmd))
2344 return 1
2345
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002346 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002347 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002348 author = None
2349 if options.contributor:
2350 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002351 hook_results = cl.RunHook(
2352 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002353 may_prompt=not options.force,
2354 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002355 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002356 if not hook_results.should_continue():
2357 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002358
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002359 # Check the tree status if the tree status URL is set.
2360 status = GetTreeStatus()
2361 if 'closed' == status:
2362 print('The tree is closed. Please wait for it to reopen. Use '
2363 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2364 return 1
2365 elif 'unknown' == status:
2366 print('Unable to determine tree status. Please verify manually and '
2367 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2368 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002369 else:
2370 breakpad.SendStack(
2371 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002372 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2373 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002374 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002375
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002376 change_desc = ChangeDescription(options.message)
2377 if not change_desc.description and cl.GetIssue():
2378 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002379
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002380 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002381 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002382 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002383 else:
2384 print 'No description set.'
2385 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2386 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002387
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002388 # Keep a separate copy for the commit message, because the commit message
2389 # contains the link to the Rietveld issue, while the Rietveld message contains
2390 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002391 # Keep a separate copy for the commit message.
2392 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002393 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002394
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002395 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002396 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002397 # Xcode won't linkify this URL unless there is a non-whitespace character
2398 # after it. Add a period on a new line to circumvent this.
2399 commit_desc.append_footer('Review URL: %s.' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002400 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002401 commit_desc.append_footer('Patch from %s.' % options.contributor)
2402
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002403 print('Description:')
2404 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002405
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002406 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002407 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002408 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002409
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002410 # We want to squash all this branch's commits into one commit with the proper
2411 # description. We do this by doing a "reset --soft" to the base branch (which
2412 # keeps the working copy the same), then dcommitting that. If origin/master
2413 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2414 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002415 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002416 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2417 # Delete the branches if they exist.
2418 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2419 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2420 result = RunGitWithCode(showref_cmd)
2421 if result[0] == 0:
2422 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002423
2424 # We might be in a directory that's present in this branch but not in the
2425 # trunk. Move up to the top of the tree so that git commands that expect a
2426 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002427 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002428 if rel_base_path:
2429 os.chdir(rel_base_path)
2430
2431 # Stuff our change into the merge branch.
2432 # We wrap in a try...finally block so if anything goes wrong,
2433 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002434 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002435 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002436 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002437 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002438 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002439 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002440 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002441 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002442 RunGit(
2443 [
2444 'commit', '--author', options.contributor,
2445 '-m', commit_desc.description,
2446 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002447 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002448 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002449 if base_has_submodules:
2450 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2451 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2452 RunGit(['checkout', CHERRY_PICK_BRANCH])
2453 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002454 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002455 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002456 pending_prefix = settings.GetPendingRefPrefix()
2457 if not pending_prefix or branch.startswith(pending_prefix):
2458 # If not using refs/pending/heads/* at all, or target ref is already set
2459 # to pending, then push to the target ref directly.
2460 retcode, output = RunGitWithCode(
2461 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002462 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002463 else:
2464 # Cherry-pick the change on top of pending ref and then push it.
2465 assert branch.startswith('refs/'), branch
2466 assert pending_prefix[-1] == '/', pending_prefix
2467 pending_ref = pending_prefix + branch[len('refs/'):]
2468 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002469 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002470 if retcode == 0:
2471 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002472 else:
2473 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002474 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002475 'svn', 'dcommit',
2476 '-C%s' % options.similarity,
2477 '--no-rebase', '--rmdir',
2478 ]
2479 if settings.GetForceHttpsCommitUrl():
2480 # Allow forcing https commit URLs for some projects that don't allow
2481 # committing to http URLs (like Google Code).
2482 remote_url = cl.GetGitSvnRemoteUrl()
2483 if urlparse.urlparse(remote_url).scheme == 'http':
2484 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002485 cmd_args.append('--commit-url=%s' % remote_url)
2486 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002487 if 'Committed r' in output:
2488 revision = re.match(
2489 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2490 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002491 finally:
2492 # And then swap back to the original branch and clean up.
2493 RunGit(['checkout', '-q', cl.GetBranch()])
2494 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002495 if base_has_submodules:
2496 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002497
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002498 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002499 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002500 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002501
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002502 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002503 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002504 try:
2505 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2506 # We set pushed_to_pending to False, since it made it all the way to the
2507 # real ref.
2508 pushed_to_pending = False
2509 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002510 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002511
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002512 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002513 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002514 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002515 if not to_pending:
2516 if viewvc_url and revision:
2517 change_desc.append_footer(
2518 'Committed: %s%s' % (viewvc_url, revision))
2519 elif revision:
2520 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002521 print ('Closing issue '
2522 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002523 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002524 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002525 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002526 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002527 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002528 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002529 if options.bypass_hooks:
2530 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2531 else:
2532 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002533 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002534 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002535
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002536 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002537 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2538 print 'The commit is in the pending queue (%s).' % pending_ref
2539 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002540 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002541 'footer.' % branch)
2542
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002543 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2544 if os.path.isfile(hook):
2545 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002546
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002547 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002548
2549
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002550def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2551 print
2552 print 'Waiting for commit to be landed on %s...' % real_ref
2553 print '(If you are impatient, you may Ctrl-C once without harm)'
2554 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2555 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2556
2557 loop = 0
2558 while True:
2559 sys.stdout.write('fetching (%d)... \r' % loop)
2560 sys.stdout.flush()
2561 loop += 1
2562
2563 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2564 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2565 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2566 for commit in commits.splitlines():
2567 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2568 print 'Found commit on %s' % real_ref
2569 return commit
2570
2571 current_rev = to_rev
2572
2573
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002574def PushToGitPending(remote, pending_ref, upstream_ref):
2575 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2576
2577 Returns:
2578 (retcode of last operation, output log of last operation).
2579 """
2580 assert pending_ref.startswith('refs/'), pending_ref
2581 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2582 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2583 code = 0
2584 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002585 max_attempts = 3
2586 attempts_left = max_attempts
2587 while attempts_left:
2588 if attempts_left != max_attempts:
2589 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2590 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002591
2592 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002593 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002594 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002595 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002596 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002597 print 'Fetch failed with exit code %d.' % code
2598 if out.strip():
2599 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002600 continue
2601
2602 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002603 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002604 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002605 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002606 if code:
2607 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002608 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2609 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002610 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2611 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002612 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002613 return code, out
2614
2615 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002616 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002617 code, out = RunGitWithCode(
2618 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2619 if code == 0:
2620 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002621 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002622 return code, out
2623
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002624 print 'Push failed with exit code %d.' % code
2625 if out.strip():
2626 print out.strip()
2627 if IsFatalPushFailure(out):
2628 print (
2629 'Fatal push error. Make sure your .netrc credentials and git '
2630 'user.email are correct and you have push access to the repo.')
2631 return code, out
2632
2633 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002634 return code, out
2635
2636
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002637def IsFatalPushFailure(push_stdout):
2638 """True if retrying push won't help."""
2639 return '(prohibited by Gerrit)' in push_stdout
2640
2641
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002642@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002643def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002644 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002645 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002646 message = """This doesn't appear to be an SVN repository.
2647If your project has a git mirror with an upstream SVN master, you probably need
2648to run 'git svn init', see your project's git mirror documentation.
2649If your project has a true writeable upstream repository, you probably want
mark@chromium.org671c7a32014-07-31 17:09:36 +00002650to run 'git cl land' instead.
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002651Choose wisely, if you get this wrong, your commit might appear to succeed but
2652will instead be silently ignored."""
2653 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002654 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002655 return SendUpstream(parser, args, 'dcommit')
2656
2657
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002658@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002659def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002660 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002661 if settings.GetIsGitSvn():
2662 print('This appears to be an SVN repository.')
2663 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002664 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002665 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002666
2667
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002668@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002669def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002670 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002671 parser.add_option('-b', dest='newbranch',
2672 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002673 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002674 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002675 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2676 help='Change to the directory DIR immediately, '
2677 'before doing anything else.')
2678 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002679 help='failed patches spew .rej files rather than '
2680 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002681 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2682 help="don't commit after patch applies")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002683 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002684 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002685 auth_config = auth.extract_auth_config_from_options(options)
2686
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002687 if len(args) != 1:
2688 parser.print_help()
2689 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002690 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002691
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002692 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002693 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002694 return 1
2695
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002696 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002697 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002698
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002699 if options.newbranch:
2700 if options.force:
2701 RunGit(['branch', '-D', options.newbranch],
2702 stderr=subprocess2.PIPE, error_ok=True)
2703 RunGit(['checkout', '-b', options.newbranch,
2704 Changelist().GetUpstreamBranch()])
2705
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002706 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002707 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002708
2709
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002710def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00002711 # PatchIssue should never be called with a dirty tree. It is up to the
2712 # caller to check this, but just in case we assert here since the
2713 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002714 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002715
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002716 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002717 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002718 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002719 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002720 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002721 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002722 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002723 # Assume it's a URL to the patch. Default to https.
2724 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00002725 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002726 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002727 DieWithError('Must pass an issue ID or full URL for '
2728 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00002729 issue = int(match.group(2))
2730 cl = Changelist(issue=issue, auth_config=auth_config)
2731 cl.rietveld_server = match.group(1)
2732 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002733 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002734
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002735 # Switch up to the top-level directory, if necessary, in preparation for
2736 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002737 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002738 if top:
2739 os.chdir(top)
2740
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002741 # Git patches have a/ at the beginning of source paths. We strip that out
2742 # with a sed script rather than the -p flag to patch so we can feed either
2743 # Git or svn-style patches into the same apply command.
2744 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002745 try:
2746 patch_data = subprocess2.check_output(
2747 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2748 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002749 DieWithError('Git patch mungling failed.')
2750 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002751
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002752 # We use "git apply" to apply the patch instead of "patch" so that we can
2753 # pick up file adds.
2754 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002755 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002756 if directory:
2757 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002758 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002759 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002760 elif IsGitVersionAtLeast('1.7.12'):
2761 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002762 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002763 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002764 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002765 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00002766 print 'Failed to apply the patch'
2767 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002768
2769 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002770 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00002771 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
2772 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00002773 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2774 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002775 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002776 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002777 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002778 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002779 else:
2780 print "Patch applied to index."
2781 return 0
2782
2783
2784def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002785 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002786 # Provide a wrapper for git svn rebase to help avoid accidental
2787 # git svn dcommit.
2788 # It's the only command that doesn't use parser at all since we just defer
2789 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002790
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002791 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002792
2793
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002794def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002795 """Fetches the tree status and returns either 'open', 'closed',
2796 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002797 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002798 if url:
2799 status = urllib2.urlopen(url).read().lower()
2800 if status.find('closed') != -1 or status == '0':
2801 return 'closed'
2802 elif status.find('open') != -1 or status == '1':
2803 return 'open'
2804 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002805 return 'unset'
2806
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002807
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002808def GetTreeStatusReason():
2809 """Fetches the tree status from a json url and returns the message
2810 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002811 url = settings.GetTreeStatusUrl()
2812 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002813 connection = urllib2.urlopen(json_url)
2814 status = json.loads(connection.read())
2815 connection.close()
2816 return status['message']
2817
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002818
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002819def GetBuilderMaster(bot_list):
2820 """For a given builder, fetch the master from AE if available."""
2821 map_url = 'https://builders-map.appspot.com/'
2822 try:
2823 master_map = json.load(urllib2.urlopen(map_url))
2824 except urllib2.URLError as e:
2825 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2826 (map_url, e))
2827 except ValueError as e:
2828 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2829 if not master_map:
2830 return None, 'Failed to build master map.'
2831
2832 result_master = ''
2833 for bot in bot_list:
2834 builder = bot.split(':', 1)[0]
2835 master_list = master_map.get(builder, [])
2836 if not master_list:
2837 return None, ('No matching master for builder %s.' % builder)
2838 elif len(master_list) > 1:
2839 return None, ('The builder name %s exists in multiple masters %s.' %
2840 (builder, master_list))
2841 else:
2842 cur_master = master_list[0]
2843 if not result_master:
2844 result_master = cur_master
2845 elif result_master != cur_master:
2846 return None, 'The builders do not belong to the same master.'
2847 return result_master, None
2848
2849
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002850def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002851 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002852 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002853 status = GetTreeStatus()
2854 if 'unset' == status:
2855 print 'You must configure your tree status URL by running "git cl config".'
2856 return 2
2857
2858 print "The tree is %s" % status
2859 print
2860 print GetTreeStatusReason()
2861 if status != 'open':
2862 return 1
2863 return 0
2864
2865
maruel@chromium.org15192402012-09-06 12:38:29 +00002866def CMDtry(parser, args):
2867 """Triggers a try job through Rietveld."""
2868 group = optparse.OptionGroup(parser, "Try job options")
2869 group.add_option(
2870 "-b", "--bot", action="append",
2871 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2872 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002873 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002874 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002875 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00002876 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002877 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002878 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002879 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002880 "-r", "--revision",
2881 help="Revision to use for the try job; default: the "
2882 "revision will be determined by the try server; see "
2883 "its waterfall for more info")
2884 group.add_option(
2885 "-c", "--clobber", action="store_true", default=False,
2886 help="Force a clobber before building; e.g. don't do an "
2887 "incremental build")
2888 group.add_option(
2889 "--project",
2890 help="Override which project to use. Projects are defined "
2891 "server-side to define what default bot set to use")
2892 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002893 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00002894 group.add_option(
2895 "--use-buildbucket", action="store_true", default=False,
2896 help="Use buildbucket to trigger try jobs.")
maruel@chromium.org15192402012-09-06 12:38:29 +00002897 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002898 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00002899 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002900 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00002901
2902 if args:
2903 parser.error('Unknown arguments: %s' % args)
2904
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002905 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00002906 if not cl.GetIssue():
2907 parser.error('Need to upload first')
2908
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002909 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002910 if props.get('closed'):
2911 parser.error('Cannot send tryjobs for a closed CL')
2912
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002913 if props.get('private'):
2914 parser.error('Cannot use trybots with private issue')
2915
maruel@chromium.org15192402012-09-06 12:38:29 +00002916 if not options.name:
2917 options.name = cl.GetBranch()
2918
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002919 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002920 options.master, err_msg = GetBuilderMaster(options.bot)
2921 if err_msg:
2922 parser.error('Tryserver master cannot be found because: %s\n'
2923 'Please manually specify the tryserver master'
2924 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002925
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002926 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002927 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002928 if not options.bot:
2929 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002930
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002931 # Get try masters from PRESUBMIT.py files.
2932 masters = presubmit_support.DoGetTryMasters(
2933 change,
2934 change.LocalPaths(),
2935 settings.GetRoot(),
2936 None,
2937 None,
2938 options.verbose,
2939 sys.stdout)
2940 if masters:
2941 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002942
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002943 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2944 options.bot = presubmit_support.DoGetTrySlaves(
2945 change,
2946 change.LocalPaths(),
2947 settings.GetRoot(),
2948 None,
2949 None,
2950 options.verbose,
2951 sys.stdout)
2952 if not options.bot:
2953 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002954
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002955 builders_and_tests = {}
2956 # TODO(machenbach): The old style command-line options don't support
2957 # multiple try masters yet.
2958 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2959 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2960
2961 for bot in old_style:
2962 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002963 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002964 elif ',' in bot:
2965 parser.error('Specify one bot per --bot flag')
2966 else:
2967 builders_and_tests.setdefault(bot, []).append('defaulttests')
2968
2969 for bot, tests in new_style:
2970 builders_and_tests.setdefault(bot, []).extend(tests)
2971
2972 # Return a master map with one master to be backwards compatible. The
2973 # master name defaults to an empty string, which will cause the master
2974 # not to be set on rietveld (deprecated).
2975 return {options.master: builders_and_tests}
2976
2977 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002978
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002979 for builders in masters.itervalues():
2980 if any('triggered' in b for b in builders):
2981 print >> sys.stderr, (
2982 'ERROR You are trying to send a job to a triggered bot. This type of'
2983 ' bot requires an\ninitial job from a parent (usually a builder). '
2984 'Instead send your job to the parent.\n'
2985 'Bot list: %s' % builders)
2986 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002987
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002988 patchset = cl.GetMostRecentPatchset()
2989 if patchset and patchset != cl.GetPatchset():
2990 print(
2991 '\nWARNING Mismatch between local config and server. Did a previous '
2992 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2993 'Continuing using\npatchset %s.\n' % patchset)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00002994 if options.use_buildbucket:
2995 try:
2996 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
2997 except BuildbucketResponseException as ex:
2998 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002999 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003000 except Exception as e:
3001 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3002 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3003 e, stacktrace)
3004 return 1
3005 else:
3006 try:
3007 cl.RpcServer().trigger_distributed_try_jobs(
3008 cl.GetIssue(), patchset, options.name, options.clobber,
3009 options.revision, masters)
3010 except urllib2.HTTPError as e:
3011 if e.code == 404:
3012 print('404 from rietveld; '
3013 'did you mean to use "git try" instead of "git cl try"?')
3014 return 1
3015 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003016
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003017 for (master, builders) in sorted(masters.iteritems()):
3018 if master:
3019 print 'Master: %s' % master
3020 length = max(len(builder) for builder in builders)
3021 for builder in sorted(builders):
3022 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003023 return 0
3024
3025
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003026@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003027def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003028 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003029 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003030 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003031 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003032
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003033 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003034 if args:
3035 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003036 branch = cl.GetBranch()
3037 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003038 cl = Changelist()
3039 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003040
3041 # Clear configured merge-base, if there is one.
3042 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003043 else:
3044 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003045 return 0
3046
3047
thestig@chromium.org00858c82013-12-02 23:08:03 +00003048def CMDweb(parser, args):
3049 """Opens the current CL in the web browser."""
3050 _, args = parser.parse_args(args)
3051 if args:
3052 parser.error('Unrecognized args: %s' % ' '.join(args))
3053
3054 issue_url = Changelist().GetIssueURL()
3055 if not issue_url:
3056 print >> sys.stderr, 'ERROR No issue to open'
3057 return 1
3058
3059 webbrowser.open(issue_url)
3060 return 0
3061
3062
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003063def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003064 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003065 auth.add_auth_options(parser)
3066 options, args = parser.parse_args(args)
3067 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003068 if args:
3069 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003070 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003071 props = cl.GetIssueProperties()
3072 if props.get('private'):
3073 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003074 cl.SetFlag('commit', '1')
3075 return 0
3076
3077
groby@chromium.org411034a2013-02-26 15:12:01 +00003078def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003079 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003080 auth.add_auth_options(parser)
3081 options, args = parser.parse_args(args)
3082 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003083 if args:
3084 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003085 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003086 # Ensure there actually is an issue to close.
3087 cl.GetDescription()
3088 cl.CloseIssue()
3089 return 0
3090
3091
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003092def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003093 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003094 auth.add_auth_options(parser)
3095 options, args = parser.parse_args(args)
3096 auth_config = auth.extract_auth_config_from_options(options)
3097 if args:
3098 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003099
3100 # Uncommitted (staged and unstaged) changes will be destroyed by
3101 # "git reset --hard" if there are merging conflicts in PatchIssue().
3102 # Staged changes would be committed along with the patch from last
3103 # upload, hence counted toward the "last upload" side in the final
3104 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003105 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003106 return 1
3107
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003108 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003109 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003110 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003111 if not issue:
3112 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003113 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003114 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003115
3116 # Create a new branch based on the merge-base
3117 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3118 try:
3119 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003120 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003121 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003122 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003123 return rtn
3124
wychen@chromium.org06928532015-02-03 02:11:29 +00003125 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003126 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003127 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003128 finally:
3129 RunGit(['checkout', '-q', branch])
3130 RunGit(['branch', '-D', TMP_BRANCH])
3131
3132 return 0
3133
3134
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003135def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003136 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003137 parser.add_option(
3138 '--no-color',
3139 action='store_true',
3140 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003141 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003142 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003143 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003144
3145 author = RunGit(['config', 'user.email']).strip() or None
3146
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003147 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003148
3149 if args:
3150 if len(args) > 1:
3151 parser.error('Unknown args')
3152 base_branch = args[0]
3153 else:
3154 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003155 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003156
3157 change = cl.GetChange(base_branch, None)
3158 return owners_finder.OwnersFinder(
3159 [f.LocalPath() for f in
3160 cl.GetChange(base_branch, None).AffectedFiles()],
3161 change.RepositoryRoot(), author,
3162 fopen=file, os_path=os.path, glob=glob.glob,
3163 disable_color=options.no_color).run()
3164
3165
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003166def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
3167 """Generates a diff command."""
3168 # Generate diff for the current branch's changes.
3169 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3170 upstream_commit, '--' ]
3171
3172 if args:
3173 for arg in args:
3174 if os.path.isdir(arg):
3175 diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
3176 elif os.path.isfile(arg):
3177 diff_cmd.append(arg)
3178 else:
3179 DieWithError('Argument "%s" is not a file or a directory' % arg)
3180 else:
3181 diff_cmd.extend('*' + ext for ext in extensions)
3182
3183 return diff_cmd
3184
3185
enne@chromium.org555cfe42014-01-29 18:21:39 +00003186@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003187def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003188 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003189 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003190 parser.add_option('--full', action='store_true',
3191 help='Reformat the full content of all touched files')
3192 parser.add_option('--dry-run', action='store_true',
3193 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003194 parser.add_option('--python', action='store_true',
3195 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003196 parser.add_option('--diff', action='store_true',
3197 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003198 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003199
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003200 # git diff generates paths against the root of the repository. Change
3201 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003202 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003203 if rel_base_path:
3204 os.chdir(rel_base_path)
3205
digit@chromium.org29e47272013-05-17 17:01:46 +00003206 # Grab the merge-base commit, i.e. the upstream commit of the current
3207 # branch when it was created or the last time it was rebased. This is
3208 # to cover the case where the user may have called "git fetch origin",
3209 # moving the origin branch to a newer commit, but hasn't rebased yet.
3210 upstream_commit = None
3211 cl = Changelist()
3212 upstream_branch = cl.GetUpstreamBranch()
3213 if upstream_branch:
3214 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3215 upstream_commit = upstream_commit.strip()
3216
3217 if not upstream_commit:
3218 DieWithError('Could not find base commit for this branch. '
3219 'Are you in detached state?')
3220
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003221 if opts.full:
3222 # Only list the names of modified files.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003223 diff_type = '--name-only'
enne@chromium.org555cfe42014-01-29 18:21:39 +00003224 else:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003225 # Only generate context-less patches.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003226 diff_type = '-U0'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003227
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003228 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, CLANG_EXTS)
digit@chromium.org29e47272013-05-17 17:01:46 +00003229 diff_output = RunGit(diff_cmd)
3230
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003231 top_dir = os.path.normpath(
3232 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3233
3234 # Locate the clang-format binary in the checkout
3235 try:
3236 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3237 except clang_format.NotFoundError, e:
3238 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003239
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003240 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3241 # formatted. This is used to block during the presubmit.
3242 return_value = 0
3243
digit@chromium.org29e47272013-05-17 17:01:46 +00003244 if opts.full:
3245 # diff_output is a list of files to send to clang-format.
3246 files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003247 if files:
3248 cmd = [clang_format_tool]
3249 if not opts.dry_run and not opts.diff:
3250 cmd.append('-i')
3251 stdout = RunCommand(cmd + files, cwd=top_dir)
3252 if opts.diff:
3253 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003254 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003255 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003256 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00003257 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003258 try:
3259 script = clang_format.FindClangFormatScriptInChromiumTree(
3260 'clang-format-diff.py')
3261 except clang_format.NotFoundError, e:
3262 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003263
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003264 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003265 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003266 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003267
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003268 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003269 if opts.diff:
3270 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003271 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003272 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003273
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003274 # Similar code to above, but using yapf on .py files rather than clang-format
3275 # on C/C++ files
3276 if opts.python:
3277 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, ['.py'])
3278 diff_output = RunGit(diff_cmd)
3279 yapf_tool = gclient_utils.FindExecutable('yapf')
3280 if yapf_tool is None:
3281 DieWithError('yapf not found in PATH')
3282
3283 if opts.full:
3284 files = diff_output.splitlines()
3285 if files:
3286 cmd = [yapf_tool]
3287 if not opts.dry_run and not opts.diff:
3288 cmd.append('-i')
3289 stdout = RunCommand(cmd + files, cwd=top_dir)
3290 if opts.diff:
3291 sys.stdout.write(stdout)
3292 else:
3293 # TODO(sbc): yapf --lines mode still has some issues.
3294 # https://github.com/google/yapf/issues/154
3295 DieWithError('--python currently only works with --full')
3296
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003297 # Build a diff command that only operates on dart files. dart's formatter
3298 # does not have the nice property of only operating on modified chunks, so
3299 # hard code full.
3300 dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3301 args, ['.dart'])
3302 dart_diff_output = RunGit(dart_diff_cmd)
3303 if dart_diff_output:
3304 try:
3305 command = [dart_format.FindDartFmtToolInChromiumTree()]
3306 if not opts.dry_run and not opts.diff:
3307 command.append('-w')
3308 command.extend(dart_diff_output.splitlines())
3309
3310 stdout = RunCommand(command, cwd=top_dir, env=env)
3311 if opts.dry_run and stdout:
3312 return_value = 2
3313 except dart_format.NotFoundError as e:
3314 print ('Unable to check dart code formatting. Dart SDK is not in ' +
3315 'this checkout.')
3316
3317 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003318
3319
maruel@chromium.org29404b52014-09-08 22:58:00 +00003320def CMDlol(parser, args):
3321 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003322 print zlib.decompress(base64.b64decode(
3323 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3324 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3325 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3326 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003327 return 0
3328
3329
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003330class OptionParser(optparse.OptionParser):
3331 """Creates the option parse and add --verbose support."""
3332 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003333 optparse.OptionParser.__init__(
3334 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003335 self.add_option(
3336 '-v', '--verbose', action='count', default=0,
3337 help='Use 2 times for more debugging info')
3338
3339 def parse_args(self, args=None, values=None):
3340 options, args = optparse.OptionParser.parse_args(self, args, values)
3341 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3342 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3343 return options, args
3344
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003345
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003346def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003347 if sys.hexversion < 0x02060000:
3348 print >> sys.stderr, (
3349 '\nYour python version %s is unsupported, please upgrade.\n' %
3350 sys.version.split(' ', 1)[0])
3351 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003352
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003353 # Reload settings.
3354 global settings
3355 settings = Settings()
3356
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003357 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003358 dispatcher = subcommand.CommandDispatcher(__name__)
3359 try:
3360 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003361 except auth.AuthenticationError as e:
3362 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003363 except urllib2.HTTPError, e:
3364 if e.code != 500:
3365 raise
3366 DieWithError(
3367 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3368 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003369 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003370
3371
3372if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003373 # These affect sys.stdout so do it outside of main() to simplify mocks in
3374 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003375 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003376 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003377 try:
3378 sys.exit(main(sys.argv[1:]))
3379 except KeyboardInterrupt:
3380 sys.stderr.write('interrupted\n')
3381 sys.exit(1)