blob: 7c3f530b7c2fe346614e69ba8ed34500d645cda6 [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
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +000047from git_footers import get_footer_svn_id
piman@chromium.org336f9122014-09-04 02:16:55 +000048import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000049import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000050import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000051import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000052import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000053import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000054import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000055import watchlists
56
maruel@chromium.org0633fb42013-08-16 20:06:14 +000057__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000059DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000060POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000061DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000062GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000063CHANGE_ID = 'Change-Id:'
rmistry@google.comc68112d2015-03-03 12:48:06 +000064REFS_THAT_ALIAS_TO_OTHER_REFS = {
65 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
66 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
67}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068
sheyang@google.com6ebaf782015-05-12 19:17:54 +000069# Buildbucket-related constants
70BUILDBUCKET_HOST = 'cr-buildbucket.appspot.com'
71
thestig@chromium.org44202a22014-03-11 19:22:18 +000072# Valid extensions for files we want to lint.
73DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
74DEFAULT_LINT_IGNORE_REGEX = r"$^"
75
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000076# Shortcut since it quickly becomes redundant.
77Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000078
maruel@chromium.orgddd59412011-11-30 14:20:38 +000079# Initialized in main()
80settings = None
81
82
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000083def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000084 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000085 sys.exit(1)
86
87
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000088def GetNoGitPagerEnv():
89 env = os.environ.copy()
90 # 'cat' is a magical git string that disables pagers on all platforms.
91 env['GIT_PAGER'] = 'cat'
92 return env
93
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000094
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000095def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000096 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000097 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000098 except subprocess2.CalledProcessError as e:
99 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000100 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000101 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000102 'Command "%s" failed.\n%s' % (
103 ' '.join(args), error_message or e.stdout or ''))
104 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000105
106
107def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000108 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000109 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000110
111
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000112def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000113 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000114 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115 if suppress_stderr:
116 stderr = subprocess2.VOID
117 else:
118 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000119 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000120 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000121 stdout=subprocess2.PIPE,
122 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000123 return code, out[0]
124 except ValueError:
125 # When the subprocess fails, it returns None. That triggers a ValueError
126 # when trying to unpack the return value into (out, code).
127 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000128
129
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000130def RunGitSilent(args):
131 """Returns stdout, suppresses stderr and ingores the return code."""
132 return RunGitWithCode(args, suppress_stderr=True)[1]
133
134
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000135def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000136 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000137 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000138 return (version.startswith(prefix) and
139 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140
141
maruel@chromium.org90541732011-04-01 17:54:18 +0000142def ask_for_data(prompt):
143 try:
144 return raw_input(prompt)
145 except KeyboardInterrupt:
146 # Hide the exception.
147 sys.exit(1)
148
149
iannucci@chromium.org79540052012-10-19 23:15:26 +0000150def git_set_branch_value(key, value):
151 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000152 if not branch:
153 return
154
155 cmd = ['config']
156 if isinstance(value, int):
157 cmd.append('--int')
158 git_key = 'branch.%s.%s' % (branch, key)
159 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000160
161
162def git_get_branch_default(key, default):
163 branch = Changelist().GetBranch()
164 if branch:
165 git_key = 'branch.%s.%s' % (branch, key)
166 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
167 try:
168 return int(stdout.strip())
169 except ValueError:
170 pass
171 return default
172
173
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000174def add_git_similarity(parser):
175 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000176 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000177 help='Sets the percentage that a pair of files need to match in order to'
178 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000179 parser.add_option(
180 '--find-copies', action='store_true',
181 help='Allows git to look for copies.')
182 parser.add_option(
183 '--no-find-copies', action='store_false', dest='find_copies',
184 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000185
186 old_parser_args = parser.parse_args
187 def Parse(args):
188 options, args = old_parser_args(args)
189
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000190 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000191 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000192 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000193 print('Note: Saving similarity of %d%% in git config.'
194 % options.similarity)
195 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000196
iannucci@chromium.org79540052012-10-19 23:15:26 +0000197 options.similarity = max(0, min(options.similarity, 100))
198
199 if options.find_copies is None:
200 options.find_copies = bool(
201 git_get_branch_default('git-find-copies', True))
202 else:
203 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000204
205 print('Using %d%% similarity for rename/copy detection. '
206 'Override with --similarity.' % options.similarity)
207
208 return options, args
209 parser.parse_args = Parse
210
211
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000212def _prefix_master(master):
213 """Convert user-specified master name to full master name.
214
215 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
216 name, while the developers always use shortened master name
217 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
218 function does the conversion for buildbucket migration.
219 """
220 prefix = 'master.'
221 if master.startswith(prefix):
222 return master
223 return '%s%s' % (prefix, master)
224
225
machenbach@chromium.org79e43ff2015-05-15 05:56:13 +0000226def trigger_try_jobs(auth_config, changelist, options, masters, category,
227 override_properties=None):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000228 rietveld_url = settings.GetDefaultServerUrl()
229 rietveld_host = urlparse.urlparse(rietveld_url).hostname
230 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
231 http = authenticator.authorize(httplib2.Http())
232 http.force_exception_to_status_code = True
233 issue_props = changelist.GetIssueProperties()
234 issue = changelist.GetIssue()
235 patchset = changelist.GetMostRecentPatchset()
236
237 buildbucket_put_url = (
238 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
239 hostname=BUILDBUCKET_HOST))
240 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
241 hostname=rietveld_host,
242 issue=issue,
243 patch=patchset)
244
245 batch_req_body = {'builds': []}
246 print_text = []
247 print_text.append('Tried jobs on:')
248 for master, builders_and_tests in sorted(masters.iteritems()):
249 print_text.append('Master: %s' % master)
250 bucket = _prefix_master(master)
251 for builder, tests in sorted(builders_and_tests.iteritems()):
252 print_text.append(' %s: %s' % (builder, tests))
253 parameters = {
254 'builder_name': builder,
255 'changes': [
256 {'author': {'email': issue_props['owner_email']}},
257 ],
258 'properties': {
259 'category': category,
260 'issue': issue,
261 'master': master,
262 'patch_project': issue_props['project'],
263 'patch_storage': 'rietveld',
264 'patchset': patchset,
265 'reason': options.name,
266 'revision': options.revision,
267 'rietveld': rietveld_url,
268 'testfilter': tests,
269 },
270 }
machenbach@chromium.org79e43ff2015-05-15 05:56:13 +0000271 if override_properties:
272 parameters['properties'].update(override_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000273 if options.clobber:
274 parameters['properties']['clobber'] = True
275 batch_req_body['builds'].append(
276 {
277 'bucket': bucket,
278 'parameters_json': json.dumps(parameters),
279 'tags': ['builder:%s' % builder,
280 'buildset:%s' % buildset,
281 'master:%s' % master,
282 'user_agent:git_cl_try']
283 }
284 )
285
286 for try_count in xrange(3):
287 response, content = http.request(
288 buildbucket_put_url,
289 'PUT',
290 body=json.dumps(batch_req_body),
291 headers={'Content-Type': 'application/json'},
292 )
293 content_json = None
294 try:
295 content_json = json.loads(content)
296 except ValueError:
297 pass
298
299 # Buildbucket could return an error even if status==200.
300 if content_json and content_json.get('error'):
301 msg = 'Error in response. Code: %d. Reason: %s. Message: %s.' % (
302 content_json['error'].get('code', ''),
303 content_json['error'].get('reason', ''),
304 content_json['error'].get('message', ''))
305 raise BuildbucketResponseException(msg)
306
307 if response.status == 200:
308 if not content_json:
309 raise BuildbucketResponseException(
310 'Buildbucket returns invalid json content: %s.\n'
311 'Please file bugs at crbug.com, label "Infra-BuildBucket".' %
312 content)
313 break
314 if response.status < 500 or try_count >= 2:
315 raise httplib2.HttpLib2Error(content)
316
317 # status >= 500 means transient failures.
318 logging.debug('Transient errors when triggering tryjobs. Will retry.')
319 time.sleep(0.5 + 1.5*try_count)
320
321 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000322
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000323
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000324def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
325 """Return the corresponding git ref if |base_url| together with |glob_spec|
326 matches the full |url|.
327
328 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
329 """
330 fetch_suburl, as_ref = glob_spec.split(':')
331 if allow_wildcards:
332 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
333 if glob_match:
334 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
335 # "branches/{472,597,648}/src:refs/remotes/svn/*".
336 branch_re = re.escape(base_url)
337 if glob_match.group(1):
338 branch_re += '/' + re.escape(glob_match.group(1))
339 wildcard = glob_match.group(2)
340 if wildcard == '*':
341 branch_re += '([^/]*)'
342 else:
343 # Escape and replace surrounding braces with parentheses and commas
344 # with pipe symbols.
345 wildcard = re.escape(wildcard)
346 wildcard = re.sub('^\\\\{', '(', wildcard)
347 wildcard = re.sub('\\\\,', '|', wildcard)
348 wildcard = re.sub('\\\\}$', ')', wildcard)
349 branch_re += wildcard
350 if glob_match.group(3):
351 branch_re += re.escape(glob_match.group(3))
352 match = re.match(branch_re, url)
353 if match:
354 return re.sub('\*$', match.group(1), as_ref)
355
356 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
357 if fetch_suburl:
358 full_url = base_url + '/' + fetch_suburl
359 else:
360 full_url = base_url
361 if full_url == url:
362 return as_ref
363 return None
364
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000365
iannucci@chromium.org79540052012-10-19 23:15:26 +0000366def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000367 """Prints statistics about the change to the user."""
368 # --no-ext-diff is broken in some versions of Git, so try to work around
369 # this by overriding the environment (but there is still a problem if the
370 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000371 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000372 if 'GIT_EXTERNAL_DIFF' in env:
373 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000374
375 if find_copies:
376 similarity_options = ['--find-copies-harder', '-l100000',
377 '-C%s' % similarity]
378 else:
379 similarity_options = ['-M%s' % similarity]
380
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000381 try:
382 stdout = sys.stdout.fileno()
383 except AttributeError:
384 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000385 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000386 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000387 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000388 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000389
390
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000391class BuildbucketResponseException(Exception):
392 pass
393
394
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000395class Settings(object):
396 def __init__(self):
397 self.default_server = None
398 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000399 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000400 self.is_git_svn = None
401 self.svn_branch = None
402 self.tree_status_url = None
403 self.viewvc_url = None
404 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000405 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000406 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000407 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000408 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000409 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000410
411 def LazyUpdateIfNeeded(self):
412 """Updates the settings from a codereview.settings file, if available."""
413 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000414 # The only value that actually changes the behavior is
415 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000416 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000417 error_ok=True
418 ).strip().lower()
419
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000420 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000421 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000422 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000423 # set updated to True to avoid infinite calling loop
424 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000425 self.updated = True
426 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000427 self.updated = True
428
429 def GetDefaultServerUrl(self, error_ok=False):
430 if not self.default_server:
431 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000432 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000433 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000434 if error_ok:
435 return self.default_server
436 if not self.default_server:
437 error_message = ('Could not find settings file. You must configure '
438 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000439 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000440 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000441 return self.default_server
442
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000443 @staticmethod
444 def GetRelativeRoot():
445 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000446
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000447 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000448 if self.root is None:
449 self.root = os.path.abspath(self.GetRelativeRoot())
450 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000451
452 def GetIsGitSvn(self):
453 """Return true if this repo looks like it's using git-svn."""
454 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000455 if self.GetPendingRefPrefix():
456 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
457 self.is_git_svn = False
458 else:
459 # If you have any "svn-remote.*" config keys, we think you're using svn.
460 self.is_git_svn = RunGitWithCode(
461 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000462 return self.is_git_svn
463
464 def GetSVNBranch(self):
465 if self.svn_branch is None:
466 if not self.GetIsGitSvn():
467 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
468
469 # Try to figure out which remote branch we're based on.
470 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000471 # 1) iterate through our branch history and find the svn URL.
472 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000473
474 # regexp matching the git-svn line that contains the URL.
475 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
476
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000477 # We don't want to go through all of history, so read a line from the
478 # pipe at a time.
479 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000480 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000481 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
482 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000483 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000484 for line in proc.stdout:
485 match = git_svn_re.match(line)
486 if match:
487 url = match.group(1)
488 proc.stdout.close() # Cut pipe.
489 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000490
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000491 if url:
492 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
493 remotes = RunGit(['config', '--get-regexp',
494 r'^svn-remote\..*\.url']).splitlines()
495 for remote in remotes:
496 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000497 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000498 remote = match.group(1)
499 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000500 rewrite_root = RunGit(
501 ['config', 'svn-remote.%s.rewriteRoot' % remote],
502 error_ok=True).strip()
503 if rewrite_root:
504 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000505 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000506 ['config', 'svn-remote.%s.fetch' % remote],
507 error_ok=True).strip()
508 if fetch_spec:
509 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
510 if self.svn_branch:
511 break
512 branch_spec = RunGit(
513 ['config', 'svn-remote.%s.branches' % remote],
514 error_ok=True).strip()
515 if branch_spec:
516 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
517 if self.svn_branch:
518 break
519 tag_spec = RunGit(
520 ['config', 'svn-remote.%s.tags' % remote],
521 error_ok=True).strip()
522 if tag_spec:
523 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
524 if self.svn_branch:
525 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000526
527 if not self.svn_branch:
528 DieWithError('Can\'t guess svn branch -- try specifying it on the '
529 'command line')
530
531 return self.svn_branch
532
533 def GetTreeStatusUrl(self, error_ok=False):
534 if not self.tree_status_url:
535 error_message = ('You must configure your tree status URL by running '
536 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000537 self.tree_status_url = self._GetRietveldConfig(
538 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000539 return self.tree_status_url
540
541 def GetViewVCUrl(self):
542 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000543 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000544 return self.viewvc_url
545
rmistry@google.com90752582014-01-14 21:04:50 +0000546 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000547 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000548
rmistry@google.com5626a922015-02-26 14:03:30 +0000549 def GetRunPostUploadHook(self):
550 run_post_upload_hook = self._GetRietveldConfig(
551 'run-post-upload-hook', error_ok=True)
552 return run_post_upload_hook == "True"
553
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000554 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000555 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000556
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000557 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000558 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000559
ukai@chromium.orge8077812012-02-03 03:41:46 +0000560 def GetIsGerrit(self):
561 """Return true if this repo is assosiated with gerrit code review system."""
562 if self.is_gerrit is None:
563 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
564 return self.is_gerrit
565
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000566 def GetGitEditor(self):
567 """Return the editor specified in the git config, or None if none is."""
568 if self.git_editor is None:
569 self.git_editor = self._GetConfig('core.editor', error_ok=True)
570 return self.git_editor or None
571
thestig@chromium.org44202a22014-03-11 19:22:18 +0000572 def GetLintRegex(self):
573 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
574 DEFAULT_LINT_REGEX)
575
576 def GetLintIgnoreRegex(self):
577 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
578 DEFAULT_LINT_IGNORE_REGEX)
579
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000580 def GetProject(self):
581 if not self.project:
582 self.project = self._GetRietveldConfig('project', error_ok=True)
583 return self.project
584
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000585 def GetForceHttpsCommitUrl(self):
586 if not self.force_https_commit_url:
587 self.force_https_commit_url = self._GetRietveldConfig(
588 'force-https-commit-url', error_ok=True)
589 return self.force_https_commit_url
590
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000591 def GetPendingRefPrefix(self):
592 if not self.pending_ref_prefix:
593 self.pending_ref_prefix = self._GetRietveldConfig(
594 'pending-ref-prefix', error_ok=True)
595 return self.pending_ref_prefix
596
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000597 def _GetRietveldConfig(self, param, **kwargs):
598 return self._GetConfig('rietveld.' + param, **kwargs)
599
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000600 def _GetConfig(self, param, **kwargs):
601 self.LazyUpdateIfNeeded()
602 return RunGit(['config', param], **kwargs).strip()
603
604
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000605def ShortBranchName(branch):
606 """Convert a name like 'refs/heads/foo' to just 'foo'."""
607 return branch.replace('refs/heads/', '')
608
609
610class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000611 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000612 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000613 global settings
614 if not settings:
615 # Happens when git_cl.py is used as a utility library.
616 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000617 settings.GetDefaultServerUrl()
618 self.branchref = branchref
619 if self.branchref:
620 self.branch = ShortBranchName(self.branchref)
621 else:
622 self.branch = None
623 self.rietveld_server = None
624 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000625 self.lookedup_issue = False
626 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000627 self.has_description = False
628 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000629 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000630 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000631 self.cc = None
632 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000633 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000634 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000635 self._remote = None
636 self._rpc_server = None
637
638 @property
639 def auth_config(self):
640 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000641
642 def GetCCList(self):
643 """Return the users cc'd on this CL.
644
645 Return is a string suitable for passing to gcl with the --cc flag.
646 """
647 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000648 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000649 more_cc = ','.join(self.watchers)
650 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
651 return self.cc
652
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000653 def GetCCListWithoutDefault(self):
654 """Return the users cc'd on this CL excluding default ones."""
655 if self.cc is None:
656 self.cc = ','.join(self.watchers)
657 return self.cc
658
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000659 def SetWatchers(self, watchers):
660 """Set the list of email addresses that should be cc'd based on the changed
661 files in this CL.
662 """
663 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000664
665 def GetBranch(self):
666 """Returns the short branch name, e.g. 'master'."""
667 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000668 branchref = RunGit(['symbolic-ref', 'HEAD'],
669 stderr=subprocess2.VOID, error_ok=True).strip()
670 if not branchref:
671 return None
672 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000673 self.branch = ShortBranchName(self.branchref)
674 return self.branch
675
676 def GetBranchRef(self):
677 """Returns the full branch name, e.g. 'refs/heads/master'."""
678 self.GetBranch() # Poke the lazy loader.
679 return self.branchref
680
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000681 @staticmethod
682 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000683 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000684 e.g. 'origin', 'refs/heads/master'
685 """
686 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000687 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
688 error_ok=True).strip()
689 if upstream_branch:
690 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
691 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000692 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
693 error_ok=True).strip()
694 if upstream_branch:
695 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000696 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000697 # Fall back on trying a git-svn upstream branch.
698 if settings.GetIsGitSvn():
699 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000700 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000701 # Else, try to guess the origin remote.
702 remote_branches = RunGit(['branch', '-r']).split()
703 if 'origin/master' in remote_branches:
704 # Fall back on origin/master if it exits.
705 remote = 'origin'
706 upstream_branch = 'refs/heads/master'
707 elif 'origin/trunk' in remote_branches:
708 # Fall back on origin/trunk if it exists. Generally a shared
709 # git-svn clone
710 remote = 'origin'
711 upstream_branch = 'refs/heads/trunk'
712 else:
713 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000714Either pass complete "git diff"-style arguments, like
715 git cl upload origin/master
716or verify this branch is set up to track another (via the --track argument to
717"git checkout -b ...").""")
718
719 return remote, upstream_branch
720
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000721 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000722 return git_common.get_or_create_merge_base(self.GetBranch(),
723 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000724
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725 def GetUpstreamBranch(self):
726 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000727 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000728 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000729 upstream_branch = upstream_branch.replace('refs/heads/',
730 'refs/remotes/%s/' % remote)
731 upstream_branch = upstream_branch.replace('refs/branch-heads/',
732 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000733 self.upstream_branch = upstream_branch
734 return self.upstream_branch
735
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000736 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000737 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000738 remote, branch = None, self.GetBranch()
739 seen_branches = set()
740 while branch not in seen_branches:
741 seen_branches.add(branch)
742 remote, branch = self.FetchUpstreamTuple(branch)
743 branch = ShortBranchName(branch)
744 if remote != '.' or branch.startswith('refs/remotes'):
745 break
746 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000747 remotes = RunGit(['remote'], error_ok=True).split()
748 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000749 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000750 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000751 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000752 logging.warning('Could not determine which remote this change is '
753 'associated with, so defaulting to "%s". This may '
754 'not be what you want. You may prevent this message '
755 'by running "git svn info" as documented here: %s',
756 self._remote,
757 GIT_INSTRUCTIONS_URL)
758 else:
759 logging.warn('Could not determine which remote this change is '
760 'associated with. You may prevent this message by '
761 'running "git svn info" as documented here: %s',
762 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000763 branch = 'HEAD'
764 if branch.startswith('refs/remotes'):
765 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000766 elif branch.startswith('refs/branch-heads/'):
767 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000768 else:
769 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000770 return self._remote
771
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000772 def GitSanityChecks(self, upstream_git_obj):
773 """Checks git repo status and ensures diff is from local commits."""
774
sbc@chromium.org79706062015-01-14 21:18:12 +0000775 if upstream_git_obj is None:
776 if self.GetBranch() is None:
777 print >> sys.stderr, (
778 'ERROR: unable to dertermine current branch (detached HEAD?)')
779 else:
780 print >> sys.stderr, (
781 'ERROR: no upstream branch')
782 return False
783
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000784 # Verify the commit we're diffing against is in our current branch.
785 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
786 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
787 if upstream_sha != common_ancestor:
788 print >> sys.stderr, (
789 'ERROR: %s is not in the current branch. You may need to rebase '
790 'your tracking branch' % upstream_sha)
791 return False
792
793 # List the commits inside the diff, and verify they are all local.
794 commits_in_diff = RunGit(
795 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
796 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
797 remote_branch = remote_branch.strip()
798 if code != 0:
799 _, remote_branch = self.GetRemoteBranch()
800
801 commits_in_remote = RunGit(
802 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
803
804 common_commits = set(commits_in_diff) & set(commits_in_remote)
805 if common_commits:
806 print >> sys.stderr, (
807 'ERROR: Your diff contains %d commits already in %s.\n'
808 'Run "git log --oneline %s..HEAD" to get a list of commits in '
809 'the diff. If you are using a custom git flow, you can override'
810 ' the reference used for this check with "git config '
811 'gitcl.remotebranch <git-ref>".' % (
812 len(common_commits), remote_branch, upstream_git_obj))
813 return False
814 return True
815
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000816 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000817 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000818
819 Returns None if it is not set.
820 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000821 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
822 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000823
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000824 def GetGitSvnRemoteUrl(self):
825 """Return the configured git-svn remote URL parsed from git svn info.
826
827 Returns None if it is not set.
828 """
829 # URL is dependent on the current directory.
830 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
831 if data:
832 keys = dict(line.split(': ', 1) for line in data.splitlines()
833 if ': ' in line)
834 return keys.get('URL', None)
835 return None
836
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000837 def GetRemoteUrl(self):
838 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
839
840 Returns None if there is no remote.
841 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000842 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000843 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
844
845 # If URL is pointing to a local directory, it is probably a git cache.
846 if os.path.isdir(url):
847 url = RunGit(['config', 'remote.%s.url' % remote],
848 error_ok=True,
849 cwd=url).strip()
850 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000851
852 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000853 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000854 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000855 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000856 self.issue = int(issue) or None if issue else None
857 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000858 return self.issue
859
860 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000861 if not self.rietveld_server:
862 # If we're on a branch then get the server potentially associated
863 # with that branch.
864 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000865 rietveld_server_config = self._RietveldServer()
866 if rietveld_server_config:
867 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
868 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000869 if not self.rietveld_server:
870 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000871 return self.rietveld_server
872
873 def GetIssueURL(self):
874 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000875 if not self.GetIssue():
876 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000877 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
878
879 def GetDescription(self, pretty=False):
880 if not self.has_description:
881 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000882 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000883 try:
884 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000885 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000886 if e.code == 404:
887 DieWithError(
888 ('\nWhile fetching the description for issue %d, received a '
889 '404 (not found)\n'
890 'error. It is likely that you deleted this '
891 'issue on the server. If this is the\n'
892 'case, please run\n\n'
893 ' git cl issue 0\n\n'
894 'to clear the association with the deleted issue. Then run '
895 'this command again.') % issue)
896 else:
897 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000898 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000899 except urllib2.URLError as e:
900 print >> sys.stderr, (
901 'Warning: Failed to retrieve CL description due to network '
902 'failure.')
903 self.description = ''
904
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000905 self.has_description = True
906 if pretty:
907 wrapper = textwrap.TextWrapper()
908 wrapper.initial_indent = wrapper.subsequent_indent = ' '
909 return wrapper.fill(self.description)
910 return self.description
911
912 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000913 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000914 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000915 patchset = RunGit(['config', self._PatchsetSetting()],
916 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000917 self.patchset = int(patchset) or None if patchset else None
918 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000919 return self.patchset
920
921 def SetPatchset(self, patchset):
922 """Set this branch's patchset. If patchset=0, clears the patchset."""
923 if patchset:
924 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000925 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926 else:
927 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000928 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000929 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000930
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000931 def GetMostRecentPatchset(self):
932 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000933
934 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000935 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000936 '/download/issue%s_%s.diff' % (issue, patchset))
937
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000938 def GetIssueProperties(self):
939 if self._props is None:
940 issue = self.GetIssue()
941 if not issue:
942 self._props = {}
943 else:
944 self._props = self.RpcServer().get_issue_properties(issue, True)
945 return self._props
946
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000947 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000948 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000949
apavlov@chromium.orge4efd512014-11-05 09:05:29 +0000950 def AddComment(self, message):
951 return self.RpcServer().add_comment(self.GetIssue(), message)
952
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000953 def SetIssue(self, issue):
954 """Set this branch's issue. If issue=0, clears the issue."""
955 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000956 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957 RunGit(['config', self._IssueSetting(), str(issue)])
958 if self.rietveld_server:
959 RunGit(['config', self._RietveldServer(), self.rietveld_server])
960 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000961 current_issue = self.GetIssue()
962 if current_issue:
963 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000964 self.issue = None
965 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000966
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000967 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000968 if not self.GitSanityChecks(upstream_branch):
969 DieWithError('\nGit sanity check failure')
970
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000971 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000972 if not root:
973 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000974 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000975
976 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000977 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000978 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000979 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000980 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000981 except subprocess2.CalledProcessError:
982 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000983 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000984 'This branch probably doesn\'t exist anymore. To reset the\n'
985 'tracking branch, please run\n'
986 ' git branch --set-upstream %s trunk\n'
987 'replacing trunk with origin/master or the relevant branch') %
988 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000989
maruel@chromium.org52424302012-08-29 15:14:30 +0000990 issue = self.GetIssue()
991 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000992 if issue:
993 description = self.GetDescription()
994 else:
995 # If the change was never uploaded, use the log messages of all commits
996 # up to the branch point, as git cl upload will prefill the description
997 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000998 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
999 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001000
1001 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001002 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001003 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001004 name,
1005 description,
1006 absroot,
1007 files,
1008 issue,
1009 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001010 author,
1011 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001012
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001013 def GetStatus(self):
1014 """Apply a rough heuristic to give a simple summary of an issue's review
1015 or CQ status, assuming adherence to a common workflow.
1016
1017 Returns None if no issue for this branch, or one of the following keywords:
1018 * 'error' - error from review tool (including deleted issues)
1019 * 'unsent' - not sent for review
1020 * 'waiting' - waiting for review
1021 * 'reply' - waiting for owner to reply to review
1022 * 'lgtm' - LGTM from at least one approved reviewer
1023 * 'commit' - in the commit queue
1024 * 'closed' - closed
1025 """
1026 if not self.GetIssue():
1027 return None
1028
1029 try:
1030 props = self.GetIssueProperties()
1031 except urllib2.HTTPError:
1032 return 'error'
1033
1034 if props.get('closed'):
1035 # Issue is closed.
1036 return 'closed'
1037 if props.get('commit'):
1038 # Issue is in the commit queue.
1039 return 'commit'
1040
1041 try:
1042 reviewers = self.GetApprovingReviewers()
1043 except urllib2.HTTPError:
1044 return 'error'
1045
1046 if reviewers:
1047 # Was LGTM'ed.
1048 return 'lgtm'
1049
1050 messages = props.get('messages') or []
1051
1052 if not messages:
1053 # No message was sent.
1054 return 'unsent'
1055 if messages[-1]['sender'] != props.get('owner_email'):
1056 # Non-LGTM reply from non-owner
1057 return 'reply'
1058 return 'waiting'
1059
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001060 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001061 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001062
1063 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001064 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001065 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001066 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001067 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001068 except presubmit_support.PresubmitFailure, e:
1069 DieWithError(
1070 ('%s\nMaybe your depot_tools is out of date?\n'
1071 'If all fails, contact maruel@') % e)
1072
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001073 def UpdateDescription(self, description):
1074 self.description = description
1075 return self.RpcServer().update_description(
1076 self.GetIssue(), self.description)
1077
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001078 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001079 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001080 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001082 def SetFlag(self, flag, value):
1083 """Patchset must match."""
1084 if not self.GetPatchset():
1085 DieWithError('The patchset needs to match. Send another patchset.')
1086 try:
1087 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001088 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001089 except urllib2.HTTPError, e:
1090 if e.code == 404:
1091 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1092 if e.code == 403:
1093 DieWithError(
1094 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1095 'match?') % (self.GetIssue(), self.GetPatchset()))
1096 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001097
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001098 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099 """Returns an upload.RpcServer() to access this review's rietveld instance.
1100 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001101 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001102 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001103 self.GetRietveldServer(),
1104 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001105 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106
1107 def _IssueSetting(self):
1108 """Return the git setting that stores this change's issue."""
1109 return 'branch.%s.rietveldissue' % self.GetBranch()
1110
1111 def _PatchsetSetting(self):
1112 """Return the git setting that stores this change's most recent patchset."""
1113 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1114
1115 def _RietveldServer(self):
1116 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001117 branch = self.GetBranch()
1118 if branch:
1119 return 'branch.%s.rietveldserver' % branch
1120 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001121
1122
1123def GetCodereviewSettingsInteractively():
1124 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001125 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001126 server = settings.GetDefaultServerUrl(error_ok=True)
1127 prompt = 'Rietveld server (host[:port])'
1128 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001129 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001130 if not server and not newserver:
1131 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001132 if newserver:
1133 newserver = gclient_utils.UpgradeToHttps(newserver)
1134 if newserver != server:
1135 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001136
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001137 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 prompt = caption
1139 if initial:
1140 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001141 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142 if new_val == 'x':
1143 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001144 elif new_val:
1145 if is_url:
1146 new_val = gclient_utils.UpgradeToHttps(new_val)
1147 if new_val != initial:
1148 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001150 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001151 SetProperty(settings.GetDefaultPrivateFlag(),
1152 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001154 'tree-status-url', False)
1155 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001156 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001157 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1158 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001159
1160 # TODO: configure a default branch to diff against, rather than this
1161 # svn-based hackery.
1162
1163
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001164class ChangeDescription(object):
1165 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001166 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001167 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001168
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001169 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001170 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001171
agable@chromium.org42c20792013-09-12 17:34:49 +00001172 @property # www.logilab.org/ticket/89786
1173 def description(self): # pylint: disable=E0202
1174 return '\n'.join(self._description_lines)
1175
1176 def set_description(self, desc):
1177 if isinstance(desc, basestring):
1178 lines = desc.splitlines()
1179 else:
1180 lines = [line.rstrip() for line in desc]
1181 while lines and not lines[0]:
1182 lines.pop(0)
1183 while lines and not lines[-1]:
1184 lines.pop(-1)
1185 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001186
piman@chromium.org336f9122014-09-04 02:16:55 +00001187 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001188 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001189 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001190 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001191 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001192 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001193
agable@chromium.org42c20792013-09-12 17:34:49 +00001194 # Get the set of R= and TBR= lines and remove them from the desciption.
1195 regexp = re.compile(self.R_LINE)
1196 matches = [regexp.match(line) for line in self._description_lines]
1197 new_desc = [l for i, l in enumerate(self._description_lines)
1198 if not matches[i]]
1199 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001200
agable@chromium.org42c20792013-09-12 17:34:49 +00001201 # Construct new unified R= and TBR= lines.
1202 r_names = []
1203 tbr_names = []
1204 for match in matches:
1205 if not match:
1206 continue
1207 people = cleanup_list([match.group(2).strip()])
1208 if match.group(1) == 'TBR':
1209 tbr_names.extend(people)
1210 else:
1211 r_names.extend(people)
1212 for name in r_names:
1213 if name not in reviewers:
1214 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001215 if add_owners_tbr:
1216 owners_db = owners.Database(change.RepositoryRoot(),
1217 fopen=file, os_path=os.path, glob=glob.glob)
1218 all_reviewers = set(tbr_names + reviewers)
1219 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1220 all_reviewers)
1221 tbr_names.extend(owners_db.reviewers_for(missing_files,
1222 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001223 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1224 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1225
1226 # Put the new lines in the description where the old first R= line was.
1227 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1228 if 0 <= line_loc < len(self._description_lines):
1229 if new_tbr_line:
1230 self._description_lines.insert(line_loc, new_tbr_line)
1231 if new_r_line:
1232 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001233 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001234 if new_r_line:
1235 self.append_footer(new_r_line)
1236 if new_tbr_line:
1237 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001238
1239 def prompt(self):
1240 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001241 self.set_description([
1242 '# Enter a description of the change.',
1243 '# This will be displayed on the codereview site.',
1244 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001245 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001246 '--------------------',
1247 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001248
agable@chromium.org42c20792013-09-12 17:34:49 +00001249 regexp = re.compile(self.BUG_LINE)
1250 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001251 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001252 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001253 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001254 if not content:
1255 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001256 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001257
1258 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001259 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1260 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001261 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001262 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001263
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001264 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001265 if self._description_lines:
1266 # Add an empty line if either the last line or the new line isn't a tag.
1267 last_line = self._description_lines[-1]
1268 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1269 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1270 self._description_lines.append('')
1271 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001272
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001273 def get_reviewers(self):
1274 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001275 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1276 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001277 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001278
1279
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001280def get_approving_reviewers(props):
1281 """Retrieves the reviewers that approved a CL from the issue properties with
1282 messages.
1283
1284 Note that the list may contain reviewers that are not committer, thus are not
1285 considered by the CQ.
1286 """
1287 return sorted(
1288 set(
1289 message['sender']
1290 for message in props['messages']
1291 if message['approval'] and message['sender'] in props['reviewers']
1292 )
1293 )
1294
1295
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001296def FindCodereviewSettingsFile(filename='codereview.settings'):
1297 """Finds the given file starting in the cwd and going up.
1298
1299 Only looks up to the top of the repository unless an
1300 'inherit-review-settings-ok' file exists in the root of the repository.
1301 """
1302 inherit_ok_file = 'inherit-review-settings-ok'
1303 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001304 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001305 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1306 root = '/'
1307 while True:
1308 if filename in os.listdir(cwd):
1309 if os.path.isfile(os.path.join(cwd, filename)):
1310 return open(os.path.join(cwd, filename))
1311 if cwd == root:
1312 break
1313 cwd = os.path.dirname(cwd)
1314
1315
1316def LoadCodereviewSettingsFromFile(fileobj):
1317 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001318 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001319
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001320 def SetProperty(name, setting, unset_error_ok=False):
1321 fullname = 'rietveld.' + name
1322 if setting in keyvals:
1323 RunGit(['config', fullname, keyvals[setting]])
1324 else:
1325 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1326
1327 SetProperty('server', 'CODE_REVIEW_SERVER')
1328 # Only server setting is required. Other settings can be absent.
1329 # In that case, we ignore errors raised during option deletion attempt.
1330 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001331 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001332 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1333 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001334 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001335 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001336 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1337 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001338 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001339 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001340 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001341 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1342 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001343
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001344 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001345 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001346
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001347 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1348 #should be of the form
1349 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1350 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1351 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1352 keyvals['ORIGIN_URL_CONFIG']])
1353
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001354
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001355def urlretrieve(source, destination):
1356 """urllib is broken for SSL connections via a proxy therefore we
1357 can't use urllib.urlretrieve()."""
1358 with open(destination, 'w') as f:
1359 f.write(urllib2.urlopen(source).read())
1360
1361
ukai@chromium.org712d6102013-11-27 00:52:58 +00001362def hasSheBang(fname):
1363 """Checks fname is a #! script."""
1364 with open(fname) as f:
1365 return f.read(2).startswith('#!')
1366
1367
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001368def DownloadHooks(force):
1369 """downloads hooks
1370
1371 Args:
1372 force: True to update hooks. False to install hooks if not present.
1373 """
1374 if not settings.GetIsGerrit():
1375 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001376 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001377 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1378 if not os.access(dst, os.X_OK):
1379 if os.path.exists(dst):
1380 if not force:
1381 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001382 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001383 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001384 if not hasSheBang(dst):
1385 DieWithError('Not a script: %s\n'
1386 'You need to download from\n%s\n'
1387 'into .git/hooks/commit-msg and '
1388 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001389 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1390 except Exception:
1391 if os.path.exists(dst):
1392 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001393 DieWithError('\nFailed to download hooks.\n'
1394 'You need to download from\n%s\n'
1395 'into .git/hooks/commit-msg and '
1396 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001397
1398
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001399@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001400def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001401 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001403 parser.add_option('--activate-update', action='store_true',
1404 help='activate auto-updating [rietveld] section in '
1405 '.git/config')
1406 parser.add_option('--deactivate-update', action='store_true',
1407 help='deactivate auto-updating [rietveld] section in '
1408 '.git/config')
1409 options, args = parser.parse_args(args)
1410
1411 if options.deactivate_update:
1412 RunGit(['config', 'rietveld.autoupdate', 'false'])
1413 return
1414
1415 if options.activate_update:
1416 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1417 return
1418
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419 if len(args) == 0:
1420 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001421 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422 return 0
1423
1424 url = args[0]
1425 if not url.endswith('codereview.settings'):
1426 url = os.path.join(url, 'codereview.settings')
1427
1428 # Load code review settings and download hooks (if available).
1429 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001430 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431 return 0
1432
1433
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001434def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001435 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001436 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1437 branch = ShortBranchName(branchref)
1438 _, args = parser.parse_args(args)
1439 if not args:
1440 print("Current base-url:")
1441 return RunGit(['config', 'branch.%s.base-url' % branch],
1442 error_ok=False).strip()
1443 else:
1444 print("Setting base-url to %s" % args[0])
1445 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1446 error_ok=False).strip()
1447
1448
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001449def color_for_status(status):
1450 """Maps a Changelist status to color, for CMDstatus and other tools."""
1451 return {
1452 'unsent': Fore.RED,
1453 'waiting': Fore.BLUE,
1454 'reply': Fore.YELLOW,
1455 'lgtm': Fore.GREEN,
1456 'commit': Fore.MAGENTA,
1457 'closed': Fore.CYAN,
1458 'error': Fore.WHITE,
1459 }.get(status, Fore.WHITE)
1460
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001461def fetch_cl_status(branch, auth_config=None):
1462 """Fetches information for an issue and returns (branch, issue, status)."""
1463 cl = Changelist(branchref=branch, auth_config=auth_config)
1464 url = cl.GetIssueURL()
1465 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001466
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001467 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001468 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001469 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001470
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001471 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001472
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:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001504 cl = Changelist(branchref=b, auth_config=auth_config)
1505 url = cl.GetIssueURL()
1506 yield (b, url, 'waiting' if url else 'error')
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:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001573 b, i, status = output.next()
1574 branch_statuses[b] = (i, status)
1575 issue_url, status = branch_statuses.pop(branch)
1576 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001577 reset = Fore.RESET
1578 if not sys.stdout.isatty():
1579 color = ''
1580 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001581 status_str = '(%s)' % status if status else ''
1582 print ' %*s : %s%s %s%s' % (
1583 alignment, ShortBranchName(branch), color, issue_url, status_str,
1584 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001585
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001586 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001587 print
1588 print 'Current branch:',
1589 if not cl.GetIssue():
1590 print 'no issue assigned.'
1591 return 0
1592 print cl.GetBranch()
1593 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001594 if not options.fast:
1595 print 'Issue description:'
1596 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001597 return 0
1598
1599
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001600def colorize_CMDstatus_doc():
1601 """To be called once in main() to add colors to git cl status help."""
1602 colors = [i for i in dir(Fore) if i[0].isupper()]
1603
1604 def colorize_line(line):
1605 for color in colors:
1606 if color in line.upper():
1607 # Extract whitespaces first and the leading '-'.
1608 indent = len(line) - len(line.lstrip(' ')) + 1
1609 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1610 return line
1611
1612 lines = CMDstatus.__doc__.splitlines()
1613 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1614
1615
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001616@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001617def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001618 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001619
1620 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001621 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001622 parser.add_option('-r', '--reverse', action='store_true',
1623 help='Lookup the branch(es) for the specified issues. If '
1624 'no issues are specified, all branches with mapped '
1625 'issues will be listed.')
1626 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001627
dnj@chromium.org406c4402015-03-03 17:22:28 +00001628 if options.reverse:
1629 branches = RunGit(['for-each-ref', 'refs/heads',
1630 '--format=%(refname:short)']).splitlines()
1631
1632 # Reverse issue lookup.
1633 issue_branch_map = {}
1634 for branch in branches:
1635 cl = Changelist(branchref=branch)
1636 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1637 if not args:
1638 args = sorted(issue_branch_map.iterkeys())
1639 for issue in args:
1640 if not issue:
1641 continue
1642 print 'Branch for issue number %s: %s' % (
1643 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1644 else:
1645 cl = Changelist()
1646 if len(args) > 0:
1647 try:
1648 issue = int(args[0])
1649 except ValueError:
1650 DieWithError('Pass a number to set the issue or none to list it.\n'
1651 'Maybe you want to run git cl status?')
1652 cl.SetIssue(issue)
1653 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001654 return 0
1655
1656
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001657def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001658 """Shows or posts review comments for any changelist."""
1659 parser.add_option('-a', '--add-comment', dest='comment',
1660 help='comment to add to an issue')
1661 parser.add_option('-i', dest='issue',
1662 help="review issue id (defaults to current issue)")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001663 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001664 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001665 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001666
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001667 issue = None
1668 if options.issue:
1669 try:
1670 issue = int(options.issue)
1671 except ValueError:
1672 DieWithError('A review issue id is expected to be a number')
1673
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001674 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001675
1676 if options.comment:
1677 cl.AddComment(options.comment)
1678 return 0
1679
1680 data = cl.GetIssueProperties()
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001681 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001682 if message['disapproval']:
1683 color = Fore.RED
1684 elif message['approval']:
1685 color = Fore.GREEN
1686 elif message['sender'] == data['owner_email']:
1687 color = Fore.MAGENTA
1688 else:
1689 color = Fore.BLUE
1690 print '\n%s%s %s%s' % (
1691 color, message['date'].split('.', 1)[0], message['sender'],
1692 Fore.RESET)
1693 if message['text'].strip():
1694 print '\n'.join(' ' + l for l in message['text'].splitlines())
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001695 return 0
1696
1697
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001698def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001699 """Brings up the editor for the current CL's description."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001700 auth.add_auth_options(parser)
1701 options, _ = parser.parse_args(args)
1702 auth_config = auth.extract_auth_config_from_options(options)
1703 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001704 if not cl.GetIssue():
1705 DieWithError('This branch has no associated changelist.')
1706 description = ChangeDescription(cl.GetDescription())
1707 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00001708 if cl.GetDescription() != description.description:
1709 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001710 return 0
1711
1712
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001713def CreateDescriptionFromLog(args):
1714 """Pulls out the commit log to use as a base for the CL description."""
1715 log_args = []
1716 if len(args) == 1 and not args[0].endswith('.'):
1717 log_args = [args[0] + '..']
1718 elif len(args) == 1 and args[0].endswith('...'):
1719 log_args = [args[0][:-1]]
1720 elif len(args) == 2:
1721 log_args = [args[0] + '..' + args[1]]
1722 else:
1723 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001724 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001725
1726
thestig@chromium.org44202a22014-03-11 19:22:18 +00001727def CMDlint(parser, args):
1728 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001729 parser.add_option('--filter', action='append', metavar='-x,+y',
1730 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001731 auth.add_auth_options(parser)
1732 options, args = parser.parse_args(args)
1733 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001734
1735 # Access to a protected member _XX of a client class
1736 # pylint: disable=W0212
1737 try:
1738 import cpplint
1739 import cpplint_chromium
1740 except ImportError:
1741 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1742 return 1
1743
1744 # Change the current working directory before calling lint so that it
1745 # shows the correct base.
1746 previous_cwd = os.getcwd()
1747 os.chdir(settings.GetRoot())
1748 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001749 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001750 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1751 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001752 if not files:
1753 print "Cannot lint an empty CL"
1754 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001755
1756 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001757 command = args + files
1758 if options.filter:
1759 command = ['--filter=' + ','.join(options.filter)] + command
1760 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001761
1762 white_regex = re.compile(settings.GetLintRegex())
1763 black_regex = re.compile(settings.GetLintIgnoreRegex())
1764 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1765 for filename in filenames:
1766 if white_regex.match(filename):
1767 if black_regex.match(filename):
1768 print "Ignoring file %s" % filename
1769 else:
1770 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1771 extra_check_functions)
1772 else:
1773 print "Skipping file %s" % filename
1774 finally:
1775 os.chdir(previous_cwd)
1776 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1777 if cpplint._cpplint_state.error_count != 0:
1778 return 1
1779 return 0
1780
1781
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001782def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001783 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001784 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001785 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001786 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001787 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001788 auth.add_auth_options(parser)
1789 options, args = parser.parse_args(args)
1790 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001791
sbc@chromium.org71437c02015-04-09 19:29:40 +00001792 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00001793 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001794 return 1
1795
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001796 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001797 if args:
1798 base_branch = args[0]
1799 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001800 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001801 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001802
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001803 cl.RunHook(
1804 committing=not options.upload,
1805 may_prompt=False,
1806 verbose=options.verbose,
1807 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001808 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001809
1810
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001811def AddChangeIdToCommitMessage(options, args):
1812 """Re-commits using the current message, assumes the commit hook is in
1813 place.
1814 """
1815 log_desc = options.message or CreateDescriptionFromLog(args)
1816 git_command = ['commit', '--amend', '-m', log_desc]
1817 RunGit(git_command)
1818 new_log_desc = CreateDescriptionFromLog(args)
1819 if CHANGE_ID in new_log_desc:
1820 print 'git-cl: Added Change-Id to commit message.'
1821 else:
1822 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1823
1824
piman@chromium.org336f9122014-09-04 02:16:55 +00001825def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001826 """upload the current branch to gerrit."""
1827 # We assume the remote called "origin" is the one we want.
1828 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001829 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00001830
1831 remote, remote_branch = cl.GetRemoteBranch()
1832 branch = GetTargetRef(remote, remote_branch, options.target_branch,
1833 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001834
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001835 change_desc = ChangeDescription(
1836 options.message or CreateDescriptionFromLog(args))
1837 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001838 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001839 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001840
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001841 if options.squash:
1842 # Try to get the message from a previous upload.
1843 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
1844 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
1845 if not message:
1846 if not options.force:
1847 change_desc.prompt()
1848
1849 if CHANGE_ID not in change_desc.description:
1850 # Run the commit-msg hook without modifying the head commit by writing
1851 # the commit message to a temporary file and running the hook over it,
1852 # then reading the file back in.
1853 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
1854 'commit-msg')
1855 file_handle, msg_file = tempfile.mkstemp(text=True,
1856 prefix='commit_msg')
1857 try:
1858 try:
1859 with os.fdopen(file_handle, 'w') as fileobj:
1860 fileobj.write(change_desc.description)
1861 finally:
1862 os.close(file_handle)
1863 RunCommand([commit_msg_hook, msg_file])
1864 change_desc.set_description(gclient_utils.FileRead(msg_file))
1865 finally:
1866 os.remove(msg_file)
1867
1868 if not change_desc.description:
1869 print "Description is empty; aborting."
1870 return 1
1871
1872 message = change_desc.description
1873
1874 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1875 if remote is '.':
1876 # If our upstream branch is local, we base our squashed commit on its
1877 # squashed version.
1878 parent = ('refs/heads/git_cl_uploads/' +
1879 scm.GIT.ShortBranchName(upstream_branch))
1880
1881 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
1882 # will create additional CLs when uploading.
1883 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
1884 RunGitSilent(['rev-parse', parent + ':'])):
1885 print 'Upload upstream branch ' + upstream_branch + ' first.'
1886 return 1
1887 else:
1888 parent = cl.GetCommonAncestorWithUpstream()
1889
1890 tree = RunGit(['rev-parse', 'HEAD:']).strip()
1891 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
1892 '-m', message]).strip()
1893 else:
1894 if CHANGE_ID not in change_desc.description:
1895 AddChangeIdToCommitMessage(options, args)
1896 ref_to_push = 'HEAD'
1897 parent = '%s/%s' % (gerrit_remote, branch)
1898
1899 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
1900 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001901 if len(commits) > 1:
1902 print('WARNING: This will upload %d commits. Run the following command '
1903 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001904 print('git log %s..%s' % (parent, ref_to_push))
1905 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001906 'commit.')
1907 ask_for_data('About to upload; enter to confirm.')
1908
piman@chromium.org336f9122014-09-04 02:16:55 +00001909 if options.reviewers or options.tbr_owners:
1910 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001911
ukai@chromium.orge8077812012-02-03 03:41:46 +00001912 receive_options = []
1913 cc = cl.GetCCList().split(',')
1914 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001915 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001916 cc = filter(None, cc)
1917 if cc:
1918 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001919 if change_desc.get_reviewers():
1920 receive_options.extend(
1921 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001922
ukai@chromium.orge8077812012-02-03 03:41:46 +00001923 git_command = ['push']
1924 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001925 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001926 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001927 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00001928 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001929
1930 if options.squash:
1931 head = RunGit(['rev-parse', 'HEAD']).strip()
1932 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
1933
ukai@chromium.orge8077812012-02-03 03:41:46 +00001934 # TODO(ukai): parse Change-Id: and set issue number?
1935 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001936
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001937
wittman@chromium.org455dc922015-01-26 20:15:50 +00001938def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
1939 """Computes the remote branch ref to use for the CL.
1940
1941 Args:
1942 remote (str): The git remote for the CL.
1943 remote_branch (str): The git remote branch for the CL.
1944 target_branch (str): The target branch specified by the user.
1945 pending_prefix (str): The pending prefix from the settings.
1946 """
1947 if not (remote and remote_branch):
1948 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001949
wittman@chromium.org455dc922015-01-26 20:15:50 +00001950 if target_branch:
1951 # Cannonicalize branch references to the equivalent local full symbolic
1952 # refs, which are then translated into the remote full symbolic refs
1953 # below.
1954 if '/' not in target_branch:
1955 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
1956 else:
1957 prefix_replacements = (
1958 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
1959 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
1960 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
1961 )
1962 match = None
1963 for regex, replacement in prefix_replacements:
1964 match = re.search(regex, target_branch)
1965 if match:
1966 remote_branch = target_branch.replace(match.group(0), replacement)
1967 break
1968 if not match:
1969 # This is a branch path but not one we recognize; use as-is.
1970 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00001971 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
1972 # Handle the refs that need to land in different refs.
1973 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001974
wittman@chromium.org455dc922015-01-26 20:15:50 +00001975 # Create the true path to the remote branch.
1976 # Does the following translation:
1977 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
1978 # * refs/remotes/origin/master -> refs/heads/master
1979 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
1980 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
1981 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
1982 elif remote_branch.startswith('refs/remotes/%s/' % remote):
1983 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
1984 'refs/heads/')
1985 elif remote_branch.startswith('refs/remotes/branch-heads'):
1986 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
1987 # If a pending prefix exists then replace refs/ with it.
1988 if pending_prefix:
1989 remote_branch = remote_branch.replace('refs/', pending_prefix)
1990 return remote_branch
1991
1992
piman@chromium.org336f9122014-09-04 02:16:55 +00001993def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001994 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001995 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1996 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001997 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001998 if options.emulate_svn_auto_props:
1999 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002000
2001 change_desc = None
2002
pgervais@chromium.org91141372014-01-09 23:27:20 +00002003 if options.email is not None:
2004 upload_args.extend(['--email', options.email])
2005
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002006 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002007 if options.title:
2008 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002009 if options.message:
2010 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002011 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002012 print ("This branch is associated with issue %s. "
2013 "Adding patch to that issue." % cl.GetIssue())
2014 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002015 if options.title:
2016 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002017 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002018 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002019 if options.reviewers or options.tbr_owners:
2020 change_desc.update_reviewers(options.reviewers,
2021 options.tbr_owners,
2022 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002023 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002024 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002025
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002026 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002027 print "Description is empty; aborting."
2028 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002029
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002030 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002031 if change_desc.get_reviewers():
2032 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002033 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002034 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002035 DieWithError("Must specify reviewers to send email.")
2036 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002037
2038 # We check this before applying rietveld.private assuming that in
2039 # rietveld.cc only addresses which we can send private CLs to are listed
2040 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2041 # --private is specified explicitly on the command line.
2042 if options.private:
2043 logging.warn('rietveld.cc is ignored since private flag is specified. '
2044 'You need to review and add them manually if necessary.')
2045 cc = cl.GetCCListWithoutDefault()
2046 else:
2047 cc = cl.GetCCList()
2048 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002049 if cc:
2050 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002051
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002052 if options.private or settings.GetDefaultPrivateFlag() == "True":
2053 upload_args.append('--private')
2054
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002055 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002056 if not options.find_copies:
2057 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002058
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002059 # Include the upstream repo's URL in the change -- this is useful for
2060 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002061 remote_url = cl.GetGitBaseUrlFromConfig()
2062 if not remote_url:
2063 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002064 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002065 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002066 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2067 remote_url = (cl.GetRemoteUrl() + '@'
2068 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002069 if remote_url:
2070 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002071 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002072 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2073 settings.GetPendingRefPrefix())
2074 if target_ref:
2075 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002076
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002077 project = settings.GetProject()
2078 if project:
2079 upload_args.extend(['--project', project])
2080
rmistry@google.comef966222015-04-07 11:15:01 +00002081 if options.cq_dry_run:
2082 upload_args.extend(['--cq_dry_run'])
2083
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002084 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002085 upload_args = ['upload'] + upload_args + args
2086 logging.info('upload.RealMain(%s)', upload_args)
2087 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002088 issue = int(issue)
2089 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002090 except KeyboardInterrupt:
2091 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002092 except:
2093 # If we got an exception after the user typed a description for their
2094 # change, back up the description before re-raising.
2095 if change_desc:
2096 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2097 print '\nGot exception while uploading -- saving description to %s\n' \
2098 % backup_path
2099 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002100 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002101 backup_file.close()
2102 raise
2103
2104 if not cl.GetIssue():
2105 cl.SetIssue(issue)
2106 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002107
2108 if options.use_commit_queue:
2109 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002110 return 0
2111
2112
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002113def cleanup_list(l):
2114 """Fixes a list so that comma separated items are put as individual items.
2115
2116 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2117 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2118 """
2119 items = sum((i.split(',') for i in l), [])
2120 stripped_items = (i.strip() for i in items)
2121 return sorted(filter(None, stripped_items))
2122
2123
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002124@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002125def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002126 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00002127 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2128 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002129 parser.add_option('--bypass-watchlists', action='store_true',
2130 dest='bypass_watchlists',
2131 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002132 parser.add_option('-f', action='store_true', dest='force',
2133 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002134 parser.add_option('-m', dest='message', help='message for patchset')
2135 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002136 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002137 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002138 help='reviewer email addresses')
2139 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002140 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002141 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002142 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002143 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002144 parser.add_option('--emulate_svn_auto_props',
2145 '--emulate-svn-auto-props',
2146 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002147 dest="emulate_svn_auto_props",
2148 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002149 parser.add_option('-c', '--use-commit-queue', action='store_true',
2150 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002151 parser.add_option('--private', action='store_true',
2152 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002153 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002154 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002155 metavar='TARGET',
2156 help='Apply CL to remote ref TARGET. ' +
2157 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002158 parser.add_option('--squash', action='store_true',
2159 help='Squash multiple commits into one (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002160 parser.add_option('--email', default=None,
2161 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002162 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2163 help='add a set of OWNERS to TBR')
rmistry@google.comef966222015-04-07 11:15:01 +00002164 parser.add_option('--cq-dry-run', dest='cq_dry_run', action='store_true',
2165 help='Send the patchset to do a CQ dry run right after '
2166 'upload.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002167
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002168 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002169 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002170 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002171 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002172
sbc@chromium.org71437c02015-04-09 19:29:40 +00002173 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002174 return 1
2175
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002176 options.reviewers = cleanup_list(options.reviewers)
2177 options.cc = cleanup_list(options.cc)
2178
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002179 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002180 if args:
2181 # TODO(ukai): is it ok for gerrit case?
2182 base_branch = args[0]
2183 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002184 if cl.GetBranch() is None:
2185 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2186
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002187 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002188 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002189 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002190
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002191 # Make sure authenticated to Rietveld before running expensive hooks. It is
2192 # a fast, best efforts check. Rietveld still can reject the authentication
2193 # during the actual upload.
2194 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2195 authenticator = auth.get_authenticator_for_host(
2196 cl.GetRietveldServer(), auth_config)
2197 if not authenticator.has_cached_credentials():
2198 raise auth.LoginRequiredError(cl.GetRietveldServer())
2199
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002200 # Apply watchlists on upload.
2201 change = cl.GetChange(base_branch, None)
2202 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2203 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002204 if not options.bypass_watchlists:
2205 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002206
ukai@chromium.orge8077812012-02-03 03:41:46 +00002207 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002208 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002209 # Set the reviewer list now so that presubmit checks can access it.
2210 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002211 change_description.update_reviewers(options.reviewers,
2212 options.tbr_owners,
2213 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002214 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002215 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002216 may_prompt=not options.force,
2217 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002218 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002219 if not hook_results.should_continue():
2220 return 1
2221 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002222 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002223
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002224 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002225 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002226 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002227 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002228 print ('The last upload made from this repository was patchset #%d but '
2229 'the most recent patchset on the server is #%d.'
2230 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002231 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2232 'from another machine or branch the patch you\'re uploading now '
2233 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002234 ask_for_data('About to upload; enter to confirm.')
2235
iannucci@chromium.org79540052012-10-19 23:15:26 +00002236 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002237 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00002238 return GerritUpload(options, args, cl, change)
2239 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002240 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002241 git_set_branch_value('last-upload-hash',
2242 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002243 # Run post upload hooks, if specified.
2244 if settings.GetRunPostUploadHook():
2245 presubmit_support.DoPostUploadExecuter(
2246 change,
2247 cl,
2248 settings.GetRoot(),
2249 options.verbose,
2250 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002251
2252 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002253
2254
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002255def IsSubmoduleMergeCommit(ref):
2256 # When submodules are added to the repo, we expect there to be a single
2257 # non-git-svn merge commit at remote HEAD with a signature comment.
2258 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002259 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002260 return RunGit(cmd) != ''
2261
2262
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002263def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002264 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002265
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002266 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002267 Updates changelog with metadata (e.g. pointer to review).
2268 Pushes/dcommits the code upstream.
2269 Updates review and closes.
2270 """
2271 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2272 help='bypass upload presubmit hook')
2273 parser.add_option('-m', dest='message',
2274 help="override review description")
2275 parser.add_option('-f', action='store_true', dest='force',
2276 help="force yes to questions (don't prompt)")
2277 parser.add_option('-c', dest='contributor',
2278 help="external contributor for patch (appended to " +
2279 "description and used as author for git). Should be " +
2280 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002281 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002282 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002283 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002284 auth_config = auth.extract_auth_config_from_options(options)
2285
2286 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002287
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002288 current = cl.GetBranch()
2289 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2290 if not settings.GetIsGitSvn() and remote == '.':
2291 print
2292 print 'Attempting to push branch %r into another local branch!' % current
2293 print
2294 print 'Either reparent this branch on top of origin/master:'
2295 print ' git reparent-branch --root'
2296 print
2297 print 'OR run `git rebase-update` if you think the parent branch is already'
2298 print 'committed.'
2299 print
2300 print ' Current parent: %r' % upstream_branch
2301 return 1
2302
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002303 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002304 # Default to merging against our best guess of the upstream branch.
2305 args = [cl.GetUpstreamBranch()]
2306
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002307 if options.contributor:
2308 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2309 print "Please provide contibutor as 'First Last <email@example.com>'"
2310 return 1
2311
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002312 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002313 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002314
sbc@chromium.org71437c02015-04-09 19:29:40 +00002315 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002316 return 1
2317
2318 # This rev-list syntax means "show all commits not in my branch that
2319 # are in base_branch".
2320 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2321 base_branch]).splitlines()
2322 if upstream_commits:
2323 print ('Base branch "%s" has %d commits '
2324 'not in this branch.' % (base_branch, len(upstream_commits)))
2325 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2326 return 1
2327
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002328 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002329 svn_head = None
2330 if cmd == 'dcommit' or base_has_submodules:
2331 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2332 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002333
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002334 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002335 # If the base_head is a submodule merge commit, the first parent of the
2336 # base_head should be a git-svn commit, which is what we're interested in.
2337 base_svn_head = base_branch
2338 if base_has_submodules:
2339 base_svn_head += '^1'
2340
2341 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002342 if extra_commits:
2343 print ('This branch has %d additional commits not upstreamed yet.'
2344 % len(extra_commits.splitlines()))
2345 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2346 'before attempting to %s.' % (base_branch, cmd))
2347 return 1
2348
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002349 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002350 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002351 author = None
2352 if options.contributor:
2353 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002354 hook_results = cl.RunHook(
2355 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002356 may_prompt=not options.force,
2357 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002358 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002359 if not hook_results.should_continue():
2360 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002361
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002362 # Check the tree status if the tree status URL is set.
2363 status = GetTreeStatus()
2364 if 'closed' == status:
2365 print('The tree is closed. Please wait for it to reopen. Use '
2366 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2367 return 1
2368 elif 'unknown' == status:
2369 print('Unable to determine tree status. Please verify manually and '
2370 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2371 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002372 else:
2373 breakpad.SendStack(
2374 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002375 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2376 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002377 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002378
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002379 change_desc = ChangeDescription(options.message)
2380 if not change_desc.description and cl.GetIssue():
2381 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002382
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002383 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002384 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002385 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002386 else:
2387 print 'No description set.'
2388 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2389 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002390
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002391 # Keep a separate copy for the commit message, because the commit message
2392 # contains the link to the Rietveld issue, while the Rietveld message contains
2393 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002394 # Keep a separate copy for the commit message.
2395 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002396 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002397
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002398 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002399 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002400 # Xcode won't linkify this URL unless there is a non-whitespace character
2401 # after it. Add a period on a new line to circumvent this.
2402 commit_desc.append_footer('Review URL: %s.' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002403 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002404 commit_desc.append_footer('Patch from %s.' % options.contributor)
2405
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002406 print('Description:')
2407 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002408
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002409 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002410 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002411 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002412
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002413 # We want to squash all this branch's commits into one commit with the proper
2414 # description. We do this by doing a "reset --soft" to the base branch (which
2415 # keeps the working copy the same), then dcommitting that. If origin/master
2416 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2417 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002418 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002419 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2420 # Delete the branches if they exist.
2421 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2422 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2423 result = RunGitWithCode(showref_cmd)
2424 if result[0] == 0:
2425 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002426
2427 # We might be in a directory that's present in this branch but not in the
2428 # trunk. Move up to the top of the tree so that git commands that expect a
2429 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002430 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002431 if rel_base_path:
2432 os.chdir(rel_base_path)
2433
2434 # Stuff our change into the merge branch.
2435 # We wrap in a try...finally block so if anything goes wrong,
2436 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002437 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002438 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002439 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002440 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002441 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002442 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002443 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002444 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002445 RunGit(
2446 [
2447 'commit', '--author', options.contributor,
2448 '-m', commit_desc.description,
2449 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002450 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002451 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002452 if base_has_submodules:
2453 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2454 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2455 RunGit(['checkout', CHERRY_PICK_BRANCH])
2456 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002457 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002458 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002459 pending_prefix = settings.GetPendingRefPrefix()
2460 if not pending_prefix or branch.startswith(pending_prefix):
2461 # If not using refs/pending/heads/* at all, or target ref is already set
2462 # to pending, then push to the target ref directly.
2463 retcode, output = RunGitWithCode(
2464 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002465 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002466 else:
2467 # Cherry-pick the change on top of pending ref and then push it.
2468 assert branch.startswith('refs/'), branch
2469 assert pending_prefix[-1] == '/', pending_prefix
2470 pending_ref = pending_prefix + branch[len('refs/'):]
2471 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002472 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002473 if retcode == 0:
2474 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002475 else:
2476 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002477 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002478 'svn', 'dcommit',
2479 '-C%s' % options.similarity,
2480 '--no-rebase', '--rmdir',
2481 ]
2482 if settings.GetForceHttpsCommitUrl():
2483 # Allow forcing https commit URLs for some projects that don't allow
2484 # committing to http URLs (like Google Code).
2485 remote_url = cl.GetGitSvnRemoteUrl()
2486 if urlparse.urlparse(remote_url).scheme == 'http':
2487 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002488 cmd_args.append('--commit-url=%s' % remote_url)
2489 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002490 if 'Committed r' in output:
2491 revision = re.match(
2492 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2493 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002494 finally:
2495 # And then swap back to the original branch and clean up.
2496 RunGit(['checkout', '-q', cl.GetBranch()])
2497 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002498 if base_has_submodules:
2499 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002500
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002501 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002502 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002503 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002504
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002505 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002506 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002507 try:
2508 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2509 # We set pushed_to_pending to False, since it made it all the way to the
2510 # real ref.
2511 pushed_to_pending = False
2512 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002513 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002514
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002515 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002516 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002517 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002518 if not to_pending:
2519 if viewvc_url and revision:
2520 change_desc.append_footer(
2521 'Committed: %s%s' % (viewvc_url, revision))
2522 elif revision:
2523 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002524 print ('Closing issue '
2525 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002526 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002527 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002528 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002529 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002530 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002531 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002532 if options.bypass_hooks:
2533 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2534 else:
2535 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002536 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002537 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002538
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002539 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002540 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2541 print 'The commit is in the pending queue (%s).' % pending_ref
2542 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002543 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002544 'footer.' % branch)
2545
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002546 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2547 if os.path.isfile(hook):
2548 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002549
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002550 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002551
2552
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002553def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2554 print
2555 print 'Waiting for commit to be landed on %s...' % real_ref
2556 print '(If you are impatient, you may Ctrl-C once without harm)'
2557 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2558 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2559
2560 loop = 0
2561 while True:
2562 sys.stdout.write('fetching (%d)... \r' % loop)
2563 sys.stdout.flush()
2564 loop += 1
2565
2566 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2567 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2568 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2569 for commit in commits.splitlines():
2570 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2571 print 'Found commit on %s' % real_ref
2572 return commit
2573
2574 current_rev = to_rev
2575
2576
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002577def PushToGitPending(remote, pending_ref, upstream_ref):
2578 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2579
2580 Returns:
2581 (retcode of last operation, output log of last operation).
2582 """
2583 assert pending_ref.startswith('refs/'), pending_ref
2584 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2585 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2586 code = 0
2587 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002588 max_attempts = 3
2589 attempts_left = max_attempts
2590 while attempts_left:
2591 if attempts_left != max_attempts:
2592 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2593 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002594
2595 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002596 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002597 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002598 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002599 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002600 print 'Fetch failed with exit code %d.' % code
2601 if out.strip():
2602 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002603 continue
2604
2605 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002606 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002607 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002608 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002609 if code:
2610 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002611 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2612 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002613 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2614 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002615 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002616 return code, out
2617
2618 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002619 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002620 code, out = RunGitWithCode(
2621 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2622 if code == 0:
2623 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002624 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002625 return code, out
2626
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002627 print 'Push failed with exit code %d.' % code
2628 if out.strip():
2629 print out.strip()
2630 if IsFatalPushFailure(out):
2631 print (
2632 'Fatal push error. Make sure your .netrc credentials and git '
2633 'user.email are correct and you have push access to the repo.')
2634 return code, out
2635
2636 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002637 return code, out
2638
2639
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002640def IsFatalPushFailure(push_stdout):
2641 """True if retrying push won't help."""
2642 return '(prohibited by Gerrit)' in push_stdout
2643
2644
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002645@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002646def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002647 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002648 if not settings.GetIsGitSvn():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002649 if get_footer_svn_id():
2650 # If it looks like previous commits were mirrored with git-svn.
2651 message = """This repository appears to be a git-svn mirror, but no
2652upstream SVN master is set. You probably need to run 'git auto-svn' once."""
2653 else:
2654 message = """This doesn't appear to be an SVN repository.
2655If your project has a true, writeable git repository, you probably want to run
2656'git cl land' instead.
2657If your project has a git mirror of an upstream SVN master, you probably need
2658to run 'git svn init'.
2659
2660Using the wrong command might cause your commit to appear to succeed, and the
2661review to be closed, without actually landing upstream. If you choose to
2662proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002663 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002664 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002665 return SendUpstream(parser, args, 'dcommit')
2666
2667
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002668@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002669def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002670 """Commits the current changelist via git."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002671 if settings.GetIsGitSvn() or get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002672 print('This appears to be an SVN repository.')
2673 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002674 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00002675 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002676 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002677
2678
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002679@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002680def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002681 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002682 parser.add_option('-b', dest='newbranch',
2683 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002684 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002685 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002686 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2687 help='Change to the directory DIR immediately, '
2688 'before doing anything else.')
2689 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002690 help='failed patches spew .rej files rather than '
2691 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002692 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2693 help="don't commit after patch applies")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002694 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002695 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002696 auth_config = auth.extract_auth_config_from_options(options)
2697
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002698 if len(args) != 1:
2699 parser.print_help()
2700 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002701 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002702
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002703 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002704 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002705 return 1
2706
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002707 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002708 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002709
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002710 if options.newbranch:
2711 if options.force:
2712 RunGit(['branch', '-D', options.newbranch],
2713 stderr=subprocess2.PIPE, error_ok=True)
2714 RunGit(['checkout', '-b', options.newbranch,
2715 Changelist().GetUpstreamBranch()])
2716
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002717 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002718 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002719
2720
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002721def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00002722 # PatchIssue should never be called with a dirty tree. It is up to the
2723 # caller to check this, but just in case we assert here since the
2724 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002725 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002726
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002727 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002728 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002729 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002730 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002731 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002732 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002733 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002734 # Assume it's a URL to the patch. Default to https.
2735 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00002736 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002737 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002738 DieWithError('Must pass an issue ID or full URL for '
2739 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00002740 issue = int(match.group(2))
2741 cl = Changelist(issue=issue, auth_config=auth_config)
2742 cl.rietveld_server = match.group(1)
2743 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002744 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002745
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002746 # Switch up to the top-level directory, if necessary, in preparation for
2747 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002748 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002749 if top:
2750 os.chdir(top)
2751
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002752 # Git patches have a/ at the beginning of source paths. We strip that out
2753 # with a sed script rather than the -p flag to patch so we can feed either
2754 # Git or svn-style patches into the same apply command.
2755 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002756 try:
2757 patch_data = subprocess2.check_output(
2758 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2759 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002760 DieWithError('Git patch mungling failed.')
2761 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002762
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002763 # We use "git apply" to apply the patch instead of "patch" so that we can
2764 # pick up file adds.
2765 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002766 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002767 if directory:
2768 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002769 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002770 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002771 elif IsGitVersionAtLeast('1.7.12'):
2772 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002773 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002774 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002775 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002776 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00002777 print 'Failed to apply the patch'
2778 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002779
2780 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002781 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00002782 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
2783 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00002784 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2785 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002786 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002787 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002788 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002789 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002790 else:
2791 print "Patch applied to index."
2792 return 0
2793
2794
2795def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002796 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002797 # Provide a wrapper for git svn rebase to help avoid accidental
2798 # git svn dcommit.
2799 # It's the only command that doesn't use parser at all since we just defer
2800 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002801
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002802 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002803
2804
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002805def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002806 """Fetches the tree status and returns either 'open', 'closed',
2807 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002808 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002809 if url:
2810 status = urllib2.urlopen(url).read().lower()
2811 if status.find('closed') != -1 or status == '0':
2812 return 'closed'
2813 elif status.find('open') != -1 or status == '1':
2814 return 'open'
2815 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002816 return 'unset'
2817
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002818
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002819def GetTreeStatusReason():
2820 """Fetches the tree status from a json url and returns the message
2821 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002822 url = settings.GetTreeStatusUrl()
2823 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002824 connection = urllib2.urlopen(json_url)
2825 status = json.loads(connection.read())
2826 connection.close()
2827 return status['message']
2828
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002829
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002830def GetBuilderMaster(bot_list):
2831 """For a given builder, fetch the master from AE if available."""
2832 map_url = 'https://builders-map.appspot.com/'
2833 try:
2834 master_map = json.load(urllib2.urlopen(map_url))
2835 except urllib2.URLError as e:
2836 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2837 (map_url, e))
2838 except ValueError as e:
2839 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2840 if not master_map:
2841 return None, 'Failed to build master map.'
2842
2843 result_master = ''
2844 for bot in bot_list:
2845 builder = bot.split(':', 1)[0]
2846 master_list = master_map.get(builder, [])
2847 if not master_list:
2848 return None, ('No matching master for builder %s.' % builder)
2849 elif len(master_list) > 1:
2850 return None, ('The builder name %s exists in multiple masters %s.' %
2851 (builder, master_list))
2852 else:
2853 cur_master = master_list[0]
2854 if not result_master:
2855 result_master = cur_master
2856 elif result_master != cur_master:
2857 return None, 'The builders do not belong to the same master.'
2858 return result_master, None
2859
2860
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002861def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002862 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002863 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002864 status = GetTreeStatus()
2865 if 'unset' == status:
2866 print 'You must configure your tree status URL by running "git cl config".'
2867 return 2
2868
2869 print "The tree is %s" % status
2870 print
2871 print GetTreeStatusReason()
2872 if status != 'open':
2873 return 1
2874 return 0
2875
2876
maruel@chromium.org15192402012-09-06 12:38:29 +00002877def CMDtry(parser, args):
2878 """Triggers a try job through Rietveld."""
2879 group = optparse.OptionGroup(parser, "Try job options")
2880 group.add_option(
2881 "-b", "--bot", action="append",
2882 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2883 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002884 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002885 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002886 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00002887 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002888 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002889 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002890 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002891 "-r", "--revision",
2892 help="Revision to use for the try job; default: the "
2893 "revision will be determined by the try server; see "
2894 "its waterfall for more info")
2895 group.add_option(
2896 "-c", "--clobber", action="store_true", default=False,
2897 help="Force a clobber before building; e.g. don't do an "
2898 "incremental build")
2899 group.add_option(
2900 "--project",
2901 help="Override which project to use. Projects are defined "
2902 "server-side to define what default bot set to use")
2903 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002904 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00002905 group.add_option(
2906 "--use-buildbucket", action="store_true", default=False,
2907 help="Use buildbucket to trigger try jobs.")
maruel@chromium.org15192402012-09-06 12:38:29 +00002908 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002909 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00002910 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002911 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00002912
2913 if args:
2914 parser.error('Unknown arguments: %s' % args)
2915
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002916 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00002917 if not cl.GetIssue():
2918 parser.error('Need to upload first')
2919
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002920 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002921 if props.get('closed'):
2922 parser.error('Cannot send tryjobs for a closed CL')
2923
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002924 if props.get('private'):
2925 parser.error('Cannot use trybots with private issue')
2926
maruel@chromium.org15192402012-09-06 12:38:29 +00002927 if not options.name:
2928 options.name = cl.GetBranch()
2929
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002930 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002931 options.master, err_msg = GetBuilderMaster(options.bot)
2932 if err_msg:
2933 parser.error('Tryserver master cannot be found because: %s\n'
2934 'Please manually specify the tryserver master'
2935 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002936
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002937 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002938 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002939 if not options.bot:
2940 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002941
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002942 # Get try masters from PRESUBMIT.py files.
2943 masters = presubmit_support.DoGetTryMasters(
2944 change,
2945 change.LocalPaths(),
2946 settings.GetRoot(),
2947 None,
2948 None,
2949 options.verbose,
2950 sys.stdout)
2951 if masters:
2952 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002953
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002954 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2955 options.bot = presubmit_support.DoGetTrySlaves(
2956 change,
2957 change.LocalPaths(),
2958 settings.GetRoot(),
2959 None,
2960 None,
2961 options.verbose,
2962 sys.stdout)
2963 if not options.bot:
2964 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002965
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002966 builders_and_tests = {}
2967 # TODO(machenbach): The old style command-line options don't support
2968 # multiple try masters yet.
2969 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2970 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2971
2972 for bot in old_style:
2973 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002974 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002975 elif ',' in bot:
2976 parser.error('Specify one bot per --bot flag')
2977 else:
2978 builders_and_tests.setdefault(bot, []).append('defaulttests')
2979
2980 for bot, tests in new_style:
2981 builders_and_tests.setdefault(bot, []).extend(tests)
2982
2983 # Return a master map with one master to be backwards compatible. The
2984 # master name defaults to an empty string, which will cause the master
2985 # not to be set on rietveld (deprecated).
2986 return {options.master: builders_and_tests}
2987
2988 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002989
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002990 for builders in masters.itervalues():
2991 if any('triggered' in b for b in builders):
2992 print >> sys.stderr, (
2993 'ERROR You are trying to send a job to a triggered bot. This type of'
2994 ' bot requires an\ninitial job from a parent (usually a builder). '
2995 'Instead send your job to the parent.\n'
2996 'Bot list: %s' % builders)
2997 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002998
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002999 patchset = cl.GetMostRecentPatchset()
3000 if patchset and patchset != cl.GetPatchset():
3001 print(
3002 '\nWARNING Mismatch between local config and server. Did a previous '
3003 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3004 'Continuing using\npatchset %s.\n' % patchset)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003005 if options.use_buildbucket:
3006 try:
3007 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3008 except BuildbucketResponseException as ex:
3009 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003010 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003011 except Exception as e:
3012 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3013 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3014 e, stacktrace)
3015 return 1
3016 else:
3017 try:
3018 cl.RpcServer().trigger_distributed_try_jobs(
3019 cl.GetIssue(), patchset, options.name, options.clobber,
3020 options.revision, masters)
3021 except urllib2.HTTPError as e:
3022 if e.code == 404:
3023 print('404 from rietveld; '
3024 'did you mean to use "git try" instead of "git cl try"?')
3025 return 1
3026 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003027
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003028 for (master, builders) in sorted(masters.iteritems()):
3029 if master:
3030 print 'Master: %s' % master
3031 length = max(len(builder) for builder in builders)
3032 for builder in sorted(builders):
3033 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003034 return 0
3035
3036
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003037@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003038def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003039 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003040 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003041 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003042 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003043
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003044 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003045 if args:
3046 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003047 branch = cl.GetBranch()
3048 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003049 cl = Changelist()
3050 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003051
3052 # Clear configured merge-base, if there is one.
3053 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003054 else:
3055 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003056 return 0
3057
3058
thestig@chromium.org00858c82013-12-02 23:08:03 +00003059def CMDweb(parser, args):
3060 """Opens the current CL in the web browser."""
3061 _, args = parser.parse_args(args)
3062 if args:
3063 parser.error('Unrecognized args: %s' % ' '.join(args))
3064
3065 issue_url = Changelist().GetIssueURL()
3066 if not issue_url:
3067 print >> sys.stderr, 'ERROR No issue to open'
3068 return 1
3069
3070 webbrowser.open(issue_url)
3071 return 0
3072
3073
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003074def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003075 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003076 auth.add_auth_options(parser)
3077 options, args = parser.parse_args(args)
3078 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003079 if args:
3080 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003081 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003082 props = cl.GetIssueProperties()
3083 if props.get('private'):
3084 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003085 cl.SetFlag('commit', '1')
3086 return 0
3087
3088
groby@chromium.org411034a2013-02-26 15:12:01 +00003089def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003090 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003091 auth.add_auth_options(parser)
3092 options, args = parser.parse_args(args)
3093 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003094 if args:
3095 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003096 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003097 # Ensure there actually is an issue to close.
3098 cl.GetDescription()
3099 cl.CloseIssue()
3100 return 0
3101
3102
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003103def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003104 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003105 auth.add_auth_options(parser)
3106 options, args = parser.parse_args(args)
3107 auth_config = auth.extract_auth_config_from_options(options)
3108 if args:
3109 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003110
3111 # Uncommitted (staged and unstaged) changes will be destroyed by
3112 # "git reset --hard" if there are merging conflicts in PatchIssue().
3113 # Staged changes would be committed along with the patch from last
3114 # upload, hence counted toward the "last upload" side in the final
3115 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003116 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003117 return 1
3118
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003119 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003120 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003121 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003122 if not issue:
3123 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003124 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003125 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003126
3127 # Create a new branch based on the merge-base
3128 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3129 try:
3130 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003131 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003132 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003133 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003134 return rtn
3135
wychen@chromium.org06928532015-02-03 02:11:29 +00003136 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003137 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003138 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003139 finally:
3140 RunGit(['checkout', '-q', branch])
3141 RunGit(['branch', '-D', TMP_BRANCH])
3142
3143 return 0
3144
3145
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003146def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003147 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003148 parser.add_option(
3149 '--no-color',
3150 action='store_true',
3151 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003152 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003153 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003154 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003155
3156 author = RunGit(['config', 'user.email']).strip() or None
3157
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003158 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003159
3160 if args:
3161 if len(args) > 1:
3162 parser.error('Unknown args')
3163 base_branch = args[0]
3164 else:
3165 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003166 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003167
3168 change = cl.GetChange(base_branch, None)
3169 return owners_finder.OwnersFinder(
3170 [f.LocalPath() for f in
3171 cl.GetChange(base_branch, None).AffectedFiles()],
3172 change.RepositoryRoot(), author,
3173 fopen=file, os_path=os.path, glob=glob.glob,
3174 disable_color=options.no_color).run()
3175
3176
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003177def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
3178 """Generates a diff command."""
3179 # Generate diff for the current branch's changes.
3180 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3181 upstream_commit, '--' ]
3182
3183 if args:
3184 for arg in args:
3185 if os.path.isdir(arg):
3186 diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
3187 elif os.path.isfile(arg):
3188 diff_cmd.append(arg)
3189 else:
3190 DieWithError('Argument "%s" is not a file or a directory' % arg)
3191 else:
3192 diff_cmd.extend('*' + ext for ext in extensions)
3193
3194 return diff_cmd
3195
3196
enne@chromium.org555cfe42014-01-29 18:21:39 +00003197@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003198def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003199 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003200 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003201 parser.add_option('--full', action='store_true',
3202 help='Reformat the full content of all touched files')
3203 parser.add_option('--dry-run', action='store_true',
3204 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003205 parser.add_option('--python', action='store_true',
3206 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003207 parser.add_option('--diff', action='store_true',
3208 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003209 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003210
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003211 # git diff generates paths against the root of the repository. Change
3212 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003213 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003214 if rel_base_path:
3215 os.chdir(rel_base_path)
3216
digit@chromium.org29e47272013-05-17 17:01:46 +00003217 # Grab the merge-base commit, i.e. the upstream commit of the current
3218 # branch when it was created or the last time it was rebased. This is
3219 # to cover the case where the user may have called "git fetch origin",
3220 # moving the origin branch to a newer commit, but hasn't rebased yet.
3221 upstream_commit = None
3222 cl = Changelist()
3223 upstream_branch = cl.GetUpstreamBranch()
3224 if upstream_branch:
3225 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3226 upstream_commit = upstream_commit.strip()
3227
3228 if not upstream_commit:
3229 DieWithError('Could not find base commit for this branch. '
3230 'Are you in detached state?')
3231
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003232 if opts.full:
3233 # Only list the names of modified files.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003234 diff_type = '--name-only'
enne@chromium.org555cfe42014-01-29 18:21:39 +00003235 else:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003236 # Only generate context-less patches.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003237 diff_type = '-U0'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003238
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003239 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, CLANG_EXTS)
digit@chromium.org29e47272013-05-17 17:01:46 +00003240 diff_output = RunGit(diff_cmd)
3241
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003242 top_dir = os.path.normpath(
3243 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3244
3245 # Locate the clang-format binary in the checkout
3246 try:
3247 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3248 except clang_format.NotFoundError, e:
3249 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003250
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003251 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3252 # formatted. This is used to block during the presubmit.
3253 return_value = 0
3254
digit@chromium.org29e47272013-05-17 17:01:46 +00003255 if opts.full:
3256 # diff_output is a list of files to send to clang-format.
3257 files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003258 if files:
3259 cmd = [clang_format_tool]
3260 if not opts.dry_run and not opts.diff:
3261 cmd.append('-i')
3262 stdout = RunCommand(cmd + files, cwd=top_dir)
3263 if opts.diff:
3264 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003265 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003266 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003267 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00003268 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003269 try:
3270 script = clang_format.FindClangFormatScriptInChromiumTree(
3271 'clang-format-diff.py')
3272 except clang_format.NotFoundError, e:
3273 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003274
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003275 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003276 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003277 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003278
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003279 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003280 if opts.diff:
3281 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003282 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003283 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003284
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003285 # Similar code to above, but using yapf on .py files rather than clang-format
3286 # on C/C++ files
3287 if opts.python:
3288 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, ['.py'])
3289 diff_output = RunGit(diff_cmd)
3290 yapf_tool = gclient_utils.FindExecutable('yapf')
3291 if yapf_tool is None:
3292 DieWithError('yapf not found in PATH')
3293
3294 if opts.full:
3295 files = diff_output.splitlines()
3296 if files:
3297 cmd = [yapf_tool]
3298 if not opts.dry_run and not opts.diff:
3299 cmd.append('-i')
3300 stdout = RunCommand(cmd + files, cwd=top_dir)
3301 if opts.diff:
3302 sys.stdout.write(stdout)
3303 else:
3304 # TODO(sbc): yapf --lines mode still has some issues.
3305 # https://github.com/google/yapf/issues/154
3306 DieWithError('--python currently only works with --full')
3307
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003308 # Build a diff command that only operates on dart files. dart's formatter
3309 # does not have the nice property of only operating on modified chunks, so
3310 # hard code full.
3311 dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3312 args, ['.dart'])
3313 dart_diff_output = RunGit(dart_diff_cmd)
3314 if dart_diff_output:
3315 try:
3316 command = [dart_format.FindDartFmtToolInChromiumTree()]
3317 if not opts.dry_run and not opts.diff:
3318 command.append('-w')
3319 command.extend(dart_diff_output.splitlines())
3320
3321 stdout = RunCommand(command, cwd=top_dir, env=env)
3322 if opts.dry_run and stdout:
3323 return_value = 2
3324 except dart_format.NotFoundError as e:
3325 print ('Unable to check dart code formatting. Dart SDK is not in ' +
3326 'this checkout.')
3327
3328 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003329
3330
maruel@chromium.org29404b52014-09-08 22:58:00 +00003331def CMDlol(parser, args):
3332 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003333 print zlib.decompress(base64.b64decode(
3334 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3335 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3336 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3337 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003338 return 0
3339
3340
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003341class OptionParser(optparse.OptionParser):
3342 """Creates the option parse and add --verbose support."""
3343 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003344 optparse.OptionParser.__init__(
3345 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003346 self.add_option(
3347 '-v', '--verbose', action='count', default=0,
3348 help='Use 2 times for more debugging info')
3349
3350 def parse_args(self, args=None, values=None):
3351 options, args = optparse.OptionParser.parse_args(self, args, values)
3352 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3353 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3354 return options, args
3355
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003356
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003357def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003358 if sys.hexversion < 0x02060000:
3359 print >> sys.stderr, (
3360 '\nYour python version %s is unsupported, please upgrade.\n' %
3361 sys.version.split(' ', 1)[0])
3362 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003363
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003364 # Reload settings.
3365 global settings
3366 settings = Settings()
3367
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003368 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003369 dispatcher = subcommand.CommandDispatcher(__name__)
3370 try:
3371 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003372 except auth.AuthenticationError as e:
3373 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003374 except urllib2.HTTPError, e:
3375 if e.code != 500:
3376 raise
3377 DieWithError(
3378 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3379 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003380 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003381
3382
3383if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003384 # These affect sys.stdout so do it outside of main() to simplify mocks in
3385 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003386 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003387 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003388 try:
3389 sys.exit(main(sys.argv[1:]))
3390 except KeyboardInterrupt:
3391 sys.stderr.write('interrupted\n')
3392 sys.exit(1)