blob: fd80fb701cfd2822aef83ecb1aa34db5b08280c5 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000013import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000014import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000015import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import logging
17import optparse
18import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000019import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000021import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000023import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000025import time
26import traceback
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000028import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000029import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000030import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031
32try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000033 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034except ImportError:
35 pass
36
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000037from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000038from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000039from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000040import auth
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000042import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000043import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000044import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000045import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000046import git_common
piman@chromium.org336f9122014-09-04 02:16:55 +000047import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000048import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000049import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000050import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000051import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000052import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000053import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000054import watchlists
55
maruel@chromium.org0633fb42013-08-16 20:06:14 +000056__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000058DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000059POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000060DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000061GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000062CHANGE_ID = 'Change-Id:'
rmistry@google.comc68112d2015-03-03 12:48:06 +000063REFS_THAT_ALIAS_TO_OTHER_REFS = {
64 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
65 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
66}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067
sheyang@google.com6ebaf782015-05-12 19:17:54 +000068# Buildbucket-related constants
69BUILDBUCKET_HOST = 'cr-buildbucket.appspot.com'
70
thestig@chromium.org44202a22014-03-11 19:22:18 +000071# Valid extensions for files we want to lint.
72DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
73DEFAULT_LINT_IGNORE_REGEX = r"$^"
74
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000075# Shortcut since it quickly becomes redundant.
76Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000077
maruel@chromium.orgddd59412011-11-30 14:20:38 +000078# Initialized in main()
79settings = None
80
81
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000082def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000083 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000084 sys.exit(1)
85
86
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000087def GetNoGitPagerEnv():
88 env = os.environ.copy()
89 # 'cat' is a magical git string that disables pagers on all platforms.
90 env['GIT_PAGER'] = 'cat'
91 return env
92
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000093
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000094def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000095 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000096 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000097 except subprocess2.CalledProcessError as e:
98 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000099 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000100 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000101 'Command "%s" failed.\n%s' % (
102 ' '.join(args), error_message or e.stdout or ''))
103 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104
105
106def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000107 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000108 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000109
110
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000111def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000112 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000113 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000114 if suppress_stderr:
115 stderr = subprocess2.VOID
116 else:
117 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000118 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000119 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000120 stdout=subprocess2.PIPE,
121 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000122 return code, out[0]
123 except ValueError:
124 # When the subprocess fails, it returns None. That triggers a ValueError
125 # when trying to unpack the return value into (out, code).
126 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000127
128
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000129def RunGitSilent(args):
130 """Returns stdout, suppresses stderr and ingores the return code."""
131 return RunGitWithCode(args, suppress_stderr=True)[1]
132
133
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000134def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000135 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000136 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000137 return (version.startswith(prefix) and
138 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000139
140
maruel@chromium.org90541732011-04-01 17:54:18 +0000141def ask_for_data(prompt):
142 try:
143 return raw_input(prompt)
144 except KeyboardInterrupt:
145 # Hide the exception.
146 sys.exit(1)
147
148
iannucci@chromium.org79540052012-10-19 23:15:26 +0000149def git_set_branch_value(key, value):
150 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000151 if not branch:
152 return
153
154 cmd = ['config']
155 if isinstance(value, int):
156 cmd.append('--int')
157 git_key = 'branch.%s.%s' % (branch, key)
158 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000159
160
161def git_get_branch_default(key, default):
162 branch = Changelist().GetBranch()
163 if branch:
164 git_key = 'branch.%s.%s' % (branch, key)
165 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
166 try:
167 return int(stdout.strip())
168 except ValueError:
169 pass
170 return default
171
172
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000173def add_git_similarity(parser):
174 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000175 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000176 help='Sets the percentage that a pair of files need to match in order to'
177 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000178 parser.add_option(
179 '--find-copies', action='store_true',
180 help='Allows git to look for copies.')
181 parser.add_option(
182 '--no-find-copies', action='store_false', dest='find_copies',
183 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000184
185 old_parser_args = parser.parse_args
186 def Parse(args):
187 options, args = old_parser_args(args)
188
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000189 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000190 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000191 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000192 print('Note: Saving similarity of %d%% in git config.'
193 % options.similarity)
194 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000195
iannucci@chromium.org79540052012-10-19 23:15:26 +0000196 options.similarity = max(0, min(options.similarity, 100))
197
198 if options.find_copies is None:
199 options.find_copies = bool(
200 git_get_branch_default('git-find-copies', True))
201 else:
202 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000203
204 print('Using %d%% similarity for rename/copy detection. '
205 'Override with --similarity.' % options.similarity)
206
207 return options, args
208 parser.parse_args = Parse
209
210
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000211def _prefix_master(master):
212 """Convert user-specified master name to full master name.
213
214 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
215 name, while the developers always use shortened master name
216 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
217 function does the conversion for buildbucket migration.
218 """
219 prefix = 'master.'
220 if master.startswith(prefix):
221 return master
222 return '%s%s' % (prefix, master)
223
224
225def trigger_try_jobs(auth_config, changelist, options, masters, category):
226 rietveld_url = settings.GetDefaultServerUrl()
227 rietveld_host = urlparse.urlparse(rietveld_url).hostname
228 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
229 http = authenticator.authorize(httplib2.Http())
230 http.force_exception_to_status_code = True
231 issue_props = changelist.GetIssueProperties()
232 issue = changelist.GetIssue()
233 patchset = changelist.GetMostRecentPatchset()
234
235 buildbucket_put_url = (
236 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
237 hostname=BUILDBUCKET_HOST))
238 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
239 hostname=rietveld_host,
240 issue=issue,
241 patch=patchset)
242
243 batch_req_body = {'builds': []}
244 print_text = []
245 print_text.append('Tried jobs on:')
246 for master, builders_and_tests in sorted(masters.iteritems()):
247 print_text.append('Master: %s' % master)
248 bucket = _prefix_master(master)
249 for builder, tests in sorted(builders_and_tests.iteritems()):
250 print_text.append(' %s: %s' % (builder, tests))
251 parameters = {
252 'builder_name': builder,
253 'changes': [
254 {'author': {'email': issue_props['owner_email']}},
255 ],
256 'properties': {
257 'category': category,
258 'issue': issue,
259 'master': master,
260 'patch_project': issue_props['project'],
261 'patch_storage': 'rietveld',
262 'patchset': patchset,
263 'reason': options.name,
264 'revision': options.revision,
265 'rietveld': rietveld_url,
266 'testfilter': tests,
267 },
268 }
269 if options.clobber:
270 parameters['properties']['clobber'] = True
271 batch_req_body['builds'].append(
272 {
273 'bucket': bucket,
274 'parameters_json': json.dumps(parameters),
275 'tags': ['builder:%s' % builder,
276 'buildset:%s' % buildset,
277 'master:%s' % master,
278 'user_agent:git_cl_try']
279 }
280 )
281
282 for try_count in xrange(3):
283 response, content = http.request(
284 buildbucket_put_url,
285 'PUT',
286 body=json.dumps(batch_req_body),
287 headers={'Content-Type': 'application/json'},
288 )
289 content_json = None
290 try:
291 content_json = json.loads(content)
292 except ValueError:
293 pass
294
295 # Buildbucket could return an error even if status==200.
296 if content_json and content_json.get('error'):
297 msg = 'Error in response. Code: %d. Reason: %s. Message: %s.' % (
298 content_json['error'].get('code', ''),
299 content_json['error'].get('reason', ''),
300 content_json['error'].get('message', ''))
301 raise BuildbucketResponseException(msg)
302
303 if response.status == 200:
304 if not content_json:
305 raise BuildbucketResponseException(
306 'Buildbucket returns invalid json content: %s.\n'
307 'Please file bugs at crbug.com, label "Infra-BuildBucket".' %
308 content)
309 break
310 if response.status < 500 or try_count >= 2:
311 raise httplib2.HttpLib2Error(content)
312
313 # status >= 500 means transient failures.
314 logging.debug('Transient errors when triggering tryjobs. Will retry.')
315 time.sleep(0.5 + 1.5*try_count)
316
317 print '\n'.join(print_text)
318
319
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000320def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
321 """Return the corresponding git ref if |base_url| together with |glob_spec|
322 matches the full |url|.
323
324 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
325 """
326 fetch_suburl, as_ref = glob_spec.split(':')
327 if allow_wildcards:
328 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
329 if glob_match:
330 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
331 # "branches/{472,597,648}/src:refs/remotes/svn/*".
332 branch_re = re.escape(base_url)
333 if glob_match.group(1):
334 branch_re += '/' + re.escape(glob_match.group(1))
335 wildcard = glob_match.group(2)
336 if wildcard == '*':
337 branch_re += '([^/]*)'
338 else:
339 # Escape and replace surrounding braces with parentheses and commas
340 # with pipe symbols.
341 wildcard = re.escape(wildcard)
342 wildcard = re.sub('^\\\\{', '(', wildcard)
343 wildcard = re.sub('\\\\,', '|', wildcard)
344 wildcard = re.sub('\\\\}$', ')', wildcard)
345 branch_re += wildcard
346 if glob_match.group(3):
347 branch_re += re.escape(glob_match.group(3))
348 match = re.match(branch_re, url)
349 if match:
350 return re.sub('\*$', match.group(1), as_ref)
351
352 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
353 if fetch_suburl:
354 full_url = base_url + '/' + fetch_suburl
355 else:
356 full_url = base_url
357 if full_url == url:
358 return as_ref
359 return None
360
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000361
iannucci@chromium.org79540052012-10-19 23:15:26 +0000362def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000363 """Prints statistics about the change to the user."""
364 # --no-ext-diff is broken in some versions of Git, so try to work around
365 # this by overriding the environment (but there is still a problem if the
366 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000367 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000368 if 'GIT_EXTERNAL_DIFF' in env:
369 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000370
371 if find_copies:
372 similarity_options = ['--find-copies-harder', '-l100000',
373 '-C%s' % similarity]
374 else:
375 similarity_options = ['-M%s' % similarity]
376
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000377 try:
378 stdout = sys.stdout.fileno()
379 except AttributeError:
380 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000381 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000382 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000383 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000384 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000385
386
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000387class BuildbucketResponseException(Exception):
388 pass
389
390
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000391class Settings(object):
392 def __init__(self):
393 self.default_server = None
394 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000395 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000396 self.is_git_svn = None
397 self.svn_branch = None
398 self.tree_status_url = None
399 self.viewvc_url = None
400 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000401 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000402 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000403 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000404 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000405 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000406
407 def LazyUpdateIfNeeded(self):
408 """Updates the settings from a codereview.settings file, if available."""
409 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000410 # The only value that actually changes the behavior is
411 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000412 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000413 error_ok=True
414 ).strip().lower()
415
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000416 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000417 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000418 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000419 # set updated to True to avoid infinite calling loop
420 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000421 self.updated = True
422 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000423 self.updated = True
424
425 def GetDefaultServerUrl(self, error_ok=False):
426 if not self.default_server:
427 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000428 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000429 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000430 if error_ok:
431 return self.default_server
432 if not self.default_server:
433 error_message = ('Could not find settings file. You must configure '
434 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000435 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000436 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000437 return self.default_server
438
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000439 @staticmethod
440 def GetRelativeRoot():
441 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000442
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000443 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000444 if self.root is None:
445 self.root = os.path.abspath(self.GetRelativeRoot())
446 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000447
448 def GetIsGitSvn(self):
449 """Return true if this repo looks like it's using git-svn."""
450 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000451 if self.GetPendingRefPrefix():
452 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
453 self.is_git_svn = False
454 else:
455 # If you have any "svn-remote.*" config keys, we think you're using svn.
456 self.is_git_svn = RunGitWithCode(
457 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000458 return self.is_git_svn
459
460 def GetSVNBranch(self):
461 if self.svn_branch is None:
462 if not self.GetIsGitSvn():
463 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
464
465 # Try to figure out which remote branch we're based on.
466 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000467 # 1) iterate through our branch history and find the svn URL.
468 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000469
470 # regexp matching the git-svn line that contains the URL.
471 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
472
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000473 # We don't want to go through all of history, so read a line from the
474 # pipe at a time.
475 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000476 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000477 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
478 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000479 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000480 for line in proc.stdout:
481 match = git_svn_re.match(line)
482 if match:
483 url = match.group(1)
484 proc.stdout.close() # Cut pipe.
485 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000486
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000487 if url:
488 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
489 remotes = RunGit(['config', '--get-regexp',
490 r'^svn-remote\..*\.url']).splitlines()
491 for remote in remotes:
492 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000493 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000494 remote = match.group(1)
495 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000496 rewrite_root = RunGit(
497 ['config', 'svn-remote.%s.rewriteRoot' % remote],
498 error_ok=True).strip()
499 if rewrite_root:
500 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000501 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000502 ['config', 'svn-remote.%s.fetch' % remote],
503 error_ok=True).strip()
504 if fetch_spec:
505 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
506 if self.svn_branch:
507 break
508 branch_spec = RunGit(
509 ['config', 'svn-remote.%s.branches' % remote],
510 error_ok=True).strip()
511 if branch_spec:
512 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
513 if self.svn_branch:
514 break
515 tag_spec = RunGit(
516 ['config', 'svn-remote.%s.tags' % remote],
517 error_ok=True).strip()
518 if tag_spec:
519 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
520 if self.svn_branch:
521 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000522
523 if not self.svn_branch:
524 DieWithError('Can\'t guess svn branch -- try specifying it on the '
525 'command line')
526
527 return self.svn_branch
528
529 def GetTreeStatusUrl(self, error_ok=False):
530 if not self.tree_status_url:
531 error_message = ('You must configure your tree status URL by running '
532 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000533 self.tree_status_url = self._GetRietveldConfig(
534 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000535 return self.tree_status_url
536
537 def GetViewVCUrl(self):
538 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000539 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000540 return self.viewvc_url
541
rmistry@google.com90752582014-01-14 21:04:50 +0000542 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000543 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000544
rmistry@google.com5626a922015-02-26 14:03:30 +0000545 def GetRunPostUploadHook(self):
546 run_post_upload_hook = self._GetRietveldConfig(
547 'run-post-upload-hook', error_ok=True)
548 return run_post_upload_hook == "True"
549
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000550 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000551 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000552
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000553 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000554 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000555
ukai@chromium.orge8077812012-02-03 03:41:46 +0000556 def GetIsGerrit(self):
557 """Return true if this repo is assosiated with gerrit code review system."""
558 if self.is_gerrit is None:
559 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
560 return self.is_gerrit
561
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000562 def GetGitEditor(self):
563 """Return the editor specified in the git config, or None if none is."""
564 if self.git_editor is None:
565 self.git_editor = self._GetConfig('core.editor', error_ok=True)
566 return self.git_editor or None
567
thestig@chromium.org44202a22014-03-11 19:22:18 +0000568 def GetLintRegex(self):
569 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
570 DEFAULT_LINT_REGEX)
571
572 def GetLintIgnoreRegex(self):
573 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
574 DEFAULT_LINT_IGNORE_REGEX)
575
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000576 def GetProject(self):
577 if not self.project:
578 self.project = self._GetRietveldConfig('project', error_ok=True)
579 return self.project
580
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000581 def GetForceHttpsCommitUrl(self):
582 if not self.force_https_commit_url:
583 self.force_https_commit_url = self._GetRietveldConfig(
584 'force-https-commit-url', error_ok=True)
585 return self.force_https_commit_url
586
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000587 def GetPendingRefPrefix(self):
588 if not self.pending_ref_prefix:
589 self.pending_ref_prefix = self._GetRietveldConfig(
590 'pending-ref-prefix', error_ok=True)
591 return self.pending_ref_prefix
592
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000593 def _GetRietveldConfig(self, param, **kwargs):
594 return self._GetConfig('rietveld.' + param, **kwargs)
595
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000596 def _GetConfig(self, param, **kwargs):
597 self.LazyUpdateIfNeeded()
598 return RunGit(['config', param], **kwargs).strip()
599
600
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000601def ShortBranchName(branch):
602 """Convert a name like 'refs/heads/foo' to just 'foo'."""
603 return branch.replace('refs/heads/', '')
604
605
606class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000607 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000609 global settings
610 if not settings:
611 # Happens when git_cl.py is used as a utility library.
612 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000613 settings.GetDefaultServerUrl()
614 self.branchref = branchref
615 if self.branchref:
616 self.branch = ShortBranchName(self.branchref)
617 else:
618 self.branch = None
619 self.rietveld_server = None
620 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000621 self.lookedup_issue = False
622 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000623 self.has_description = False
624 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000625 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000626 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000627 self.cc = None
628 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000629 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000630 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000631 self._remote = None
632 self._rpc_server = None
633
634 @property
635 def auth_config(self):
636 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000637
638 def GetCCList(self):
639 """Return the users cc'd on this CL.
640
641 Return is a string suitable for passing to gcl with the --cc flag.
642 """
643 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000644 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000645 more_cc = ','.join(self.watchers)
646 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
647 return self.cc
648
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000649 def GetCCListWithoutDefault(self):
650 """Return the users cc'd on this CL excluding default ones."""
651 if self.cc is None:
652 self.cc = ','.join(self.watchers)
653 return self.cc
654
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000655 def SetWatchers(self, watchers):
656 """Set the list of email addresses that should be cc'd based on the changed
657 files in this CL.
658 """
659 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000660
661 def GetBranch(self):
662 """Returns the short branch name, e.g. 'master'."""
663 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000664 branchref = RunGit(['symbolic-ref', 'HEAD'],
665 stderr=subprocess2.VOID, error_ok=True).strip()
666 if not branchref:
667 return None
668 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000669 self.branch = ShortBranchName(self.branchref)
670 return self.branch
671
672 def GetBranchRef(self):
673 """Returns the full branch name, e.g. 'refs/heads/master'."""
674 self.GetBranch() # Poke the lazy loader.
675 return self.branchref
676
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000677 @staticmethod
678 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000679 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000680 e.g. 'origin', 'refs/heads/master'
681 """
682 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000683 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
684 error_ok=True).strip()
685 if upstream_branch:
686 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
687 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000688 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
689 error_ok=True).strip()
690 if upstream_branch:
691 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000692 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000693 # Fall back on trying a git-svn upstream branch.
694 if settings.GetIsGitSvn():
695 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000696 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000697 # Else, try to guess the origin remote.
698 remote_branches = RunGit(['branch', '-r']).split()
699 if 'origin/master' in remote_branches:
700 # Fall back on origin/master if it exits.
701 remote = 'origin'
702 upstream_branch = 'refs/heads/master'
703 elif 'origin/trunk' in remote_branches:
704 # Fall back on origin/trunk if it exists. Generally a shared
705 # git-svn clone
706 remote = 'origin'
707 upstream_branch = 'refs/heads/trunk'
708 else:
709 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000710Either pass complete "git diff"-style arguments, like
711 git cl upload origin/master
712or verify this branch is set up to track another (via the --track argument to
713"git checkout -b ...").""")
714
715 return remote, upstream_branch
716
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000717 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000718 return git_common.get_or_create_merge_base(self.GetBranch(),
719 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000720
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721 def GetUpstreamBranch(self):
722 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000723 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000724 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000725 upstream_branch = upstream_branch.replace('refs/heads/',
726 'refs/remotes/%s/' % remote)
727 upstream_branch = upstream_branch.replace('refs/branch-heads/',
728 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000729 self.upstream_branch = upstream_branch
730 return self.upstream_branch
731
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000732 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000733 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000734 remote, branch = None, self.GetBranch()
735 seen_branches = set()
736 while branch not in seen_branches:
737 seen_branches.add(branch)
738 remote, branch = self.FetchUpstreamTuple(branch)
739 branch = ShortBranchName(branch)
740 if remote != '.' or branch.startswith('refs/remotes'):
741 break
742 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000743 remotes = RunGit(['remote'], error_ok=True).split()
744 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000745 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000746 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000747 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000748 logging.warning('Could not determine which remote this change is '
749 'associated with, so defaulting to "%s". This may '
750 'not be what you want. You may prevent this message '
751 'by running "git svn info" as documented here: %s',
752 self._remote,
753 GIT_INSTRUCTIONS_URL)
754 else:
755 logging.warn('Could not determine which remote this change is '
756 'associated with. You may prevent this message by '
757 'running "git svn info" as documented here: %s',
758 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000759 branch = 'HEAD'
760 if branch.startswith('refs/remotes'):
761 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000762 elif branch.startswith('refs/branch-heads/'):
763 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000764 else:
765 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000766 return self._remote
767
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000768 def GitSanityChecks(self, upstream_git_obj):
769 """Checks git repo status and ensures diff is from local commits."""
770
sbc@chromium.org79706062015-01-14 21:18:12 +0000771 if upstream_git_obj is None:
772 if self.GetBranch() is None:
773 print >> sys.stderr, (
774 'ERROR: unable to dertermine current branch (detached HEAD?)')
775 else:
776 print >> sys.stderr, (
777 'ERROR: no upstream branch')
778 return False
779
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000780 # Verify the commit we're diffing against is in our current branch.
781 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
782 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
783 if upstream_sha != common_ancestor:
784 print >> sys.stderr, (
785 'ERROR: %s is not in the current branch. You may need to rebase '
786 'your tracking branch' % upstream_sha)
787 return False
788
789 # List the commits inside the diff, and verify they are all local.
790 commits_in_diff = RunGit(
791 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
792 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
793 remote_branch = remote_branch.strip()
794 if code != 0:
795 _, remote_branch = self.GetRemoteBranch()
796
797 commits_in_remote = RunGit(
798 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
799
800 common_commits = set(commits_in_diff) & set(commits_in_remote)
801 if common_commits:
802 print >> sys.stderr, (
803 'ERROR: Your diff contains %d commits already in %s.\n'
804 'Run "git log --oneline %s..HEAD" to get a list of commits in '
805 'the diff. If you are using a custom git flow, you can override'
806 ' the reference used for this check with "git config '
807 'gitcl.remotebranch <git-ref>".' % (
808 len(common_commits), remote_branch, upstream_git_obj))
809 return False
810 return True
811
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000812 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000813 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000814
815 Returns None if it is not set.
816 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000817 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
818 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000819
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000820 def GetGitSvnRemoteUrl(self):
821 """Return the configured git-svn remote URL parsed from git svn info.
822
823 Returns None if it is not set.
824 """
825 # URL is dependent on the current directory.
826 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
827 if data:
828 keys = dict(line.split(': ', 1) for line in data.splitlines()
829 if ': ' in line)
830 return keys.get('URL', None)
831 return None
832
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000833 def GetRemoteUrl(self):
834 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
835
836 Returns None if there is no remote.
837 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000838 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000839 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
840
841 # If URL is pointing to a local directory, it is probably a git cache.
842 if os.path.isdir(url):
843 url = RunGit(['config', 'remote.%s.url' % remote],
844 error_ok=True,
845 cwd=url).strip()
846 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000847
848 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000849 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000850 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000851 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000852 self.issue = int(issue) or None if issue else None
853 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000854 return self.issue
855
856 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000857 if not self.rietveld_server:
858 # If we're on a branch then get the server potentially associated
859 # with that branch.
860 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000861 rietveld_server_config = self._RietveldServer()
862 if rietveld_server_config:
863 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
864 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000865 if not self.rietveld_server:
866 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000867 return self.rietveld_server
868
869 def GetIssueURL(self):
870 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000871 if not self.GetIssue():
872 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000873 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
874
875 def GetDescription(self, pretty=False):
876 if not self.has_description:
877 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000878 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000879 try:
880 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000881 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000882 if e.code == 404:
883 DieWithError(
884 ('\nWhile fetching the description for issue %d, received a '
885 '404 (not found)\n'
886 'error. It is likely that you deleted this '
887 'issue on the server. If this is the\n'
888 'case, please run\n\n'
889 ' git cl issue 0\n\n'
890 'to clear the association with the deleted issue. Then run '
891 'this command again.') % issue)
892 else:
893 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000894 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000895 except urllib2.URLError as e:
896 print >> sys.stderr, (
897 'Warning: Failed to retrieve CL description due to network '
898 'failure.')
899 self.description = ''
900
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000901 self.has_description = True
902 if pretty:
903 wrapper = textwrap.TextWrapper()
904 wrapper.initial_indent = wrapper.subsequent_indent = ' '
905 return wrapper.fill(self.description)
906 return self.description
907
908 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000909 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000910 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000911 patchset = RunGit(['config', self._PatchsetSetting()],
912 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000913 self.patchset = int(patchset) or None if patchset else None
914 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000915 return self.patchset
916
917 def SetPatchset(self, patchset):
918 """Set this branch's patchset. If patchset=0, clears the patchset."""
919 if patchset:
920 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000921 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000922 else:
923 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000924 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000925 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000927 def GetMostRecentPatchset(self):
928 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000929
930 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000931 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000932 '/download/issue%s_%s.diff' % (issue, patchset))
933
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000934 def GetIssueProperties(self):
935 if self._props is None:
936 issue = self.GetIssue()
937 if not issue:
938 self._props = {}
939 else:
940 self._props = self.RpcServer().get_issue_properties(issue, True)
941 return self._props
942
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000943 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000944 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000945
apavlov@chromium.orge4efd512014-11-05 09:05:29 +0000946 def AddComment(self, message):
947 return self.RpcServer().add_comment(self.GetIssue(), message)
948
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000949 def SetIssue(self, issue):
950 """Set this branch's issue. If issue=0, clears the issue."""
951 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000952 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000953 RunGit(['config', self._IssueSetting(), str(issue)])
954 if self.rietveld_server:
955 RunGit(['config', self._RietveldServer(), self.rietveld_server])
956 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000957 current_issue = self.GetIssue()
958 if current_issue:
959 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000960 self.issue = None
961 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000962
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000963 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000964 if not self.GitSanityChecks(upstream_branch):
965 DieWithError('\nGit sanity check failure')
966
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000967 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000968 if not root:
969 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000970 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000971
972 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000973 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000974 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000975 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000976 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000977 except subprocess2.CalledProcessError:
978 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000979 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000980 'This branch probably doesn\'t exist anymore. To reset the\n'
981 'tracking branch, please run\n'
982 ' git branch --set-upstream %s trunk\n'
983 'replacing trunk with origin/master or the relevant branch') %
984 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000985
maruel@chromium.org52424302012-08-29 15:14:30 +0000986 issue = self.GetIssue()
987 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000988 if issue:
989 description = self.GetDescription()
990 else:
991 # If the change was never uploaded, use the log messages of all commits
992 # up to the branch point, as git cl upload will prefill the description
993 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000994 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
995 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000996
997 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000998 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000999 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001000 name,
1001 description,
1002 absroot,
1003 files,
1004 issue,
1005 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001006 author,
1007 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001008
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001009 def GetStatus(self):
1010 """Apply a rough heuristic to give a simple summary of an issue's review
1011 or CQ status, assuming adherence to a common workflow.
1012
1013 Returns None if no issue for this branch, or one of the following keywords:
1014 * 'error' - error from review tool (including deleted issues)
1015 * 'unsent' - not sent for review
1016 * 'waiting' - waiting for review
1017 * 'reply' - waiting for owner to reply to review
1018 * 'lgtm' - LGTM from at least one approved reviewer
1019 * 'commit' - in the commit queue
1020 * 'closed' - closed
1021 """
1022 if not self.GetIssue():
1023 return None
1024
1025 try:
1026 props = self.GetIssueProperties()
1027 except urllib2.HTTPError:
1028 return 'error'
1029
1030 if props.get('closed'):
1031 # Issue is closed.
1032 return 'closed'
1033 if props.get('commit'):
1034 # Issue is in the commit queue.
1035 return 'commit'
1036
1037 try:
1038 reviewers = self.GetApprovingReviewers()
1039 except urllib2.HTTPError:
1040 return 'error'
1041
1042 if reviewers:
1043 # Was LGTM'ed.
1044 return 'lgtm'
1045
1046 messages = props.get('messages') or []
1047
1048 if not messages:
1049 # No message was sent.
1050 return 'unsent'
1051 if messages[-1]['sender'] != props.get('owner_email'):
1052 # Non-LGTM reply from non-owner
1053 return 'reply'
1054 return 'waiting'
1055
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001056 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001057 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001058
1059 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001060 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001061 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001062 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001063 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001064 except presubmit_support.PresubmitFailure, e:
1065 DieWithError(
1066 ('%s\nMaybe your depot_tools is out of date?\n'
1067 'If all fails, contact maruel@') % e)
1068
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001069 def UpdateDescription(self, description):
1070 self.description = description
1071 return self.RpcServer().update_description(
1072 self.GetIssue(), self.description)
1073
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001074 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001075 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001076 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001077
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001078 def SetFlag(self, flag, value):
1079 """Patchset must match."""
1080 if not self.GetPatchset():
1081 DieWithError('The patchset needs to match. Send another patchset.')
1082 try:
1083 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001084 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001085 except urllib2.HTTPError, e:
1086 if e.code == 404:
1087 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1088 if e.code == 403:
1089 DieWithError(
1090 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1091 'match?') % (self.GetIssue(), self.GetPatchset()))
1092 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001093
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001094 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001095 """Returns an upload.RpcServer() to access this review's rietveld instance.
1096 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001097 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001098 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001099 self.GetRietveldServer(),
1100 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001101 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001102
1103 def _IssueSetting(self):
1104 """Return the git setting that stores this change's issue."""
1105 return 'branch.%s.rietveldissue' % self.GetBranch()
1106
1107 def _PatchsetSetting(self):
1108 """Return the git setting that stores this change's most recent patchset."""
1109 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1110
1111 def _RietveldServer(self):
1112 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001113 branch = self.GetBranch()
1114 if branch:
1115 return 'branch.%s.rietveldserver' % branch
1116 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001117
1118
1119def GetCodereviewSettingsInteractively():
1120 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001121 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001122 server = settings.GetDefaultServerUrl(error_ok=True)
1123 prompt = 'Rietveld server (host[:port])'
1124 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001125 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001126 if not server and not newserver:
1127 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001128 if newserver:
1129 newserver = gclient_utils.UpgradeToHttps(newserver)
1130 if newserver != server:
1131 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001133 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134 prompt = caption
1135 if initial:
1136 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001137 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 if new_val == 'x':
1139 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001140 elif new_val:
1141 if is_url:
1142 new_val = gclient_utils.UpgradeToHttps(new_val)
1143 if new_val != initial:
1144 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001146 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001147 SetProperty(settings.GetDefaultPrivateFlag(),
1148 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001150 'tree-status-url', False)
1151 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001152 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001153 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1154 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001155
1156 # TODO: configure a default branch to diff against, rather than this
1157 # svn-based hackery.
1158
1159
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001160class ChangeDescription(object):
1161 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001162 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001163 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001164
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001165 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001166 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001167
agable@chromium.org42c20792013-09-12 17:34:49 +00001168 @property # www.logilab.org/ticket/89786
1169 def description(self): # pylint: disable=E0202
1170 return '\n'.join(self._description_lines)
1171
1172 def set_description(self, desc):
1173 if isinstance(desc, basestring):
1174 lines = desc.splitlines()
1175 else:
1176 lines = [line.rstrip() for line in desc]
1177 while lines and not lines[0]:
1178 lines.pop(0)
1179 while lines and not lines[-1]:
1180 lines.pop(-1)
1181 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001182
piman@chromium.org336f9122014-09-04 02:16:55 +00001183 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001184 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001185 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001186 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001187 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001188 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001189
agable@chromium.org42c20792013-09-12 17:34:49 +00001190 # Get the set of R= and TBR= lines and remove them from the desciption.
1191 regexp = re.compile(self.R_LINE)
1192 matches = [regexp.match(line) for line in self._description_lines]
1193 new_desc = [l for i, l in enumerate(self._description_lines)
1194 if not matches[i]]
1195 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001196
agable@chromium.org42c20792013-09-12 17:34:49 +00001197 # Construct new unified R= and TBR= lines.
1198 r_names = []
1199 tbr_names = []
1200 for match in matches:
1201 if not match:
1202 continue
1203 people = cleanup_list([match.group(2).strip()])
1204 if match.group(1) == 'TBR':
1205 tbr_names.extend(people)
1206 else:
1207 r_names.extend(people)
1208 for name in r_names:
1209 if name not in reviewers:
1210 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001211 if add_owners_tbr:
1212 owners_db = owners.Database(change.RepositoryRoot(),
1213 fopen=file, os_path=os.path, glob=glob.glob)
1214 all_reviewers = set(tbr_names + reviewers)
1215 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1216 all_reviewers)
1217 tbr_names.extend(owners_db.reviewers_for(missing_files,
1218 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001219 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1220 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1221
1222 # Put the new lines in the description where the old first R= line was.
1223 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1224 if 0 <= line_loc < len(self._description_lines):
1225 if new_tbr_line:
1226 self._description_lines.insert(line_loc, new_tbr_line)
1227 if new_r_line:
1228 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001229 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001230 if new_r_line:
1231 self.append_footer(new_r_line)
1232 if new_tbr_line:
1233 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001234
1235 def prompt(self):
1236 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001237 self.set_description([
1238 '# Enter a description of the change.',
1239 '# This will be displayed on the codereview site.',
1240 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001241 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001242 '--------------------',
1243 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001244
agable@chromium.org42c20792013-09-12 17:34:49 +00001245 regexp = re.compile(self.BUG_LINE)
1246 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001247 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001248 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001249 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001250 if not content:
1251 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001252 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001253
1254 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001255 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1256 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001257 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001258 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001259
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001260 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001261 if self._description_lines:
1262 # Add an empty line if either the last line or the new line isn't a tag.
1263 last_line = self._description_lines[-1]
1264 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1265 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1266 self._description_lines.append('')
1267 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001268
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001269 def get_reviewers(self):
1270 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001271 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1272 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001273 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001274
1275
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001276def get_approving_reviewers(props):
1277 """Retrieves the reviewers that approved a CL from the issue properties with
1278 messages.
1279
1280 Note that the list may contain reviewers that are not committer, thus are not
1281 considered by the CQ.
1282 """
1283 return sorted(
1284 set(
1285 message['sender']
1286 for message in props['messages']
1287 if message['approval'] and message['sender'] in props['reviewers']
1288 )
1289 )
1290
1291
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001292def FindCodereviewSettingsFile(filename='codereview.settings'):
1293 """Finds the given file starting in the cwd and going up.
1294
1295 Only looks up to the top of the repository unless an
1296 'inherit-review-settings-ok' file exists in the root of the repository.
1297 """
1298 inherit_ok_file = 'inherit-review-settings-ok'
1299 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001300 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001301 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1302 root = '/'
1303 while True:
1304 if filename in os.listdir(cwd):
1305 if os.path.isfile(os.path.join(cwd, filename)):
1306 return open(os.path.join(cwd, filename))
1307 if cwd == root:
1308 break
1309 cwd = os.path.dirname(cwd)
1310
1311
1312def LoadCodereviewSettingsFromFile(fileobj):
1313 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001314 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001315
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001316 def SetProperty(name, setting, unset_error_ok=False):
1317 fullname = 'rietveld.' + name
1318 if setting in keyvals:
1319 RunGit(['config', fullname, keyvals[setting]])
1320 else:
1321 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1322
1323 SetProperty('server', 'CODE_REVIEW_SERVER')
1324 # Only server setting is required. Other settings can be absent.
1325 # In that case, we ignore errors raised during option deletion attempt.
1326 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001327 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001328 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1329 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001330 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001331 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001332 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1333 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001334 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001335 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001336 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001337 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1338 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001340 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001341 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001342
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001343 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1344 #should be of the form
1345 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1346 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1347 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1348 keyvals['ORIGIN_URL_CONFIG']])
1349
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001350
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001351def urlretrieve(source, destination):
1352 """urllib is broken for SSL connections via a proxy therefore we
1353 can't use urllib.urlretrieve()."""
1354 with open(destination, 'w') as f:
1355 f.write(urllib2.urlopen(source).read())
1356
1357
ukai@chromium.org712d6102013-11-27 00:52:58 +00001358def hasSheBang(fname):
1359 """Checks fname is a #! script."""
1360 with open(fname) as f:
1361 return f.read(2).startswith('#!')
1362
1363
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001364def DownloadHooks(force):
1365 """downloads hooks
1366
1367 Args:
1368 force: True to update hooks. False to install hooks if not present.
1369 """
1370 if not settings.GetIsGerrit():
1371 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001372 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001373 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1374 if not os.access(dst, os.X_OK):
1375 if os.path.exists(dst):
1376 if not force:
1377 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001378 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001379 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001380 if not hasSheBang(dst):
1381 DieWithError('Not a script: %s\n'
1382 'You need to download from\n%s\n'
1383 'into .git/hooks/commit-msg and '
1384 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001385 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1386 except Exception:
1387 if os.path.exists(dst):
1388 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001389 DieWithError('\nFailed to download hooks.\n'
1390 'You need to download from\n%s\n'
1391 'into .git/hooks/commit-msg and '
1392 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001393
1394
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001395@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001397 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001398
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001399 parser.add_option('--activate-update', action='store_true',
1400 help='activate auto-updating [rietveld] section in '
1401 '.git/config')
1402 parser.add_option('--deactivate-update', action='store_true',
1403 help='deactivate auto-updating [rietveld] section in '
1404 '.git/config')
1405 options, args = parser.parse_args(args)
1406
1407 if options.deactivate_update:
1408 RunGit(['config', 'rietveld.autoupdate', 'false'])
1409 return
1410
1411 if options.activate_update:
1412 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1413 return
1414
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415 if len(args) == 0:
1416 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001417 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001418 return 0
1419
1420 url = args[0]
1421 if not url.endswith('codereview.settings'):
1422 url = os.path.join(url, 'codereview.settings')
1423
1424 # Load code review settings and download hooks (if available).
1425 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001426 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427 return 0
1428
1429
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001430def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001431 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001432 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1433 branch = ShortBranchName(branchref)
1434 _, args = parser.parse_args(args)
1435 if not args:
1436 print("Current base-url:")
1437 return RunGit(['config', 'branch.%s.base-url' % branch],
1438 error_ok=False).strip()
1439 else:
1440 print("Setting base-url to %s" % args[0])
1441 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1442 error_ok=False).strip()
1443
1444
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001445def color_for_status(status):
1446 """Maps a Changelist status to color, for CMDstatus and other tools."""
1447 return {
1448 'unsent': Fore.RED,
1449 'waiting': Fore.BLUE,
1450 'reply': Fore.YELLOW,
1451 'lgtm': Fore.GREEN,
1452 'commit': Fore.MAGENTA,
1453 'closed': Fore.CYAN,
1454 'error': Fore.WHITE,
1455 }.get(status, Fore.WHITE)
1456
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001457def fetch_cl_status(b, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001458 """Fetches information for an issue and returns (branch, issue, color)."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001459 c = Changelist(branchref=b, auth_config=auth_config)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001460 i = c.GetIssueURL()
1461 status = c.GetStatus()
1462 color = color_for_status(status)
1463
1464 if i and (not status or status == 'error'):
1465 # The issue probably doesn't exist anymore.
1466 i += ' (broken)'
1467
1468 return (b, i, color)
1469
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001470def get_cl_statuses(
1471 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001472 """Returns a blocking iterable of (branch, issue, color) for given branches.
1473
1474 If fine_grained is true, this will fetch CL statuses from the server.
1475 Otherwise, simply indicate if there's a matching url for the given branches.
1476
1477 If max_processes is specified, it is used as the maximum number of processes
1478 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1479 spawned.
1480 """
1481 # Silence upload.py otherwise it becomes unwieldly.
1482 upload.verbosity = 0
1483
1484 if fine_grained:
1485 # Process one branch synchronously to work through authentication, then
1486 # spawn processes to process all the other branches in parallel.
1487 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001488 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1489 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001490
1491 branches_to_fetch = branches[1:]
1492 pool = ThreadPool(
1493 min(max_processes, len(branches_to_fetch))
1494 if max_processes is not None
1495 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001496 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001497 yield x
1498 else:
1499 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1500 for b in branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001501 c = Changelist(branchref=b, auth_config=auth_config)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001502 url = c.GetIssueURL()
1503 yield (b, url, Fore.BLUE if url else Fore.WHITE)
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001504
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001505def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001506 """Show status of changelists.
1507
1508 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001509 - Red not sent for review or broken
1510 - Blue waiting for review
1511 - Yellow waiting for you to reply to review
1512 - Green LGTM'ed
1513 - Magenta in the commit queue
1514 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001515
1516 Also see 'git cl comments'.
1517 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001518 parser.add_option('--field',
1519 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001520 parser.add_option('-f', '--fast', action='store_true',
1521 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001522 parser.add_option(
1523 '-j', '--maxjobs', action='store', type=int,
1524 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001525
1526 auth.add_auth_options(parser)
1527 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001528 if args:
1529 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001530 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001531
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001532 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001533 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001534 if options.field.startswith('desc'):
1535 print cl.GetDescription()
1536 elif options.field == 'id':
1537 issueid = cl.GetIssue()
1538 if issueid:
1539 print issueid
1540 elif options.field == 'patch':
1541 patchset = cl.GetPatchset()
1542 if patchset:
1543 print patchset
1544 elif options.field == 'url':
1545 url = cl.GetIssueURL()
1546 if url:
1547 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001548 return 0
1549
1550 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1551 if not branches:
1552 print('No local branch found.')
1553 return 0
1554
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001555 changes = (
1556 Changelist(branchref=b, auth_config=auth_config)
1557 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001558 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001559 alignment = max(5, max(len(b) for b in branches))
1560 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001561 output = get_cl_statuses(branches,
1562 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001563 max_processes=options.maxjobs,
1564 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001565
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001566 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001567 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001568 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001569 while branch not in branch_statuses:
1570 b, i, color = output.next()
1571 branch_statuses[b] = (i, color)
1572 issue, color = branch_statuses.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001573 reset = Fore.RESET
1574 if not sys.stdout.isatty():
1575 color = ''
1576 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001577 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001578 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001579
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001580 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001581 print
1582 print 'Current branch:',
1583 if not cl.GetIssue():
1584 print 'no issue assigned.'
1585 return 0
1586 print cl.GetBranch()
1587 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001588 if not options.fast:
1589 print 'Issue description:'
1590 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001591 return 0
1592
1593
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001594def colorize_CMDstatus_doc():
1595 """To be called once in main() to add colors to git cl status help."""
1596 colors = [i for i in dir(Fore) if i[0].isupper()]
1597
1598 def colorize_line(line):
1599 for color in colors:
1600 if color in line.upper():
1601 # Extract whitespaces first and the leading '-'.
1602 indent = len(line) - len(line.lstrip(' ')) + 1
1603 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1604 return line
1605
1606 lines = CMDstatus.__doc__.splitlines()
1607 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1608
1609
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001610@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001611def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001612 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001613
1614 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001615 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001616 parser.add_option('-r', '--reverse', action='store_true',
1617 help='Lookup the branch(es) for the specified issues. If '
1618 'no issues are specified, all branches with mapped '
1619 'issues will be listed.')
1620 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001621
dnj@chromium.org406c4402015-03-03 17:22:28 +00001622 if options.reverse:
1623 branches = RunGit(['for-each-ref', 'refs/heads',
1624 '--format=%(refname:short)']).splitlines()
1625
1626 # Reverse issue lookup.
1627 issue_branch_map = {}
1628 for branch in branches:
1629 cl = Changelist(branchref=branch)
1630 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1631 if not args:
1632 args = sorted(issue_branch_map.iterkeys())
1633 for issue in args:
1634 if not issue:
1635 continue
1636 print 'Branch for issue number %s: %s' % (
1637 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1638 else:
1639 cl = Changelist()
1640 if len(args) > 0:
1641 try:
1642 issue = int(args[0])
1643 except ValueError:
1644 DieWithError('Pass a number to set the issue or none to list it.\n'
1645 'Maybe you want to run git cl status?')
1646 cl.SetIssue(issue)
1647 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001648 return 0
1649
1650
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001651def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001652 """Shows or posts review comments for any changelist."""
1653 parser.add_option('-a', '--add-comment', dest='comment',
1654 help='comment to add to an issue')
1655 parser.add_option('-i', dest='issue',
1656 help="review issue id (defaults to current issue)")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001657 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001658 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001659 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001660
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001661 issue = None
1662 if options.issue:
1663 try:
1664 issue = int(options.issue)
1665 except ValueError:
1666 DieWithError('A review issue id is expected to be a number')
1667
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001668 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001669
1670 if options.comment:
1671 cl.AddComment(options.comment)
1672 return 0
1673
1674 data = cl.GetIssueProperties()
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001675 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001676 if message['disapproval']:
1677 color = Fore.RED
1678 elif message['approval']:
1679 color = Fore.GREEN
1680 elif message['sender'] == data['owner_email']:
1681 color = Fore.MAGENTA
1682 else:
1683 color = Fore.BLUE
1684 print '\n%s%s %s%s' % (
1685 color, message['date'].split('.', 1)[0], message['sender'],
1686 Fore.RESET)
1687 if message['text'].strip():
1688 print '\n'.join(' ' + l for l in message['text'].splitlines())
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001689 return 0
1690
1691
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001692def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001693 """Brings up the editor for the current CL's description."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001694 auth.add_auth_options(parser)
1695 options, _ = parser.parse_args(args)
1696 auth_config = auth.extract_auth_config_from_options(options)
1697 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001698 if not cl.GetIssue():
1699 DieWithError('This branch has no associated changelist.')
1700 description = ChangeDescription(cl.GetDescription())
1701 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00001702 if cl.GetDescription() != description.description:
1703 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001704 return 0
1705
1706
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001707def CreateDescriptionFromLog(args):
1708 """Pulls out the commit log to use as a base for the CL description."""
1709 log_args = []
1710 if len(args) == 1 and not args[0].endswith('.'):
1711 log_args = [args[0] + '..']
1712 elif len(args) == 1 and args[0].endswith('...'):
1713 log_args = [args[0][:-1]]
1714 elif len(args) == 2:
1715 log_args = [args[0] + '..' + args[1]]
1716 else:
1717 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001718 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001719
1720
thestig@chromium.org44202a22014-03-11 19:22:18 +00001721def CMDlint(parser, args):
1722 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001723 parser.add_option('--filter', action='append', metavar='-x,+y',
1724 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001725 auth.add_auth_options(parser)
1726 options, args = parser.parse_args(args)
1727 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001728
1729 # Access to a protected member _XX of a client class
1730 # pylint: disable=W0212
1731 try:
1732 import cpplint
1733 import cpplint_chromium
1734 except ImportError:
1735 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1736 return 1
1737
1738 # Change the current working directory before calling lint so that it
1739 # shows the correct base.
1740 previous_cwd = os.getcwd()
1741 os.chdir(settings.GetRoot())
1742 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001743 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001744 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1745 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001746 if not files:
1747 print "Cannot lint an empty CL"
1748 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001749
1750 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001751 command = args + files
1752 if options.filter:
1753 command = ['--filter=' + ','.join(options.filter)] + command
1754 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001755
1756 white_regex = re.compile(settings.GetLintRegex())
1757 black_regex = re.compile(settings.GetLintIgnoreRegex())
1758 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1759 for filename in filenames:
1760 if white_regex.match(filename):
1761 if black_regex.match(filename):
1762 print "Ignoring file %s" % filename
1763 else:
1764 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1765 extra_check_functions)
1766 else:
1767 print "Skipping file %s" % filename
1768 finally:
1769 os.chdir(previous_cwd)
1770 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1771 if cpplint._cpplint_state.error_count != 0:
1772 return 1
1773 return 0
1774
1775
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001776def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001777 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001778 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001779 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001780 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001781 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001782 auth.add_auth_options(parser)
1783 options, args = parser.parse_args(args)
1784 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001785
sbc@chromium.org71437c02015-04-09 19:29:40 +00001786 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00001787 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001788 return 1
1789
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001790 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001791 if args:
1792 base_branch = args[0]
1793 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001794 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001795 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001796
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001797 cl.RunHook(
1798 committing=not options.upload,
1799 may_prompt=False,
1800 verbose=options.verbose,
1801 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001802 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001803
1804
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001805def AddChangeIdToCommitMessage(options, args):
1806 """Re-commits using the current message, assumes the commit hook is in
1807 place.
1808 """
1809 log_desc = options.message or CreateDescriptionFromLog(args)
1810 git_command = ['commit', '--amend', '-m', log_desc]
1811 RunGit(git_command)
1812 new_log_desc = CreateDescriptionFromLog(args)
1813 if CHANGE_ID in new_log_desc:
1814 print 'git-cl: Added Change-Id to commit message.'
1815 else:
1816 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1817
1818
piman@chromium.org336f9122014-09-04 02:16:55 +00001819def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001820 """upload the current branch to gerrit."""
1821 # We assume the remote called "origin" is the one we want.
1822 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001823 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00001824
1825 remote, remote_branch = cl.GetRemoteBranch()
1826 branch = GetTargetRef(remote, remote_branch, options.target_branch,
1827 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001828
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001829 change_desc = ChangeDescription(
1830 options.message or CreateDescriptionFromLog(args))
1831 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001832 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001833 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001834
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001835 if options.squash:
1836 # Try to get the message from a previous upload.
1837 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
1838 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
1839 if not message:
1840 if not options.force:
1841 change_desc.prompt()
1842
1843 if CHANGE_ID not in change_desc.description:
1844 # Run the commit-msg hook without modifying the head commit by writing
1845 # the commit message to a temporary file and running the hook over it,
1846 # then reading the file back in.
1847 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
1848 'commit-msg')
1849 file_handle, msg_file = tempfile.mkstemp(text=True,
1850 prefix='commit_msg')
1851 try:
1852 try:
1853 with os.fdopen(file_handle, 'w') as fileobj:
1854 fileobj.write(change_desc.description)
1855 finally:
1856 os.close(file_handle)
1857 RunCommand([commit_msg_hook, msg_file])
1858 change_desc.set_description(gclient_utils.FileRead(msg_file))
1859 finally:
1860 os.remove(msg_file)
1861
1862 if not change_desc.description:
1863 print "Description is empty; aborting."
1864 return 1
1865
1866 message = change_desc.description
1867
1868 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1869 if remote is '.':
1870 # If our upstream branch is local, we base our squashed commit on its
1871 # squashed version.
1872 parent = ('refs/heads/git_cl_uploads/' +
1873 scm.GIT.ShortBranchName(upstream_branch))
1874
1875 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
1876 # will create additional CLs when uploading.
1877 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
1878 RunGitSilent(['rev-parse', parent + ':'])):
1879 print 'Upload upstream branch ' + upstream_branch + ' first.'
1880 return 1
1881 else:
1882 parent = cl.GetCommonAncestorWithUpstream()
1883
1884 tree = RunGit(['rev-parse', 'HEAD:']).strip()
1885 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
1886 '-m', message]).strip()
1887 else:
1888 if CHANGE_ID not in change_desc.description:
1889 AddChangeIdToCommitMessage(options, args)
1890 ref_to_push = 'HEAD'
1891 parent = '%s/%s' % (gerrit_remote, branch)
1892
1893 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
1894 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001895 if len(commits) > 1:
1896 print('WARNING: This will upload %d commits. Run the following command '
1897 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001898 print('git log %s..%s' % (parent, ref_to_push))
1899 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001900 'commit.')
1901 ask_for_data('About to upload; enter to confirm.')
1902
piman@chromium.org336f9122014-09-04 02:16:55 +00001903 if options.reviewers or options.tbr_owners:
1904 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001905
ukai@chromium.orge8077812012-02-03 03:41:46 +00001906 receive_options = []
1907 cc = cl.GetCCList().split(',')
1908 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001909 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001910 cc = filter(None, cc)
1911 if cc:
1912 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001913 if change_desc.get_reviewers():
1914 receive_options.extend(
1915 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001916
ukai@chromium.orge8077812012-02-03 03:41:46 +00001917 git_command = ['push']
1918 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001919 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001920 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001921 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00001922 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001923
1924 if options.squash:
1925 head = RunGit(['rev-parse', 'HEAD']).strip()
1926 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
1927
ukai@chromium.orge8077812012-02-03 03:41:46 +00001928 # TODO(ukai): parse Change-Id: and set issue number?
1929 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001930
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001931
wittman@chromium.org455dc922015-01-26 20:15:50 +00001932def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
1933 """Computes the remote branch ref to use for the CL.
1934
1935 Args:
1936 remote (str): The git remote for the CL.
1937 remote_branch (str): The git remote branch for the CL.
1938 target_branch (str): The target branch specified by the user.
1939 pending_prefix (str): The pending prefix from the settings.
1940 """
1941 if not (remote and remote_branch):
1942 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001943
wittman@chromium.org455dc922015-01-26 20:15:50 +00001944 if target_branch:
1945 # Cannonicalize branch references to the equivalent local full symbolic
1946 # refs, which are then translated into the remote full symbolic refs
1947 # below.
1948 if '/' not in target_branch:
1949 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
1950 else:
1951 prefix_replacements = (
1952 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
1953 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
1954 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
1955 )
1956 match = None
1957 for regex, replacement in prefix_replacements:
1958 match = re.search(regex, target_branch)
1959 if match:
1960 remote_branch = target_branch.replace(match.group(0), replacement)
1961 break
1962 if not match:
1963 # This is a branch path but not one we recognize; use as-is.
1964 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00001965 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
1966 # Handle the refs that need to land in different refs.
1967 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001968
wittman@chromium.org455dc922015-01-26 20:15:50 +00001969 # Create the true path to the remote branch.
1970 # Does the following translation:
1971 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
1972 # * refs/remotes/origin/master -> refs/heads/master
1973 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
1974 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
1975 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
1976 elif remote_branch.startswith('refs/remotes/%s/' % remote):
1977 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
1978 'refs/heads/')
1979 elif remote_branch.startswith('refs/remotes/branch-heads'):
1980 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
1981 # If a pending prefix exists then replace refs/ with it.
1982 if pending_prefix:
1983 remote_branch = remote_branch.replace('refs/', pending_prefix)
1984 return remote_branch
1985
1986
piman@chromium.org336f9122014-09-04 02:16:55 +00001987def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001988 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001989 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1990 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001991 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001992 if options.emulate_svn_auto_props:
1993 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001994
1995 change_desc = None
1996
pgervais@chromium.org91141372014-01-09 23:27:20 +00001997 if options.email is not None:
1998 upload_args.extend(['--email', options.email])
1999
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002000 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002001 if options.title:
2002 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002003 if options.message:
2004 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002005 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002006 print ("This branch is associated with issue %s. "
2007 "Adding patch to that issue." % cl.GetIssue())
2008 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002009 if options.title:
2010 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002011 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002012 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002013 if options.reviewers or options.tbr_owners:
2014 change_desc.update_reviewers(options.reviewers,
2015 options.tbr_owners,
2016 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002017 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002018 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002019
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002020 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002021 print "Description is empty; aborting."
2022 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002023
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002024 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002025 if change_desc.get_reviewers():
2026 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002027 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002028 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002029 DieWithError("Must specify reviewers to send email.")
2030 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002031
2032 # We check this before applying rietveld.private assuming that in
2033 # rietveld.cc only addresses which we can send private CLs to are listed
2034 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2035 # --private is specified explicitly on the command line.
2036 if options.private:
2037 logging.warn('rietveld.cc is ignored since private flag is specified. '
2038 'You need to review and add them manually if necessary.')
2039 cc = cl.GetCCListWithoutDefault()
2040 else:
2041 cc = cl.GetCCList()
2042 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002043 if cc:
2044 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002045
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002046 if options.private or settings.GetDefaultPrivateFlag() == "True":
2047 upload_args.append('--private')
2048
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002049 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002050 if not options.find_copies:
2051 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002052
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002053 # Include the upstream repo's URL in the change -- this is useful for
2054 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002055 remote_url = cl.GetGitBaseUrlFromConfig()
2056 if not remote_url:
2057 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002058 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002059 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002060 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2061 remote_url = (cl.GetRemoteUrl() + '@'
2062 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002063 if remote_url:
2064 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002065 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002066 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2067 settings.GetPendingRefPrefix())
2068 if target_ref:
2069 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002070
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002071 project = settings.GetProject()
2072 if project:
2073 upload_args.extend(['--project', project])
2074
rmistry@google.comef966222015-04-07 11:15:01 +00002075 if options.cq_dry_run:
2076 upload_args.extend(['--cq_dry_run'])
2077
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002078 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002079 upload_args = ['upload'] + upload_args + args
2080 logging.info('upload.RealMain(%s)', upload_args)
2081 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002082 issue = int(issue)
2083 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002084 except KeyboardInterrupt:
2085 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002086 except:
2087 # If we got an exception after the user typed a description for their
2088 # change, back up the description before re-raising.
2089 if change_desc:
2090 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2091 print '\nGot exception while uploading -- saving description to %s\n' \
2092 % backup_path
2093 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002094 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002095 backup_file.close()
2096 raise
2097
2098 if not cl.GetIssue():
2099 cl.SetIssue(issue)
2100 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002101
2102 if options.use_commit_queue:
2103 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002104 return 0
2105
2106
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002107def cleanup_list(l):
2108 """Fixes a list so that comma separated items are put as individual items.
2109
2110 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2111 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2112 """
2113 items = sum((i.split(',') for i in l), [])
2114 stripped_items = (i.strip() for i in items)
2115 return sorted(filter(None, stripped_items))
2116
2117
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002118@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002119def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002120 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00002121 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2122 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002123 parser.add_option('--bypass-watchlists', action='store_true',
2124 dest='bypass_watchlists',
2125 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002126 parser.add_option('-f', action='store_true', dest='force',
2127 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002128 parser.add_option('-m', dest='message', help='message for patchset')
2129 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002130 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002131 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002132 help='reviewer email addresses')
2133 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002134 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002135 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002136 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002137 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002138 parser.add_option('--emulate_svn_auto_props',
2139 '--emulate-svn-auto-props',
2140 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002141 dest="emulate_svn_auto_props",
2142 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002143 parser.add_option('-c', '--use-commit-queue', action='store_true',
2144 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002145 parser.add_option('--private', action='store_true',
2146 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002147 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002148 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002149 metavar='TARGET',
2150 help='Apply CL to remote ref TARGET. ' +
2151 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002152 parser.add_option('--squash', action='store_true',
2153 help='Squash multiple commits into one (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002154 parser.add_option('--email', default=None,
2155 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002156 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2157 help='add a set of OWNERS to TBR')
rmistry@google.comef966222015-04-07 11:15:01 +00002158 parser.add_option('--cq-dry-run', dest='cq_dry_run', action='store_true',
2159 help='Send the patchset to do a CQ dry run right after '
2160 'upload.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002161
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002162 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002163 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002164 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002165 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002166
sbc@chromium.org71437c02015-04-09 19:29:40 +00002167 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002168 return 1
2169
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002170 options.reviewers = cleanup_list(options.reviewers)
2171 options.cc = cleanup_list(options.cc)
2172
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002173 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002174 if args:
2175 # TODO(ukai): is it ok for gerrit case?
2176 base_branch = args[0]
2177 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002178 if cl.GetBranch() is None:
2179 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2180
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002181 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002182 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002183 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002184
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002185 # Make sure authenticated to Rietveld before running expensive hooks. It is
2186 # a fast, best efforts check. Rietveld still can reject the authentication
2187 # during the actual upload.
2188 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2189 authenticator = auth.get_authenticator_for_host(
2190 cl.GetRietveldServer(), auth_config)
2191 if not authenticator.has_cached_credentials():
2192 raise auth.LoginRequiredError(cl.GetRietveldServer())
2193
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002194 # Apply watchlists on upload.
2195 change = cl.GetChange(base_branch, None)
2196 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2197 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002198 if not options.bypass_watchlists:
2199 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002200
ukai@chromium.orge8077812012-02-03 03:41:46 +00002201 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002202 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002203 # Set the reviewer list now so that presubmit checks can access it.
2204 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002205 change_description.update_reviewers(options.reviewers,
2206 options.tbr_owners,
2207 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002208 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002209 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002210 may_prompt=not options.force,
2211 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002212 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002213 if not hook_results.should_continue():
2214 return 1
2215 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002216 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002217
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002218 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002219 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002220 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002221 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002222 print ('The last upload made from this repository was patchset #%d but '
2223 'the most recent patchset on the server is #%d.'
2224 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002225 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2226 'from another machine or branch the patch you\'re uploading now '
2227 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002228 ask_for_data('About to upload; enter to confirm.')
2229
iannucci@chromium.org79540052012-10-19 23:15:26 +00002230 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002231 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00002232 return GerritUpload(options, args, cl, change)
2233 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002234 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002235 git_set_branch_value('last-upload-hash',
2236 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002237 # Run post upload hooks, if specified.
2238 if settings.GetRunPostUploadHook():
2239 presubmit_support.DoPostUploadExecuter(
2240 change,
2241 cl,
2242 settings.GetRoot(),
2243 options.verbose,
2244 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002245
2246 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002247
2248
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002249def IsSubmoduleMergeCommit(ref):
2250 # When submodules are added to the repo, we expect there to be a single
2251 # non-git-svn merge commit at remote HEAD with a signature comment.
2252 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002253 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002254 return RunGit(cmd) != ''
2255
2256
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002257def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002258 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002259
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002260 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002261 Updates changelog with metadata (e.g. pointer to review).
2262 Pushes/dcommits the code upstream.
2263 Updates review and closes.
2264 """
2265 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2266 help='bypass upload presubmit hook')
2267 parser.add_option('-m', dest='message',
2268 help="override review description")
2269 parser.add_option('-f', action='store_true', dest='force',
2270 help="force yes to questions (don't prompt)")
2271 parser.add_option('-c', dest='contributor',
2272 help="external contributor for patch (appended to " +
2273 "description and used as author for git). Should be " +
2274 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002275 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002276 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002277 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002278 auth_config = auth.extract_auth_config_from_options(options)
2279
2280 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002281
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002282 current = cl.GetBranch()
2283 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2284 if not settings.GetIsGitSvn() and remote == '.':
2285 print
2286 print 'Attempting to push branch %r into another local branch!' % current
2287 print
2288 print 'Either reparent this branch on top of origin/master:'
2289 print ' git reparent-branch --root'
2290 print
2291 print 'OR run `git rebase-update` if you think the parent branch is already'
2292 print 'committed.'
2293 print
2294 print ' Current parent: %r' % upstream_branch
2295 return 1
2296
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002297 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002298 # Default to merging against our best guess of the upstream branch.
2299 args = [cl.GetUpstreamBranch()]
2300
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002301 if options.contributor:
2302 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2303 print "Please provide contibutor as 'First Last <email@example.com>'"
2304 return 1
2305
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002306 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002307 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002308
sbc@chromium.org71437c02015-04-09 19:29:40 +00002309 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002310 return 1
2311
2312 # This rev-list syntax means "show all commits not in my branch that
2313 # are in base_branch".
2314 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2315 base_branch]).splitlines()
2316 if upstream_commits:
2317 print ('Base branch "%s" has %d commits '
2318 'not in this branch.' % (base_branch, len(upstream_commits)))
2319 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2320 return 1
2321
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002322 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002323 svn_head = None
2324 if cmd == 'dcommit' or base_has_submodules:
2325 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2326 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002327
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002328 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002329 # If the base_head is a submodule merge commit, the first parent of the
2330 # base_head should be a git-svn commit, which is what we're interested in.
2331 base_svn_head = base_branch
2332 if base_has_submodules:
2333 base_svn_head += '^1'
2334
2335 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002336 if extra_commits:
2337 print ('This branch has %d additional commits not upstreamed yet.'
2338 % len(extra_commits.splitlines()))
2339 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2340 'before attempting to %s.' % (base_branch, cmd))
2341 return 1
2342
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002343 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002344 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002345 author = None
2346 if options.contributor:
2347 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002348 hook_results = cl.RunHook(
2349 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002350 may_prompt=not options.force,
2351 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002352 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002353 if not hook_results.should_continue():
2354 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002355
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002356 # Check the tree status if the tree status URL is set.
2357 status = GetTreeStatus()
2358 if 'closed' == status:
2359 print('The tree is closed. Please wait for it to reopen. Use '
2360 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2361 return 1
2362 elif 'unknown' == status:
2363 print('Unable to determine tree status. Please verify manually and '
2364 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2365 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002366 else:
2367 breakpad.SendStack(
2368 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002369 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2370 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002371 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002372
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002373 change_desc = ChangeDescription(options.message)
2374 if not change_desc.description and cl.GetIssue():
2375 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002376
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002377 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002378 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002379 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002380 else:
2381 print 'No description set.'
2382 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2383 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002384
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002385 # Keep a separate copy for the commit message, because the commit message
2386 # contains the link to the Rietveld issue, while the Rietveld message contains
2387 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002388 # Keep a separate copy for the commit message.
2389 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002390 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002391
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002392 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002393 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002394 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002395 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002396 commit_desc.append_footer('Patch from %s.' % options.contributor)
2397
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002398 print('Description:')
2399 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002400
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002401 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002402 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002403 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002404
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002405 # We want to squash all this branch's commits into one commit with the proper
2406 # description. We do this by doing a "reset --soft" to the base branch (which
2407 # keeps the working copy the same), then dcommitting that. If origin/master
2408 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2409 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002410 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002411 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2412 # Delete the branches if they exist.
2413 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2414 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2415 result = RunGitWithCode(showref_cmd)
2416 if result[0] == 0:
2417 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002418
2419 # We might be in a directory that's present in this branch but not in the
2420 # trunk. Move up to the top of the tree so that git commands that expect a
2421 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002422 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002423 if rel_base_path:
2424 os.chdir(rel_base_path)
2425
2426 # Stuff our change into the merge branch.
2427 # We wrap in a try...finally block so if anything goes wrong,
2428 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002429 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002430 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002431 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002432 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002433 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002434 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002435 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002436 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002437 RunGit(
2438 [
2439 'commit', '--author', options.contributor,
2440 '-m', commit_desc.description,
2441 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002442 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002443 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002444 if base_has_submodules:
2445 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2446 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2447 RunGit(['checkout', CHERRY_PICK_BRANCH])
2448 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002449 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002450 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002451 pending_prefix = settings.GetPendingRefPrefix()
2452 if not pending_prefix or branch.startswith(pending_prefix):
2453 # If not using refs/pending/heads/* at all, or target ref is already set
2454 # to pending, then push to the target ref directly.
2455 retcode, output = RunGitWithCode(
2456 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002457 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002458 else:
2459 # Cherry-pick the change on top of pending ref and then push it.
2460 assert branch.startswith('refs/'), branch
2461 assert pending_prefix[-1] == '/', pending_prefix
2462 pending_ref = pending_prefix + branch[len('refs/'):]
2463 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002464 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002465 if retcode == 0:
2466 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002467 else:
2468 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002469 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002470 'svn', 'dcommit',
2471 '-C%s' % options.similarity,
2472 '--no-rebase', '--rmdir',
2473 ]
2474 if settings.GetForceHttpsCommitUrl():
2475 # Allow forcing https commit URLs for some projects that don't allow
2476 # committing to http URLs (like Google Code).
2477 remote_url = cl.GetGitSvnRemoteUrl()
2478 if urlparse.urlparse(remote_url).scheme == 'http':
2479 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002480 cmd_args.append('--commit-url=%s' % remote_url)
2481 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002482 if 'Committed r' in output:
2483 revision = re.match(
2484 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2485 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002486 finally:
2487 # And then swap back to the original branch and clean up.
2488 RunGit(['checkout', '-q', cl.GetBranch()])
2489 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002490 if base_has_submodules:
2491 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002492
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002493 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002494 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002495 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002496
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002497 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002498 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002499 try:
2500 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2501 # We set pushed_to_pending to False, since it made it all the way to the
2502 # real ref.
2503 pushed_to_pending = False
2504 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002505 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002506
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002507 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002508 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002509 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002510 if not to_pending:
2511 if viewvc_url and revision:
2512 change_desc.append_footer(
2513 'Committed: %s%s' % (viewvc_url, revision))
2514 elif revision:
2515 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002516 print ('Closing issue '
2517 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002518 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002519 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002520 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002521 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002522 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002523 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002524 if options.bypass_hooks:
2525 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2526 else:
2527 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002528 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002529 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002530
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002531 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002532 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2533 print 'The commit is in the pending queue (%s).' % pending_ref
2534 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002535 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002536 'footer.' % branch)
2537
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002538 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2539 if os.path.isfile(hook):
2540 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002541
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002542 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002543
2544
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002545def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2546 print
2547 print 'Waiting for commit to be landed on %s...' % real_ref
2548 print '(If you are impatient, you may Ctrl-C once without harm)'
2549 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2550 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2551
2552 loop = 0
2553 while True:
2554 sys.stdout.write('fetching (%d)... \r' % loop)
2555 sys.stdout.flush()
2556 loop += 1
2557
2558 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2559 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2560 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2561 for commit in commits.splitlines():
2562 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2563 print 'Found commit on %s' % real_ref
2564 return commit
2565
2566 current_rev = to_rev
2567
2568
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002569def PushToGitPending(remote, pending_ref, upstream_ref):
2570 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2571
2572 Returns:
2573 (retcode of last operation, output log of last operation).
2574 """
2575 assert pending_ref.startswith('refs/'), pending_ref
2576 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2577 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2578 code = 0
2579 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002580 max_attempts = 3
2581 attempts_left = max_attempts
2582 while attempts_left:
2583 if attempts_left != max_attempts:
2584 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2585 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002586
2587 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002588 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002589 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002590 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002591 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002592 print 'Fetch failed with exit code %d.' % code
2593 if out.strip():
2594 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002595 continue
2596
2597 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002598 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002599 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002600 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002601 if code:
2602 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002603 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2604 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002605 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2606 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002607 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002608 return code, out
2609
2610 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002611 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002612 code, out = RunGitWithCode(
2613 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2614 if code == 0:
2615 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002616 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002617 return code, out
2618
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002619 print 'Push failed with exit code %d.' % code
2620 if out.strip():
2621 print out.strip()
2622 if IsFatalPushFailure(out):
2623 print (
2624 'Fatal push error. Make sure your .netrc credentials and git '
2625 'user.email are correct and you have push access to the repo.')
2626 return code, out
2627
2628 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002629 return code, out
2630
2631
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002632def IsFatalPushFailure(push_stdout):
2633 """True if retrying push won't help."""
2634 return '(prohibited by Gerrit)' in push_stdout
2635
2636
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002637@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002638def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002639 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002640 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002641 message = """This doesn't appear to be an SVN repository.
2642If your project has a git mirror with an upstream SVN master, you probably need
2643to run 'git svn init', see your project's git mirror documentation.
2644If your project has a true writeable upstream repository, you probably want
mark@chromium.org671c7a32014-07-31 17:09:36 +00002645to run 'git cl land' instead.
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002646Choose wisely, if you get this wrong, your commit might appear to succeed but
2647will instead be silently ignored."""
2648 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002649 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002650 return SendUpstream(parser, args, 'dcommit')
2651
2652
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002653@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002654def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002655 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002656 if settings.GetIsGitSvn():
2657 print('This appears to be an SVN repository.')
2658 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002659 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002660 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002661
2662
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002663@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002664def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002665 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002666 parser.add_option('-b', dest='newbranch',
2667 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002668 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002669 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002670 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2671 help='Change to the directory DIR immediately, '
2672 'before doing anything else.')
2673 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002674 help='failed patches spew .rej files rather than '
2675 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002676 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2677 help="don't commit after patch applies")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002678 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002679 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002680 auth_config = auth.extract_auth_config_from_options(options)
2681
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002682 if len(args) != 1:
2683 parser.print_help()
2684 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002685 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002686
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002687 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002688 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002689 return 1
2690
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002691 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002692 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002693
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002694 if options.newbranch:
2695 if options.force:
2696 RunGit(['branch', '-D', options.newbranch],
2697 stderr=subprocess2.PIPE, error_ok=True)
2698 RunGit(['checkout', '-b', options.newbranch,
2699 Changelist().GetUpstreamBranch()])
2700
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002701 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002702 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002703
2704
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002705def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00002706 # PatchIssue should never be called with a dirty tree. It is up to the
2707 # caller to check this, but just in case we assert here since the
2708 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002709 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002710
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002711 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002712 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002713 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002714 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002715 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002716 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002717 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002718 # Assume it's a URL to the patch. Default to https.
2719 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002720 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002721 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002722 DieWithError('Must pass an issue ID or full URL for '
2723 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002724 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002725 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002726 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002727
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002728 # Switch up to the top-level directory, if necessary, in preparation for
2729 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002730 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002731 if top:
2732 os.chdir(top)
2733
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002734 # Git patches have a/ at the beginning of source paths. We strip that out
2735 # with a sed script rather than the -p flag to patch so we can feed either
2736 # Git or svn-style patches into the same apply command.
2737 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002738 try:
2739 patch_data = subprocess2.check_output(
2740 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2741 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002742 DieWithError('Git patch mungling failed.')
2743 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002744
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002745 # We use "git apply" to apply the patch instead of "patch" so that we can
2746 # pick up file adds.
2747 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002748 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002749 if directory:
2750 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002751 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002752 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002753 elif IsGitVersionAtLeast('1.7.12'):
2754 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002755 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002756 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002757 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002758 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00002759 print 'Failed to apply the patch'
2760 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002761
2762 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002763 if not nocommit:
carlosk@chromium.org71284d92014-11-14 18:12:50 +00002764 RunGit(['commit', '-m', ('patch from issue %(i)s at patchset '
2765 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2766 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002767 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002768 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002769 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002770 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002771 else:
2772 print "Patch applied to index."
2773 return 0
2774
2775
2776def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002777 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002778 # Provide a wrapper for git svn rebase to help avoid accidental
2779 # git svn dcommit.
2780 # It's the only command that doesn't use parser at all since we just defer
2781 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002782
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002783 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002784
2785
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002786def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002787 """Fetches the tree status and returns either 'open', 'closed',
2788 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002789 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002790 if url:
2791 status = urllib2.urlopen(url).read().lower()
2792 if status.find('closed') != -1 or status == '0':
2793 return 'closed'
2794 elif status.find('open') != -1 or status == '1':
2795 return 'open'
2796 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002797 return 'unset'
2798
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002799
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002800def GetTreeStatusReason():
2801 """Fetches the tree status from a json url and returns the message
2802 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002803 url = settings.GetTreeStatusUrl()
2804 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002805 connection = urllib2.urlopen(json_url)
2806 status = json.loads(connection.read())
2807 connection.close()
2808 return status['message']
2809
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002810
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002811def GetBuilderMaster(bot_list):
2812 """For a given builder, fetch the master from AE if available."""
2813 map_url = 'https://builders-map.appspot.com/'
2814 try:
2815 master_map = json.load(urllib2.urlopen(map_url))
2816 except urllib2.URLError as e:
2817 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2818 (map_url, e))
2819 except ValueError as e:
2820 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2821 if not master_map:
2822 return None, 'Failed to build master map.'
2823
2824 result_master = ''
2825 for bot in bot_list:
2826 builder = bot.split(':', 1)[0]
2827 master_list = master_map.get(builder, [])
2828 if not master_list:
2829 return None, ('No matching master for builder %s.' % builder)
2830 elif len(master_list) > 1:
2831 return None, ('The builder name %s exists in multiple masters %s.' %
2832 (builder, master_list))
2833 else:
2834 cur_master = master_list[0]
2835 if not result_master:
2836 result_master = cur_master
2837 elif result_master != cur_master:
2838 return None, 'The builders do not belong to the same master.'
2839 return result_master, None
2840
2841
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002842def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002843 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002844 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002845 status = GetTreeStatus()
2846 if 'unset' == status:
2847 print 'You must configure your tree status URL by running "git cl config".'
2848 return 2
2849
2850 print "The tree is %s" % status
2851 print
2852 print GetTreeStatusReason()
2853 if status != 'open':
2854 return 1
2855 return 0
2856
2857
maruel@chromium.org15192402012-09-06 12:38:29 +00002858def CMDtry(parser, args):
2859 """Triggers a try job through Rietveld."""
2860 group = optparse.OptionGroup(parser, "Try job options")
2861 group.add_option(
2862 "-b", "--bot", action="append",
2863 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2864 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002865 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002866 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002867 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00002868 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002869 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002870 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002871 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002872 "-r", "--revision",
2873 help="Revision to use for the try job; default: the "
2874 "revision will be determined by the try server; see "
2875 "its waterfall for more info")
2876 group.add_option(
2877 "-c", "--clobber", action="store_true", default=False,
2878 help="Force a clobber before building; e.g. don't do an "
2879 "incremental build")
2880 group.add_option(
2881 "--project",
2882 help="Override which project to use. Projects are defined "
2883 "server-side to define what default bot set to use")
2884 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002885 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00002886 group.add_option(
2887 "--use-buildbucket", action="store_true", default=False,
2888 help="Use buildbucket to trigger try jobs.")
maruel@chromium.org15192402012-09-06 12:38:29 +00002889 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002890 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00002891 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002892 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00002893
2894 if args:
2895 parser.error('Unknown arguments: %s' % args)
2896
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002897 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00002898 if not cl.GetIssue():
2899 parser.error('Need to upload first')
2900
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002901 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002902 if props.get('closed'):
2903 parser.error('Cannot send tryjobs for a closed CL')
2904
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002905 if props.get('private'):
2906 parser.error('Cannot use trybots with private issue')
2907
maruel@chromium.org15192402012-09-06 12:38:29 +00002908 if not options.name:
2909 options.name = cl.GetBranch()
2910
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002911 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002912 options.master, err_msg = GetBuilderMaster(options.bot)
2913 if err_msg:
2914 parser.error('Tryserver master cannot be found because: %s\n'
2915 'Please manually specify the tryserver master'
2916 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002917
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002918 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002919 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002920 if not options.bot:
2921 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002922
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002923 # Get try masters from PRESUBMIT.py files.
2924 masters = presubmit_support.DoGetTryMasters(
2925 change,
2926 change.LocalPaths(),
2927 settings.GetRoot(),
2928 None,
2929 None,
2930 options.verbose,
2931 sys.stdout)
2932 if masters:
2933 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002934
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002935 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2936 options.bot = presubmit_support.DoGetTrySlaves(
2937 change,
2938 change.LocalPaths(),
2939 settings.GetRoot(),
2940 None,
2941 None,
2942 options.verbose,
2943 sys.stdout)
2944 if not options.bot:
2945 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002946
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002947 builders_and_tests = {}
2948 # TODO(machenbach): The old style command-line options don't support
2949 # multiple try masters yet.
2950 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2951 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2952
2953 for bot in old_style:
2954 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002955 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002956 elif ',' in bot:
2957 parser.error('Specify one bot per --bot flag')
2958 else:
2959 builders_and_tests.setdefault(bot, []).append('defaulttests')
2960
2961 for bot, tests in new_style:
2962 builders_and_tests.setdefault(bot, []).extend(tests)
2963
2964 # Return a master map with one master to be backwards compatible. The
2965 # master name defaults to an empty string, which will cause the master
2966 # not to be set on rietveld (deprecated).
2967 return {options.master: builders_and_tests}
2968
2969 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002970
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002971 for builders in masters.itervalues():
2972 if any('triggered' in b for b in builders):
2973 print >> sys.stderr, (
2974 'ERROR You are trying to send a job to a triggered bot. This type of'
2975 ' bot requires an\ninitial job from a parent (usually a builder). '
2976 'Instead send your job to the parent.\n'
2977 'Bot list: %s' % builders)
2978 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002979
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002980 patchset = cl.GetMostRecentPatchset()
2981 if patchset and patchset != cl.GetPatchset():
2982 print(
2983 '\nWARNING Mismatch between local config and server. Did a previous '
2984 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2985 'Continuing using\npatchset %s.\n' % patchset)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00002986 if options.use_buildbucket:
2987 try:
2988 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
2989 except BuildbucketResponseException as ex:
2990 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002991 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00002992 except Exception as e:
2993 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
2994 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
2995 e, stacktrace)
2996 return 1
2997 else:
2998 try:
2999 cl.RpcServer().trigger_distributed_try_jobs(
3000 cl.GetIssue(), patchset, options.name, options.clobber,
3001 options.revision, masters)
3002 except urllib2.HTTPError as e:
3003 if e.code == 404:
3004 print('404 from rietveld; '
3005 'did you mean to use "git try" instead of "git cl try"?')
3006 return 1
3007 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003008
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003009 for (master, builders) in sorted(masters.iteritems()):
3010 if master:
3011 print 'Master: %s' % master
3012 length = max(len(builder) for builder in builders)
3013 for builder in sorted(builders):
3014 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003015 return 0
3016
3017
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003018@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003019def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003020 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003021 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003022 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003023 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003024
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003025 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003026 if args:
3027 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003028 branch = cl.GetBranch()
3029 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003030 cl = Changelist()
3031 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003032
3033 # Clear configured merge-base, if there is one.
3034 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003035 else:
3036 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003037 return 0
3038
3039
thestig@chromium.org00858c82013-12-02 23:08:03 +00003040def CMDweb(parser, args):
3041 """Opens the current CL in the web browser."""
3042 _, args = parser.parse_args(args)
3043 if args:
3044 parser.error('Unrecognized args: %s' % ' '.join(args))
3045
3046 issue_url = Changelist().GetIssueURL()
3047 if not issue_url:
3048 print >> sys.stderr, 'ERROR No issue to open'
3049 return 1
3050
3051 webbrowser.open(issue_url)
3052 return 0
3053
3054
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003055def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003056 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003057 auth.add_auth_options(parser)
3058 options, args = parser.parse_args(args)
3059 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003060 if args:
3061 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003062 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003063 props = cl.GetIssueProperties()
3064 if props.get('private'):
3065 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003066 cl.SetFlag('commit', '1')
3067 return 0
3068
3069
groby@chromium.org411034a2013-02-26 15:12:01 +00003070def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003071 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003072 auth.add_auth_options(parser)
3073 options, args = parser.parse_args(args)
3074 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003075 if args:
3076 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003077 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003078 # Ensure there actually is an issue to close.
3079 cl.GetDescription()
3080 cl.CloseIssue()
3081 return 0
3082
3083
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003084def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003085 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003086 auth.add_auth_options(parser)
3087 options, args = parser.parse_args(args)
3088 auth_config = auth.extract_auth_config_from_options(options)
3089 if args:
3090 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003091
3092 # Uncommitted (staged and unstaged) changes will be destroyed by
3093 # "git reset --hard" if there are merging conflicts in PatchIssue().
3094 # Staged changes would be committed along with the patch from last
3095 # upload, hence counted toward the "last upload" side in the final
3096 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003097 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003098 return 1
3099
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003100 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003101 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003102 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003103 if not issue:
3104 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003105 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003106 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003107
3108 # Create a new branch based on the merge-base
3109 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3110 try:
3111 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003112 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003113 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003114 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003115 return rtn
3116
wychen@chromium.org06928532015-02-03 02:11:29 +00003117 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003118 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003119 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003120 finally:
3121 RunGit(['checkout', '-q', branch])
3122 RunGit(['branch', '-D', TMP_BRANCH])
3123
3124 return 0
3125
3126
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003127def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003128 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003129 parser.add_option(
3130 '--no-color',
3131 action='store_true',
3132 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003133 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003134 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003135 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003136
3137 author = RunGit(['config', 'user.email']).strip() or None
3138
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003139 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003140
3141 if args:
3142 if len(args) > 1:
3143 parser.error('Unknown args')
3144 base_branch = args[0]
3145 else:
3146 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003147 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003148
3149 change = cl.GetChange(base_branch, None)
3150 return owners_finder.OwnersFinder(
3151 [f.LocalPath() for f in
3152 cl.GetChange(base_branch, None).AffectedFiles()],
3153 change.RepositoryRoot(), author,
3154 fopen=file, os_path=os.path, glob=glob.glob,
3155 disable_color=options.no_color).run()
3156
3157
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003158def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
3159 """Generates a diff command."""
3160 # Generate diff for the current branch's changes.
3161 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3162 upstream_commit, '--' ]
3163
3164 if args:
3165 for arg in args:
3166 if os.path.isdir(arg):
3167 diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
3168 elif os.path.isfile(arg):
3169 diff_cmd.append(arg)
3170 else:
3171 DieWithError('Argument "%s" is not a file or a directory' % arg)
3172 else:
3173 diff_cmd.extend('*' + ext for ext in extensions)
3174
3175 return diff_cmd
3176
3177
enne@chromium.org555cfe42014-01-29 18:21:39 +00003178@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003179def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003180 """Runs clang-format on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003181 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003182 parser.add_option('--full', action='store_true',
3183 help='Reformat the full content of all touched files')
3184 parser.add_option('--dry-run', action='store_true',
3185 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003186 parser.add_option('--diff', action='store_true',
3187 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003188 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003189
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003190 # git diff generates paths against the root of the repository. Change
3191 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003192 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003193 if rel_base_path:
3194 os.chdir(rel_base_path)
3195
digit@chromium.org29e47272013-05-17 17:01:46 +00003196 # Grab the merge-base commit, i.e. the upstream commit of the current
3197 # branch when it was created or the last time it was rebased. This is
3198 # to cover the case where the user may have called "git fetch origin",
3199 # moving the origin branch to a newer commit, but hasn't rebased yet.
3200 upstream_commit = None
3201 cl = Changelist()
3202 upstream_branch = cl.GetUpstreamBranch()
3203 if upstream_branch:
3204 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3205 upstream_commit = upstream_commit.strip()
3206
3207 if not upstream_commit:
3208 DieWithError('Could not find base commit for this branch. '
3209 'Are you in detached state?')
3210
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003211 if opts.full:
3212 # Only list the names of modified files.
3213 clang_diff_type = '--name-only'
enne@chromium.org555cfe42014-01-29 18:21:39 +00003214 else:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003215 # Only generate context-less patches.
3216 clang_diff_type = '-U0'
3217
3218 diff_cmd = BuildGitDiffCmd(clang_diff_type, upstream_commit, args, CLANG_EXTS)
digit@chromium.org29e47272013-05-17 17:01:46 +00003219 diff_output = RunGit(diff_cmd)
3220
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003221 top_dir = os.path.normpath(
3222 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3223
3224 # Locate the clang-format binary in the checkout
3225 try:
3226 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3227 except clang_format.NotFoundError, e:
3228 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003229
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003230 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3231 # formatted. This is used to block during the presubmit.
3232 return_value = 0
3233
digit@chromium.org29e47272013-05-17 17:01:46 +00003234 if opts.full:
3235 # diff_output is a list of files to send to clang-format.
3236 files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003237 if files:
3238 cmd = [clang_format_tool]
3239 if not opts.dry_run and not opts.diff:
3240 cmd.append('-i')
3241 stdout = RunCommand(cmd + files, cwd=top_dir)
3242 if opts.diff:
3243 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003244 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003245 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003246 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00003247 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003248 try:
3249 script = clang_format.FindClangFormatScriptInChromiumTree(
3250 'clang-format-diff.py')
3251 except clang_format.NotFoundError, e:
3252 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003253
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003254 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003255 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003256 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003257
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003258 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003259 if opts.diff:
3260 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003261 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003262 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003263
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003264 # Build a diff command that only operates on dart files. dart's formatter
3265 # does not have the nice property of only operating on modified chunks, so
3266 # hard code full.
3267 dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3268 args, ['.dart'])
3269 dart_diff_output = RunGit(dart_diff_cmd)
3270 if dart_diff_output:
3271 try:
3272 command = [dart_format.FindDartFmtToolInChromiumTree()]
3273 if not opts.dry_run and not opts.diff:
3274 command.append('-w')
3275 command.extend(dart_diff_output.splitlines())
3276
3277 stdout = RunCommand(command, cwd=top_dir, env=env)
3278 if opts.dry_run and stdout:
3279 return_value = 2
3280 except dart_format.NotFoundError as e:
3281 print ('Unable to check dart code formatting. Dart SDK is not in ' +
3282 'this checkout.')
3283
3284 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003285
3286
maruel@chromium.org29404b52014-09-08 22:58:00 +00003287def CMDlol(parser, args):
3288 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003289 print zlib.decompress(base64.b64decode(
3290 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3291 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3292 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3293 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003294 return 0
3295
3296
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003297class OptionParser(optparse.OptionParser):
3298 """Creates the option parse and add --verbose support."""
3299 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003300 optparse.OptionParser.__init__(
3301 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003302 self.add_option(
3303 '-v', '--verbose', action='count', default=0,
3304 help='Use 2 times for more debugging info')
3305
3306 def parse_args(self, args=None, values=None):
3307 options, args = optparse.OptionParser.parse_args(self, args, values)
3308 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3309 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3310 return options, args
3311
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003312
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003313def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003314 if sys.hexversion < 0x02060000:
3315 print >> sys.stderr, (
3316 '\nYour python version %s is unsupported, please upgrade.\n' %
3317 sys.version.split(' ', 1)[0])
3318 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003319
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003320 # Reload settings.
3321 global settings
3322 settings = Settings()
3323
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003324 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003325 dispatcher = subcommand.CommandDispatcher(__name__)
3326 try:
3327 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003328 except auth.AuthenticationError as e:
3329 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003330 except urllib2.HTTPError, e:
3331 if e.code != 500:
3332 raise
3333 DieWithError(
3334 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3335 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003336 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003337
3338
3339if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003340 # These affect sys.stdout so do it outside of main() to simplify mocks in
3341 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003342 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003343 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003344 try:
3345 sys.exit(main(sys.argv[1:]))
3346 except KeyboardInterrupt:
3347 sys.stderr.write('interrupted\n')
3348 sys.exit(1)