blob: f7192541f8769d793df98a84dbf2b10c288964ac [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
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000014import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000015import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000016import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import logging
18import optparse
19import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000020import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000024import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import time
27import traceback
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000029import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000030import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000031import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000032
33try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000034 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000035except ImportError:
36 pass
37
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000038from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000039from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000040from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000041import auth
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000043import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000044import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000045import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000046import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000047import git_common
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +000048from git_footers import get_footer_svn_id
piman@chromium.org336f9122014-09-04 02:16:55 +000049import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000050import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000051import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000052import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000053import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000054import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000055import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import watchlists
57
maruel@chromium.org0633fb42013-08-16 20:06:14 +000058__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000060DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000061POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000062DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000063GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000064CHANGE_ID = 'Change-Id:'
rmistry@google.comc68112d2015-03-03 12:48:06 +000065REFS_THAT_ALIAS_TO_OTHER_REFS = {
66 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
67 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
68}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069
sheyang@google.com6ebaf782015-05-12 19:17:54 +000070# Buildbucket-related constants
71BUILDBUCKET_HOST = 'cr-buildbucket.appspot.com'
72
thestig@chromium.org44202a22014-03-11 19:22:18 +000073# Valid extensions for files we want to lint.
74DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
75DEFAULT_LINT_IGNORE_REGEX = r"$^"
76
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000077# Shortcut since it quickly becomes redundant.
78Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000079
maruel@chromium.orgddd59412011-11-30 14:20:38 +000080# Initialized in main()
81settings = None
82
83
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000084def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000085 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086 sys.exit(1)
87
88
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000089def GetNoGitPagerEnv():
90 env = os.environ.copy()
91 # 'cat' is a magical git string that disables pagers on all platforms.
92 env['GIT_PAGER'] = 'cat'
93 return env
94
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000095
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000096def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000097 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000098 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000099 except subprocess2.CalledProcessError as e:
100 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000101 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000103 'Command "%s" failed.\n%s' % (
104 ' '.join(args), error_message or e.stdout or ''))
105 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000106
107
108def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000109 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000110 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000111
112
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000113def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000114 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000115 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000116 if suppress_stderr:
117 stderr = subprocess2.VOID
118 else:
119 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000120 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000121 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000122 stdout=subprocess2.PIPE,
123 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000124 return code, out[0]
125 except ValueError:
126 # When the subprocess fails, it returns None. That triggers a ValueError
127 # when trying to unpack the return value into (out, code).
128 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000129
130
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000131def RunGitSilent(args):
132 """Returns stdout, suppresses stderr and ingores the return code."""
133 return RunGitWithCode(args, suppress_stderr=True)[1]
134
135
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000136def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000137 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000138 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000139 return (version.startswith(prefix) and
140 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000141
142
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000143def BranchExists(branch):
144 """Return True if specified branch exists."""
145 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
146 suppress_stderr=True)
147 return not code
148
149
maruel@chromium.org90541732011-04-01 17:54:18 +0000150def ask_for_data(prompt):
151 try:
152 return raw_input(prompt)
153 except KeyboardInterrupt:
154 # Hide the exception.
155 sys.exit(1)
156
157
iannucci@chromium.org79540052012-10-19 23:15:26 +0000158def git_set_branch_value(key, value):
159 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000160 if not branch:
161 return
162
163 cmd = ['config']
164 if isinstance(value, int):
165 cmd.append('--int')
166 git_key = 'branch.%s.%s' % (branch, key)
167 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000168
169
170def git_get_branch_default(key, default):
171 branch = Changelist().GetBranch()
172 if branch:
173 git_key = 'branch.%s.%s' % (branch, key)
174 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
175 try:
176 return int(stdout.strip())
177 except ValueError:
178 pass
179 return default
180
181
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000182def add_git_similarity(parser):
183 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000184 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000185 help='Sets the percentage that a pair of files need to match in order to'
186 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000187 parser.add_option(
188 '--find-copies', action='store_true',
189 help='Allows git to look for copies.')
190 parser.add_option(
191 '--no-find-copies', action='store_false', dest='find_copies',
192 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000193
194 old_parser_args = parser.parse_args
195 def Parse(args):
196 options, args = old_parser_args(args)
197
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000198 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000199 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000200 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000201 print('Note: Saving similarity of %d%% in git config.'
202 % options.similarity)
203 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000204
iannucci@chromium.org79540052012-10-19 23:15:26 +0000205 options.similarity = max(0, min(options.similarity, 100))
206
207 if options.find_copies is None:
208 options.find_copies = bool(
209 git_get_branch_default('git-find-copies', True))
210 else:
211 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000212
213 print('Using %d%% similarity for rename/copy detection. '
214 'Override with --similarity.' % options.similarity)
215
216 return options, args
217 parser.parse_args = Parse
218
219
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000220def _prefix_master(master):
221 """Convert user-specified master name to full master name.
222
223 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
224 name, while the developers always use shortened master name
225 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
226 function does the conversion for buildbucket migration.
227 """
228 prefix = 'master.'
229 if master.startswith(prefix):
230 return master
231 return '%s%s' % (prefix, master)
232
233
machenbach@chromium.org79e43ff2015-05-15 05:56:13 +0000234def trigger_try_jobs(auth_config, changelist, options, masters, category,
235 override_properties=None):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000236 rietveld_url = settings.GetDefaultServerUrl()
237 rietveld_host = urlparse.urlparse(rietveld_url).hostname
238 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
239 http = authenticator.authorize(httplib2.Http())
240 http.force_exception_to_status_code = True
241 issue_props = changelist.GetIssueProperties()
242 issue = changelist.GetIssue()
243 patchset = changelist.GetMostRecentPatchset()
244
245 buildbucket_put_url = (
246 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
247 hostname=BUILDBUCKET_HOST))
248 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
249 hostname=rietveld_host,
250 issue=issue,
251 patch=patchset)
252
253 batch_req_body = {'builds': []}
254 print_text = []
255 print_text.append('Tried jobs on:')
256 for master, builders_and_tests in sorted(masters.iteritems()):
257 print_text.append('Master: %s' % master)
258 bucket = _prefix_master(master)
259 for builder, tests in sorted(builders_and_tests.iteritems()):
260 print_text.append(' %s: %s' % (builder, tests))
261 parameters = {
262 'builder_name': builder,
263 'changes': [
264 {'author': {'email': issue_props['owner_email']}},
265 ],
266 'properties': {
267 'category': category,
268 'issue': issue,
269 'master': master,
270 'patch_project': issue_props['project'],
271 'patch_storage': 'rietveld',
272 'patchset': patchset,
273 'reason': options.name,
274 'revision': options.revision,
275 'rietveld': rietveld_url,
276 'testfilter': tests,
277 },
278 }
machenbach@chromium.org79e43ff2015-05-15 05:56:13 +0000279 if override_properties:
280 parameters['properties'].update(override_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000281 if options.clobber:
282 parameters['properties']['clobber'] = True
283 batch_req_body['builds'].append(
284 {
285 'bucket': bucket,
286 'parameters_json': json.dumps(parameters),
287 'tags': ['builder:%s' % builder,
288 'buildset:%s' % buildset,
289 'master:%s' % master,
290 'user_agent:git_cl_try']
291 }
292 )
293
294 for try_count in xrange(3):
295 response, content = http.request(
296 buildbucket_put_url,
297 'PUT',
298 body=json.dumps(batch_req_body),
299 headers={'Content-Type': 'application/json'},
300 )
301 content_json = None
302 try:
303 content_json = json.loads(content)
304 except ValueError:
305 pass
306
307 # Buildbucket could return an error even if status==200.
308 if content_json and content_json.get('error'):
309 msg = 'Error in response. Code: %d. Reason: %s. Message: %s.' % (
310 content_json['error'].get('code', ''),
311 content_json['error'].get('reason', ''),
312 content_json['error'].get('message', ''))
313 raise BuildbucketResponseException(msg)
314
315 if response.status == 200:
316 if not content_json:
317 raise BuildbucketResponseException(
318 'Buildbucket returns invalid json content: %s.\n'
319 'Please file bugs at crbug.com, label "Infra-BuildBucket".' %
320 content)
321 break
322 if response.status < 500 or try_count >= 2:
323 raise httplib2.HttpLib2Error(content)
324
325 # status >= 500 means transient failures.
326 logging.debug('Transient errors when triggering tryjobs. Will retry.')
327 time.sleep(0.5 + 1.5*try_count)
328
329 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000330
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000331
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000332def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
333 """Return the corresponding git ref if |base_url| together with |glob_spec|
334 matches the full |url|.
335
336 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
337 """
338 fetch_suburl, as_ref = glob_spec.split(':')
339 if allow_wildcards:
340 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
341 if glob_match:
342 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
343 # "branches/{472,597,648}/src:refs/remotes/svn/*".
344 branch_re = re.escape(base_url)
345 if glob_match.group(1):
346 branch_re += '/' + re.escape(glob_match.group(1))
347 wildcard = glob_match.group(2)
348 if wildcard == '*':
349 branch_re += '([^/]*)'
350 else:
351 # Escape and replace surrounding braces with parentheses and commas
352 # with pipe symbols.
353 wildcard = re.escape(wildcard)
354 wildcard = re.sub('^\\\\{', '(', wildcard)
355 wildcard = re.sub('\\\\,', '|', wildcard)
356 wildcard = re.sub('\\\\}$', ')', wildcard)
357 branch_re += wildcard
358 if glob_match.group(3):
359 branch_re += re.escape(glob_match.group(3))
360 match = re.match(branch_re, url)
361 if match:
362 return re.sub('\*$', match.group(1), as_ref)
363
364 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
365 if fetch_suburl:
366 full_url = base_url + '/' + fetch_suburl
367 else:
368 full_url = base_url
369 if full_url == url:
370 return as_ref
371 return None
372
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000373
iannucci@chromium.org79540052012-10-19 23:15:26 +0000374def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000375 """Prints statistics about the change to the user."""
376 # --no-ext-diff is broken in some versions of Git, so try to work around
377 # this by overriding the environment (but there is still a problem if the
378 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000379 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000380 if 'GIT_EXTERNAL_DIFF' in env:
381 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000382
383 if find_copies:
384 similarity_options = ['--find-copies-harder', '-l100000',
385 '-C%s' % similarity]
386 else:
387 similarity_options = ['-M%s' % similarity]
388
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000389 try:
390 stdout = sys.stdout.fileno()
391 except AttributeError:
392 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000393 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000394 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000395 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000396 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000397
398
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000399class BuildbucketResponseException(Exception):
400 pass
401
402
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000403class Settings(object):
404 def __init__(self):
405 self.default_server = None
406 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000407 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000408 self.is_git_svn = None
409 self.svn_branch = None
410 self.tree_status_url = None
411 self.viewvc_url = None
412 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000413 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000414 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000415 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000416 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000417 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000418
419 def LazyUpdateIfNeeded(self):
420 """Updates the settings from a codereview.settings file, if available."""
421 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000422 # The only value that actually changes the behavior is
423 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000424 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000425 error_ok=True
426 ).strip().lower()
427
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000428 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000429 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000430 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000431 # set updated to True to avoid infinite calling loop
432 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000433 self.updated = True
434 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000435 self.updated = True
436
437 def GetDefaultServerUrl(self, error_ok=False):
438 if not self.default_server:
439 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000440 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000441 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000442 if error_ok:
443 return self.default_server
444 if not self.default_server:
445 error_message = ('Could not find settings file. You must configure '
446 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000447 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000448 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000449 return self.default_server
450
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000451 @staticmethod
452 def GetRelativeRoot():
453 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000454
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000455 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000456 if self.root is None:
457 self.root = os.path.abspath(self.GetRelativeRoot())
458 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000459
460 def GetIsGitSvn(self):
461 """Return true if this repo looks like it's using git-svn."""
462 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000463 if self.GetPendingRefPrefix():
464 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
465 self.is_git_svn = False
466 else:
467 # If you have any "svn-remote.*" config keys, we think you're using svn.
468 self.is_git_svn = RunGitWithCode(
469 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000470 return self.is_git_svn
471
472 def GetSVNBranch(self):
473 if self.svn_branch is None:
474 if not self.GetIsGitSvn():
475 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
476
477 # Try to figure out which remote branch we're based on.
478 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000479 # 1) iterate through our branch history and find the svn URL.
480 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000481
482 # regexp matching the git-svn line that contains the URL.
483 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
484
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000485 # We don't want to go through all of history, so read a line from the
486 # pipe at a time.
487 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000488 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000489 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
490 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000491 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000492 for line in proc.stdout:
493 match = git_svn_re.match(line)
494 if match:
495 url = match.group(1)
496 proc.stdout.close() # Cut pipe.
497 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000498
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000499 if url:
500 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
501 remotes = RunGit(['config', '--get-regexp',
502 r'^svn-remote\..*\.url']).splitlines()
503 for remote in remotes:
504 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000505 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000506 remote = match.group(1)
507 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000508 rewrite_root = RunGit(
509 ['config', 'svn-remote.%s.rewriteRoot' % remote],
510 error_ok=True).strip()
511 if rewrite_root:
512 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000513 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000514 ['config', 'svn-remote.%s.fetch' % remote],
515 error_ok=True).strip()
516 if fetch_spec:
517 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
518 if self.svn_branch:
519 break
520 branch_spec = RunGit(
521 ['config', 'svn-remote.%s.branches' % remote],
522 error_ok=True).strip()
523 if branch_spec:
524 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
525 if self.svn_branch:
526 break
527 tag_spec = RunGit(
528 ['config', 'svn-remote.%s.tags' % remote],
529 error_ok=True).strip()
530 if tag_spec:
531 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
532 if self.svn_branch:
533 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000534
535 if not self.svn_branch:
536 DieWithError('Can\'t guess svn branch -- try specifying it on the '
537 'command line')
538
539 return self.svn_branch
540
541 def GetTreeStatusUrl(self, error_ok=False):
542 if not self.tree_status_url:
543 error_message = ('You must configure your tree status URL by running '
544 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000545 self.tree_status_url = self._GetRietveldConfig(
546 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000547 return self.tree_status_url
548
549 def GetViewVCUrl(self):
550 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000551 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000552 return self.viewvc_url
553
rmistry@google.com90752582014-01-14 21:04:50 +0000554 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000555 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000556
rmistry@google.com5626a922015-02-26 14:03:30 +0000557 def GetRunPostUploadHook(self):
558 run_post_upload_hook = self._GetRietveldConfig(
559 'run-post-upload-hook', error_ok=True)
560 return run_post_upload_hook == "True"
561
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000562 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000563 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000564
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000565 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000566 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000567
ukai@chromium.orge8077812012-02-03 03:41:46 +0000568 def GetIsGerrit(self):
569 """Return true if this repo is assosiated with gerrit code review system."""
570 if self.is_gerrit is None:
571 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
572 return self.is_gerrit
573
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000574 def GetGitEditor(self):
575 """Return the editor specified in the git config, or None if none is."""
576 if self.git_editor is None:
577 self.git_editor = self._GetConfig('core.editor', error_ok=True)
578 return self.git_editor or None
579
thestig@chromium.org44202a22014-03-11 19:22:18 +0000580 def GetLintRegex(self):
581 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
582 DEFAULT_LINT_REGEX)
583
584 def GetLintIgnoreRegex(self):
585 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
586 DEFAULT_LINT_IGNORE_REGEX)
587
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000588 def GetProject(self):
589 if not self.project:
590 self.project = self._GetRietveldConfig('project', error_ok=True)
591 return self.project
592
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000593 def GetForceHttpsCommitUrl(self):
594 if not self.force_https_commit_url:
595 self.force_https_commit_url = self._GetRietveldConfig(
596 'force-https-commit-url', error_ok=True)
597 return self.force_https_commit_url
598
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000599 def GetPendingRefPrefix(self):
600 if not self.pending_ref_prefix:
601 self.pending_ref_prefix = self._GetRietveldConfig(
602 'pending-ref-prefix', error_ok=True)
603 return self.pending_ref_prefix
604
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000605 def _GetRietveldConfig(self, param, **kwargs):
606 return self._GetConfig('rietveld.' + param, **kwargs)
607
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 def _GetConfig(self, param, **kwargs):
609 self.LazyUpdateIfNeeded()
610 return RunGit(['config', param], **kwargs).strip()
611
612
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000613def ShortBranchName(branch):
614 """Convert a name like 'refs/heads/foo' to just 'foo'."""
615 return branch.replace('refs/heads/', '')
616
617
618class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000619 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000620 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000621 global settings
622 if not settings:
623 # Happens when git_cl.py is used as a utility library.
624 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000625 settings.GetDefaultServerUrl()
626 self.branchref = branchref
627 if self.branchref:
628 self.branch = ShortBranchName(self.branchref)
629 else:
630 self.branch = None
631 self.rietveld_server = None
632 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000633 self.lookedup_issue = False
634 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000635 self.has_description = False
636 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000637 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000638 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000639 self.cc = None
640 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000641 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000642 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000643 self._remote = None
644 self._rpc_server = None
645
646 @property
647 def auth_config(self):
648 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000649
650 def GetCCList(self):
651 """Return the users cc'd on this CL.
652
653 Return is a string suitable for passing to gcl with the --cc flag.
654 """
655 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000656 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000657 more_cc = ','.join(self.watchers)
658 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
659 return self.cc
660
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000661 def GetCCListWithoutDefault(self):
662 """Return the users cc'd on this CL excluding default ones."""
663 if self.cc is None:
664 self.cc = ','.join(self.watchers)
665 return self.cc
666
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000667 def SetWatchers(self, watchers):
668 """Set the list of email addresses that should be cc'd based on the changed
669 files in this CL.
670 """
671 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000672
673 def GetBranch(self):
674 """Returns the short branch name, e.g. 'master'."""
675 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000676 branchref = RunGit(['symbolic-ref', 'HEAD'],
677 stderr=subprocess2.VOID, error_ok=True).strip()
678 if not branchref:
679 return None
680 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000681 self.branch = ShortBranchName(self.branchref)
682 return self.branch
683
684 def GetBranchRef(self):
685 """Returns the full branch name, e.g. 'refs/heads/master'."""
686 self.GetBranch() # Poke the lazy loader.
687 return self.branchref
688
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000689 @staticmethod
690 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000691 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000692 e.g. 'origin', 'refs/heads/master'
693 """
694 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000695 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
696 error_ok=True).strip()
697 if upstream_branch:
698 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
699 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000700 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
701 error_ok=True).strip()
702 if upstream_branch:
703 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000704 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000705 # Fall back on trying a git-svn upstream branch.
706 if settings.GetIsGitSvn():
707 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000708 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000709 # Else, try to guess the origin remote.
710 remote_branches = RunGit(['branch', '-r']).split()
711 if 'origin/master' in remote_branches:
712 # Fall back on origin/master if it exits.
713 remote = 'origin'
714 upstream_branch = 'refs/heads/master'
715 elif 'origin/trunk' in remote_branches:
716 # Fall back on origin/trunk if it exists. Generally a shared
717 # git-svn clone
718 remote = 'origin'
719 upstream_branch = 'refs/heads/trunk'
720 else:
721 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000722Either pass complete "git diff"-style arguments, like
723 git cl upload origin/master
724or verify this branch is set up to track another (via the --track argument to
725"git checkout -b ...").""")
726
727 return remote, upstream_branch
728
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000729 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000730 upstream_branch = self.GetUpstreamBranch()
731 if not BranchExists(upstream_branch):
732 DieWithError('The upstream for the current branch (%s) does not exist '
733 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000734 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000735 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000736
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000737 def GetUpstreamBranch(self):
738 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000739 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000740 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000741 upstream_branch = upstream_branch.replace('refs/heads/',
742 'refs/remotes/%s/' % remote)
743 upstream_branch = upstream_branch.replace('refs/branch-heads/',
744 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000745 self.upstream_branch = upstream_branch
746 return self.upstream_branch
747
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000748 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000749 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000750 remote, branch = None, self.GetBranch()
751 seen_branches = set()
752 while branch not in seen_branches:
753 seen_branches.add(branch)
754 remote, branch = self.FetchUpstreamTuple(branch)
755 branch = ShortBranchName(branch)
756 if remote != '.' or branch.startswith('refs/remotes'):
757 break
758 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000759 remotes = RunGit(['remote'], error_ok=True).split()
760 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000761 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000762 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000763 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000764 logging.warning('Could not determine which remote this change is '
765 'associated with, so defaulting to "%s". This may '
766 'not be what you want. You may prevent this message '
767 'by running "git svn info" as documented here: %s',
768 self._remote,
769 GIT_INSTRUCTIONS_URL)
770 else:
771 logging.warn('Could not determine which remote this change is '
772 'associated with. You may prevent this message by '
773 'running "git svn info" as documented here: %s',
774 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000775 branch = 'HEAD'
776 if branch.startswith('refs/remotes'):
777 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000778 elif branch.startswith('refs/branch-heads/'):
779 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000780 else:
781 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000782 return self._remote
783
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000784 def GitSanityChecks(self, upstream_git_obj):
785 """Checks git repo status and ensures diff is from local commits."""
786
sbc@chromium.org79706062015-01-14 21:18:12 +0000787 if upstream_git_obj is None:
788 if self.GetBranch() is None:
789 print >> sys.stderr, (
790 'ERROR: unable to dertermine current branch (detached HEAD?)')
791 else:
792 print >> sys.stderr, (
793 'ERROR: no upstream branch')
794 return False
795
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000796 # Verify the commit we're diffing against is in our current branch.
797 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
798 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
799 if upstream_sha != common_ancestor:
800 print >> sys.stderr, (
801 'ERROR: %s is not in the current branch. You may need to rebase '
802 'your tracking branch' % upstream_sha)
803 return False
804
805 # List the commits inside the diff, and verify they are all local.
806 commits_in_diff = RunGit(
807 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
808 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
809 remote_branch = remote_branch.strip()
810 if code != 0:
811 _, remote_branch = self.GetRemoteBranch()
812
813 commits_in_remote = RunGit(
814 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
815
816 common_commits = set(commits_in_diff) & set(commits_in_remote)
817 if common_commits:
818 print >> sys.stderr, (
819 'ERROR: Your diff contains %d commits already in %s.\n'
820 'Run "git log --oneline %s..HEAD" to get a list of commits in '
821 'the diff. If you are using a custom git flow, you can override'
822 ' the reference used for this check with "git config '
823 'gitcl.remotebranch <git-ref>".' % (
824 len(common_commits), remote_branch, upstream_git_obj))
825 return False
826 return True
827
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000828 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000829 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000830
831 Returns None if it is not set.
832 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000833 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
834 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000835
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000836 def GetGitSvnRemoteUrl(self):
837 """Return the configured git-svn remote URL parsed from git svn info.
838
839 Returns None if it is not set.
840 """
841 # URL is dependent on the current directory.
842 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
843 if data:
844 keys = dict(line.split(': ', 1) for line in data.splitlines()
845 if ': ' in line)
846 return keys.get('URL', None)
847 return None
848
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000849 def GetRemoteUrl(self):
850 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
851
852 Returns None if there is no remote.
853 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000854 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000855 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
856
857 # If URL is pointing to a local directory, it is probably a git cache.
858 if os.path.isdir(url):
859 url = RunGit(['config', 'remote.%s.url' % remote],
860 error_ok=True,
861 cwd=url).strip()
862 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000863
864 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000865 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000866 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000867 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000868 self.issue = int(issue) or None if issue else None
869 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000870 return self.issue
871
872 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000873 if not self.rietveld_server:
874 # If we're on a branch then get the server potentially associated
875 # with that branch.
876 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000877 rietveld_server_config = self._RietveldServer()
878 if rietveld_server_config:
879 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
880 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000881 if not self.rietveld_server:
882 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000883 return self.rietveld_server
884
885 def GetIssueURL(self):
886 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000887 if not self.GetIssue():
888 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000889 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
890
891 def GetDescription(self, pretty=False):
892 if not self.has_description:
893 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000894 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000895 try:
896 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000897 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000898 if e.code == 404:
899 DieWithError(
900 ('\nWhile fetching the description for issue %d, received a '
901 '404 (not found)\n'
902 'error. It is likely that you deleted this '
903 'issue on the server. If this is the\n'
904 'case, please run\n\n'
905 ' git cl issue 0\n\n'
906 'to clear the association with the deleted issue. Then run '
907 'this command again.') % issue)
908 else:
909 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000910 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000911 except urllib2.URLError as e:
912 print >> sys.stderr, (
913 'Warning: Failed to retrieve CL description due to network '
914 'failure.')
915 self.description = ''
916
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000917 self.has_description = True
918 if pretty:
919 wrapper = textwrap.TextWrapper()
920 wrapper.initial_indent = wrapper.subsequent_indent = ' '
921 return wrapper.fill(self.description)
922 return self.description
923
924 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000925 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000926 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000927 patchset = RunGit(['config', self._PatchsetSetting()],
928 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000929 self.patchset = int(patchset) or None if patchset else None
930 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000931 return self.patchset
932
933 def SetPatchset(self, patchset):
934 """Set this branch's patchset. If patchset=0, clears the patchset."""
935 if patchset:
936 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000937 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938 else:
939 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000940 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000941 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000942
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000943 def GetMostRecentPatchset(self):
944 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000945
946 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000947 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000948 '/download/issue%s_%s.diff' % (issue, patchset))
949
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000950 def GetIssueProperties(self):
951 if self._props is None:
952 issue = self.GetIssue()
953 if not issue:
954 self._props = {}
955 else:
956 self._props = self.RpcServer().get_issue_properties(issue, True)
957 return self._props
958
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000959 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000960 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000961
apavlov@chromium.orge4efd512014-11-05 09:05:29 +0000962 def AddComment(self, message):
963 return self.RpcServer().add_comment(self.GetIssue(), message)
964
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000965 def SetIssue(self, issue):
966 """Set this branch's issue. If issue=0, clears the issue."""
967 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000968 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000969 RunGit(['config', self._IssueSetting(), str(issue)])
970 if self.rietveld_server:
971 RunGit(['config', self._RietveldServer(), self.rietveld_server])
972 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000973 current_issue = self.GetIssue()
974 if current_issue:
975 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000976 self.issue = None
977 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000978
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000979 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000980 if not self.GitSanityChecks(upstream_branch):
981 DieWithError('\nGit sanity check failure')
982
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000983 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000984 if not root:
985 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000986 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000987
988 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000989 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000990 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000991 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000992 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000993 except subprocess2.CalledProcessError:
994 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000995 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000996 'This branch probably doesn\'t exist anymore. To reset the\n'
997 'tracking branch, please run\n'
998 ' git branch --set-upstream %s trunk\n'
999 'replacing trunk with origin/master or the relevant branch') %
1000 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001001
maruel@chromium.org52424302012-08-29 15:14:30 +00001002 issue = self.GetIssue()
1003 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001004 if issue:
1005 description = self.GetDescription()
1006 else:
1007 # If the change was never uploaded, use the log messages of all commits
1008 # up to the branch point, as git cl upload will prefill the description
1009 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001010 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1011 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001012
1013 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001014 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001015 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001016 name,
1017 description,
1018 absroot,
1019 files,
1020 issue,
1021 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001022 author,
1023 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001024
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001025 def GetStatus(self):
1026 """Apply a rough heuristic to give a simple summary of an issue's review
1027 or CQ status, assuming adherence to a common workflow.
1028
1029 Returns None if no issue for this branch, or one of the following keywords:
1030 * 'error' - error from review tool (including deleted issues)
1031 * 'unsent' - not sent for review
1032 * 'waiting' - waiting for review
1033 * 'reply' - waiting for owner to reply to review
1034 * 'lgtm' - LGTM from at least one approved reviewer
1035 * 'commit' - in the commit queue
1036 * 'closed' - closed
1037 """
1038 if not self.GetIssue():
1039 return None
1040
1041 try:
1042 props = self.GetIssueProperties()
1043 except urllib2.HTTPError:
1044 return 'error'
1045
1046 if props.get('closed'):
1047 # Issue is closed.
1048 return 'closed'
1049 if props.get('commit'):
1050 # Issue is in the commit queue.
1051 return 'commit'
1052
1053 try:
1054 reviewers = self.GetApprovingReviewers()
1055 except urllib2.HTTPError:
1056 return 'error'
1057
1058 if reviewers:
1059 # Was LGTM'ed.
1060 return 'lgtm'
1061
1062 messages = props.get('messages') or []
1063
1064 if not messages:
1065 # No message was sent.
1066 return 'unsent'
1067 if messages[-1]['sender'] != props.get('owner_email'):
1068 # Non-LGTM reply from non-owner
1069 return 'reply'
1070 return 'waiting'
1071
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001072 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001073 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001074
1075 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001076 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001077 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001078 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001079 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001080 except presubmit_support.PresubmitFailure, e:
1081 DieWithError(
1082 ('%s\nMaybe your depot_tools is out of date?\n'
1083 'If all fails, contact maruel@') % e)
1084
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001085 def UpdateDescription(self, description):
1086 self.description = description
1087 return self.RpcServer().update_description(
1088 self.GetIssue(), self.description)
1089
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001091 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001092 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001093
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001094 def SetFlag(self, flag, value):
1095 """Patchset must match."""
1096 if not self.GetPatchset():
1097 DieWithError('The patchset needs to match. Send another patchset.')
1098 try:
1099 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001100 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001101 except urllib2.HTTPError, e:
1102 if e.code == 404:
1103 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1104 if e.code == 403:
1105 DieWithError(
1106 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1107 'match?') % (self.GetIssue(), self.GetPatchset()))
1108 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001109
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001110 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001111 """Returns an upload.RpcServer() to access this review's rietveld instance.
1112 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001113 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001114 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001115 self.GetRietveldServer(),
1116 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001117 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001118
1119 def _IssueSetting(self):
1120 """Return the git setting that stores this change's issue."""
1121 return 'branch.%s.rietveldissue' % self.GetBranch()
1122
1123 def _PatchsetSetting(self):
1124 """Return the git setting that stores this change's most recent patchset."""
1125 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1126
1127 def _RietveldServer(self):
1128 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001129 branch = self.GetBranch()
1130 if branch:
1131 return 'branch.%s.rietveldserver' % branch
1132 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001133
1134
1135def GetCodereviewSettingsInteractively():
1136 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001137 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 server = settings.GetDefaultServerUrl(error_ok=True)
1139 prompt = 'Rietveld server (host[:port])'
1140 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001141 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142 if not server and not newserver:
1143 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001144 if newserver:
1145 newserver = gclient_utils.UpgradeToHttps(newserver)
1146 if newserver != server:
1147 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001149 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001150 prompt = caption
1151 if initial:
1152 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001153 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 if new_val == 'x':
1155 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001156 elif new_val:
1157 if is_url:
1158 new_val = gclient_utils.UpgradeToHttps(new_val)
1159 if new_val != initial:
1160 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001161
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001162 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001163 SetProperty(settings.GetDefaultPrivateFlag(),
1164 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001166 'tree-status-url', False)
1167 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001168 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001169 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1170 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001171
1172 # TODO: configure a default branch to diff against, rather than this
1173 # svn-based hackery.
1174
1175
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001176class ChangeDescription(object):
1177 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001178 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001179 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001180
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001181 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001182 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001183
agable@chromium.org42c20792013-09-12 17:34:49 +00001184 @property # www.logilab.org/ticket/89786
1185 def description(self): # pylint: disable=E0202
1186 return '\n'.join(self._description_lines)
1187
1188 def set_description(self, desc):
1189 if isinstance(desc, basestring):
1190 lines = desc.splitlines()
1191 else:
1192 lines = [line.rstrip() for line in desc]
1193 while lines and not lines[0]:
1194 lines.pop(0)
1195 while lines and not lines[-1]:
1196 lines.pop(-1)
1197 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001198
piman@chromium.org336f9122014-09-04 02:16:55 +00001199 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001200 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001201 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001202 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001203 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001204 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001205
agable@chromium.org42c20792013-09-12 17:34:49 +00001206 # Get the set of R= and TBR= lines and remove them from the desciption.
1207 regexp = re.compile(self.R_LINE)
1208 matches = [regexp.match(line) for line in self._description_lines]
1209 new_desc = [l for i, l in enumerate(self._description_lines)
1210 if not matches[i]]
1211 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001212
agable@chromium.org42c20792013-09-12 17:34:49 +00001213 # Construct new unified R= and TBR= lines.
1214 r_names = []
1215 tbr_names = []
1216 for match in matches:
1217 if not match:
1218 continue
1219 people = cleanup_list([match.group(2).strip()])
1220 if match.group(1) == 'TBR':
1221 tbr_names.extend(people)
1222 else:
1223 r_names.extend(people)
1224 for name in r_names:
1225 if name not in reviewers:
1226 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001227 if add_owners_tbr:
1228 owners_db = owners.Database(change.RepositoryRoot(),
1229 fopen=file, os_path=os.path, glob=glob.glob)
1230 all_reviewers = set(tbr_names + reviewers)
1231 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1232 all_reviewers)
1233 tbr_names.extend(owners_db.reviewers_for(missing_files,
1234 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001235 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1236 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1237
1238 # Put the new lines in the description where the old first R= line was.
1239 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1240 if 0 <= line_loc < len(self._description_lines):
1241 if new_tbr_line:
1242 self._description_lines.insert(line_loc, new_tbr_line)
1243 if new_r_line:
1244 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001245 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001246 if new_r_line:
1247 self.append_footer(new_r_line)
1248 if new_tbr_line:
1249 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001250
1251 def prompt(self):
1252 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001253 self.set_description([
1254 '# Enter a description of the change.',
1255 '# This will be displayed on the codereview site.',
1256 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001257 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001258 '--------------------',
1259 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001260
agable@chromium.org42c20792013-09-12 17:34:49 +00001261 regexp = re.compile(self.BUG_LINE)
1262 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001263 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001264 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001265 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001266 if not content:
1267 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001268 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001269
1270 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001271 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1272 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001273 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001274 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001275
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001276 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001277 if self._description_lines:
1278 # Add an empty line if either the last line or the new line isn't a tag.
1279 last_line = self._description_lines[-1]
1280 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1281 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1282 self._description_lines.append('')
1283 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001284
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001285 def get_reviewers(self):
1286 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001287 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1288 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001289 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001290
1291
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001292def get_approving_reviewers(props):
1293 """Retrieves the reviewers that approved a CL from the issue properties with
1294 messages.
1295
1296 Note that the list may contain reviewers that are not committer, thus are not
1297 considered by the CQ.
1298 """
1299 return sorted(
1300 set(
1301 message['sender']
1302 for message in props['messages']
1303 if message['approval'] and message['sender'] in props['reviewers']
1304 )
1305 )
1306
1307
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001308def FindCodereviewSettingsFile(filename='codereview.settings'):
1309 """Finds the given file starting in the cwd and going up.
1310
1311 Only looks up to the top of the repository unless an
1312 'inherit-review-settings-ok' file exists in the root of the repository.
1313 """
1314 inherit_ok_file = 'inherit-review-settings-ok'
1315 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001316 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001317 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1318 root = '/'
1319 while True:
1320 if filename in os.listdir(cwd):
1321 if os.path.isfile(os.path.join(cwd, filename)):
1322 return open(os.path.join(cwd, filename))
1323 if cwd == root:
1324 break
1325 cwd = os.path.dirname(cwd)
1326
1327
1328def LoadCodereviewSettingsFromFile(fileobj):
1329 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001330 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001331
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001332 def SetProperty(name, setting, unset_error_ok=False):
1333 fullname = 'rietveld.' + name
1334 if setting in keyvals:
1335 RunGit(['config', fullname, keyvals[setting]])
1336 else:
1337 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1338
1339 SetProperty('server', 'CODE_REVIEW_SERVER')
1340 # Only server setting is required. Other settings can be absent.
1341 # In that case, we ignore errors raised during option deletion attempt.
1342 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001343 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001344 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1345 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001346 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001347 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001348 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1349 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001350 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001351 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001352 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001353 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1354 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001356 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001357 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001358
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001359 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1360 #should be of the form
1361 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1362 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1363 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1364 keyvals['ORIGIN_URL_CONFIG']])
1365
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001367def urlretrieve(source, destination):
1368 """urllib is broken for SSL connections via a proxy therefore we
1369 can't use urllib.urlretrieve()."""
1370 with open(destination, 'w') as f:
1371 f.write(urllib2.urlopen(source).read())
1372
1373
ukai@chromium.org712d6102013-11-27 00:52:58 +00001374def hasSheBang(fname):
1375 """Checks fname is a #! script."""
1376 with open(fname) as f:
1377 return f.read(2).startswith('#!')
1378
1379
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001380def DownloadHooks(force):
1381 """downloads hooks
1382
1383 Args:
1384 force: True to update hooks. False to install hooks if not present.
1385 """
1386 if not settings.GetIsGerrit():
1387 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001388 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001389 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1390 if not os.access(dst, os.X_OK):
1391 if os.path.exists(dst):
1392 if not force:
1393 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001394 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001395 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001396 if not hasSheBang(dst):
1397 DieWithError('Not a script: %s\n'
1398 'You need to download from\n%s\n'
1399 'into .git/hooks/commit-msg and '
1400 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001401 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1402 except Exception:
1403 if os.path.exists(dst):
1404 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001405 DieWithError('\nFailed to download hooks.\n'
1406 'You need to download from\n%s\n'
1407 'into .git/hooks/commit-msg and '
1408 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001409
1410
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001411@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001413 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001415 parser.add_option('--activate-update', action='store_true',
1416 help='activate auto-updating [rietveld] section in '
1417 '.git/config')
1418 parser.add_option('--deactivate-update', action='store_true',
1419 help='deactivate auto-updating [rietveld] section in '
1420 '.git/config')
1421 options, args = parser.parse_args(args)
1422
1423 if options.deactivate_update:
1424 RunGit(['config', 'rietveld.autoupdate', 'false'])
1425 return
1426
1427 if options.activate_update:
1428 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1429 return
1430
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431 if len(args) == 0:
1432 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001433 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001434 return 0
1435
1436 url = args[0]
1437 if not url.endswith('codereview.settings'):
1438 url = os.path.join(url, 'codereview.settings')
1439
1440 # Load code review settings and download hooks (if available).
1441 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001442 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443 return 0
1444
1445
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001446def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001447 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001448 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1449 branch = ShortBranchName(branchref)
1450 _, args = parser.parse_args(args)
1451 if not args:
1452 print("Current base-url:")
1453 return RunGit(['config', 'branch.%s.base-url' % branch],
1454 error_ok=False).strip()
1455 else:
1456 print("Setting base-url to %s" % args[0])
1457 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1458 error_ok=False).strip()
1459
1460
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001461def color_for_status(status):
1462 """Maps a Changelist status to color, for CMDstatus and other tools."""
1463 return {
1464 'unsent': Fore.RED,
1465 'waiting': Fore.BLUE,
1466 'reply': Fore.YELLOW,
1467 'lgtm': Fore.GREEN,
1468 'commit': Fore.MAGENTA,
1469 'closed': Fore.CYAN,
1470 'error': Fore.WHITE,
1471 }.get(status, Fore.WHITE)
1472
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001473def fetch_cl_status(branch, auth_config=None):
1474 """Fetches information for an issue and returns (branch, issue, status)."""
1475 cl = Changelist(branchref=branch, auth_config=auth_config)
1476 url = cl.GetIssueURL()
1477 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001478
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001479 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001480 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001481 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001482
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001483 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001484
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001485def get_cl_statuses(
1486 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001487 """Returns a blocking iterable of (branch, issue, color) for given branches.
1488
1489 If fine_grained is true, this will fetch CL statuses from the server.
1490 Otherwise, simply indicate if there's a matching url for the given branches.
1491
1492 If max_processes is specified, it is used as the maximum number of processes
1493 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1494 spawned.
1495 """
1496 # Silence upload.py otherwise it becomes unwieldly.
1497 upload.verbosity = 0
1498
1499 if fine_grained:
1500 # Process one branch synchronously to work through authentication, then
1501 # spawn processes to process all the other branches in parallel.
1502 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001503 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1504 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001505
1506 branches_to_fetch = branches[1:]
1507 pool = ThreadPool(
1508 min(max_processes, len(branches_to_fetch))
1509 if max_processes is not None
1510 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001511 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001512 yield x
1513 else:
1514 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1515 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001516 cl = Changelist(branchref=b, auth_config=auth_config)
1517 url = cl.GetIssueURL()
1518 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001519
rmistry@google.com2dd99862015-06-22 12:22:18 +00001520
1521def upload_branch_deps(cl, args):
1522 """Uploads CLs of local branches that are dependents of the current branch.
1523
1524 If the local branch dependency tree looks like:
1525 test1 -> test2.1 -> test3.1
1526 -> test3.2
1527 -> test2.2 -> test3.3
1528
1529 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
1530 run on the dependent branches in this order:
1531 test2.1, test3.1, test3.2, test2.2, test3.3
1532
1533 Note: This function does not rebase your local dependent branches. Use it when
1534 you make a change to the parent branch that will not conflict with its
1535 dependent branches, and you would like their dependencies updated in
1536 Rietveld.
1537 """
1538 if git_common.is_dirty_git_tree('upload-branch-deps'):
1539 return 1
1540
1541 root_branch = cl.GetBranch()
1542 if root_branch is None:
1543 DieWithError('Can\'t find dependent branches from detached HEAD state. '
1544 'Get on a branch!')
1545 if not cl.GetIssue() or not cl.GetPatchset():
1546 DieWithError('Current branch does not have an uploaded CL. We cannot set '
1547 'patchset dependencies without an uploaded CL.')
1548
1549 branches = RunGit(['for-each-ref',
1550 '--format=%(refname:short) %(upstream:short)',
1551 'refs/heads'])
1552 if not branches:
1553 print('No local branches found.')
1554 return 0
1555
1556 # Create a dictionary of all local branches to the branches that are dependent
1557 # on it.
1558 tracked_to_dependents = collections.defaultdict(list)
1559 for b in branches.splitlines():
1560 tokens = b.split()
1561 if len(tokens) == 2:
1562 branch_name, tracked = tokens
1563 tracked_to_dependents[tracked].append(branch_name)
1564
1565 print
1566 print 'The dependent local branches of %s are:' % root_branch
1567 dependents = []
1568 def traverse_dependents_preorder(branch, padding=''):
1569 dependents_to_process = tracked_to_dependents.get(branch, [])
1570 padding += ' '
1571 for dependent in dependents_to_process:
1572 print '%s%s' % (padding, dependent)
1573 dependents.append(dependent)
1574 traverse_dependents_preorder(dependent, padding)
1575 traverse_dependents_preorder(root_branch)
1576 print
1577
1578 if not dependents:
1579 print 'There are no dependent local branches for %s' % root_branch
1580 return 0
1581
1582 print ('This command will checkout all dependent branches and run '
1583 '"git cl upload".')
1584 ask_for_data('[Press enter to continue or ctrl-C to quit]')
1585
1586 # Add a default patchset title to all upload calls.
1587 args.extend(['-t', 'Updated patchset dependency'])
1588 # Record all dependents that failed to upload.
1589 failures = {}
1590 # Go through all dependents, checkout the branch and upload.
1591 try:
1592 for dependent_branch in dependents:
1593 print
1594 print '--------------------------------------'
1595 print 'Running "git cl upload" from %s:' % dependent_branch
1596 RunGit(['checkout', '-q', dependent_branch])
1597 print
1598 try:
1599 if CMDupload(OptionParser(), args) != 0:
1600 print 'Upload failed for %s!' % dependent_branch
1601 failures[dependent_branch] = 1
1602 except: # pylint: disable=W0702
1603 failures[dependent_branch] = 1
1604 print
1605 finally:
1606 # Swap back to the original root branch.
1607 RunGit(['checkout', '-q', root_branch])
1608
1609 print
1610 print 'Upload complete for dependent branches!'
1611 for dependent_branch in dependents:
1612 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
1613 print ' %s : %s' % (dependent_branch, upload_status)
1614 print
1615
1616 return 0
1617
1618
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001619def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001620 """Show status of changelists.
1621
1622 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001623 - Red not sent for review or broken
1624 - Blue waiting for review
1625 - Yellow waiting for you to reply to review
1626 - Green LGTM'ed
1627 - Magenta in the commit queue
1628 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001629
1630 Also see 'git cl comments'.
1631 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001632 parser.add_option('--field',
1633 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001634 parser.add_option('-f', '--fast', action='store_true',
1635 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001636 parser.add_option(
1637 '-j', '--maxjobs', action='store', type=int,
1638 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001639
1640 auth.add_auth_options(parser)
1641 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001642 if args:
1643 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001644 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001645
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001646 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001647 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001648 if options.field.startswith('desc'):
1649 print cl.GetDescription()
1650 elif options.field == 'id':
1651 issueid = cl.GetIssue()
1652 if issueid:
1653 print issueid
1654 elif options.field == 'patch':
1655 patchset = cl.GetPatchset()
1656 if patchset:
1657 print patchset
1658 elif options.field == 'url':
1659 url = cl.GetIssueURL()
1660 if url:
1661 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001662 return 0
1663
1664 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1665 if not branches:
1666 print('No local branch found.')
1667 return 0
1668
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001669 changes = (
1670 Changelist(branchref=b, auth_config=auth_config)
1671 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001672 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001673 alignment = max(5, max(len(b) for b in branches))
1674 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001675 output = get_cl_statuses(branches,
1676 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001677 max_processes=options.maxjobs,
1678 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001679
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001680 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001681 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001682 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001683 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001684 b, i, status = output.next()
1685 branch_statuses[b] = (i, status)
1686 issue_url, status = branch_statuses.pop(branch)
1687 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001688 reset = Fore.RESET
1689 if not sys.stdout.isatty():
1690 color = ''
1691 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001692 status_str = '(%s)' % status if status else ''
1693 print ' %*s : %s%s %s%s' % (
1694 alignment, ShortBranchName(branch), color, issue_url, status_str,
1695 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001696
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001697 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001698 print
1699 print 'Current branch:',
1700 if not cl.GetIssue():
1701 print 'no issue assigned.'
1702 return 0
1703 print cl.GetBranch()
1704 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001705 if not options.fast:
1706 print 'Issue description:'
1707 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001708 return 0
1709
1710
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001711def colorize_CMDstatus_doc():
1712 """To be called once in main() to add colors to git cl status help."""
1713 colors = [i for i in dir(Fore) if i[0].isupper()]
1714
1715 def colorize_line(line):
1716 for color in colors:
1717 if color in line.upper():
1718 # Extract whitespaces first and the leading '-'.
1719 indent = len(line) - len(line.lstrip(' ')) + 1
1720 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1721 return line
1722
1723 lines = CMDstatus.__doc__.splitlines()
1724 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1725
1726
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001727@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001728def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001729 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001730
1731 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001732 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001733 parser.add_option('-r', '--reverse', action='store_true',
1734 help='Lookup the branch(es) for the specified issues. If '
1735 'no issues are specified, all branches with mapped '
1736 'issues will be listed.')
1737 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001738
dnj@chromium.org406c4402015-03-03 17:22:28 +00001739 if options.reverse:
1740 branches = RunGit(['for-each-ref', 'refs/heads',
1741 '--format=%(refname:short)']).splitlines()
1742
1743 # Reverse issue lookup.
1744 issue_branch_map = {}
1745 for branch in branches:
1746 cl = Changelist(branchref=branch)
1747 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1748 if not args:
1749 args = sorted(issue_branch_map.iterkeys())
1750 for issue in args:
1751 if not issue:
1752 continue
1753 print 'Branch for issue number %s: %s' % (
1754 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1755 else:
1756 cl = Changelist()
1757 if len(args) > 0:
1758 try:
1759 issue = int(args[0])
1760 except ValueError:
1761 DieWithError('Pass a number to set the issue or none to list it.\n'
1762 'Maybe you want to run git cl status?')
1763 cl.SetIssue(issue)
1764 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001765 return 0
1766
1767
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001768def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001769 """Shows or posts review comments for any changelist."""
1770 parser.add_option('-a', '--add-comment', dest='comment',
1771 help='comment to add to an issue')
1772 parser.add_option('-i', dest='issue',
1773 help="review issue id (defaults to current issue)")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001774 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001775 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001776 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001777
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001778 issue = None
1779 if options.issue:
1780 try:
1781 issue = int(options.issue)
1782 except ValueError:
1783 DieWithError('A review issue id is expected to be a number')
1784
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001785 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001786
1787 if options.comment:
1788 cl.AddComment(options.comment)
1789 return 0
1790
1791 data = cl.GetIssueProperties()
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001792 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001793 if message['disapproval']:
1794 color = Fore.RED
1795 elif message['approval']:
1796 color = Fore.GREEN
1797 elif message['sender'] == data['owner_email']:
1798 color = Fore.MAGENTA
1799 else:
1800 color = Fore.BLUE
1801 print '\n%s%s %s%s' % (
1802 color, message['date'].split('.', 1)[0], message['sender'],
1803 Fore.RESET)
1804 if message['text'].strip():
1805 print '\n'.join(' ' + l for l in message['text'].splitlines())
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001806 return 0
1807
1808
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001809def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001810 """Brings up the editor for the current CL's description."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001811 auth.add_auth_options(parser)
1812 options, _ = parser.parse_args(args)
1813 auth_config = auth.extract_auth_config_from_options(options)
1814 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001815 if not cl.GetIssue():
1816 DieWithError('This branch has no associated changelist.')
1817 description = ChangeDescription(cl.GetDescription())
1818 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00001819 if cl.GetDescription() != description.description:
1820 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001821 return 0
1822
1823
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001824def CreateDescriptionFromLog(args):
1825 """Pulls out the commit log to use as a base for the CL description."""
1826 log_args = []
1827 if len(args) == 1 and not args[0].endswith('.'):
1828 log_args = [args[0] + '..']
1829 elif len(args) == 1 and args[0].endswith('...'):
1830 log_args = [args[0][:-1]]
1831 elif len(args) == 2:
1832 log_args = [args[0] + '..' + args[1]]
1833 else:
1834 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001835 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001836
1837
thestig@chromium.org44202a22014-03-11 19:22:18 +00001838def CMDlint(parser, args):
1839 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001840 parser.add_option('--filter', action='append', metavar='-x,+y',
1841 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001842 auth.add_auth_options(parser)
1843 options, args = parser.parse_args(args)
1844 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001845
1846 # Access to a protected member _XX of a client class
1847 # pylint: disable=W0212
1848 try:
1849 import cpplint
1850 import cpplint_chromium
1851 except ImportError:
1852 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1853 return 1
1854
1855 # Change the current working directory before calling lint so that it
1856 # shows the correct base.
1857 previous_cwd = os.getcwd()
1858 os.chdir(settings.GetRoot())
1859 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001860 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001861 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1862 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001863 if not files:
1864 print "Cannot lint an empty CL"
1865 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001866
1867 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001868 command = args + files
1869 if options.filter:
1870 command = ['--filter=' + ','.join(options.filter)] + command
1871 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001872
1873 white_regex = re.compile(settings.GetLintRegex())
1874 black_regex = re.compile(settings.GetLintIgnoreRegex())
1875 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1876 for filename in filenames:
1877 if white_regex.match(filename):
1878 if black_regex.match(filename):
1879 print "Ignoring file %s" % filename
1880 else:
1881 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1882 extra_check_functions)
1883 else:
1884 print "Skipping file %s" % filename
1885 finally:
1886 os.chdir(previous_cwd)
1887 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1888 if cpplint._cpplint_state.error_count != 0:
1889 return 1
1890 return 0
1891
1892
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001893def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001894 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001895 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001896 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001897 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001898 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001899 auth.add_auth_options(parser)
1900 options, args = parser.parse_args(args)
1901 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001902
sbc@chromium.org71437c02015-04-09 19:29:40 +00001903 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00001904 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001905 return 1
1906
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001907 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001908 if args:
1909 base_branch = args[0]
1910 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001911 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001912 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001913
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001914 cl.RunHook(
1915 committing=not options.upload,
1916 may_prompt=False,
1917 verbose=options.verbose,
1918 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001919 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001920
1921
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001922def AddChangeIdToCommitMessage(options, args):
1923 """Re-commits using the current message, assumes the commit hook is in
1924 place.
1925 """
1926 log_desc = options.message or CreateDescriptionFromLog(args)
1927 git_command = ['commit', '--amend', '-m', log_desc]
1928 RunGit(git_command)
1929 new_log_desc = CreateDescriptionFromLog(args)
1930 if CHANGE_ID in new_log_desc:
1931 print 'git-cl: Added Change-Id to commit message.'
1932 else:
1933 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1934
1935
piman@chromium.org336f9122014-09-04 02:16:55 +00001936def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001937 """upload the current branch to gerrit."""
1938 # We assume the remote called "origin" is the one we want.
1939 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001940 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00001941
1942 remote, remote_branch = cl.GetRemoteBranch()
1943 branch = GetTargetRef(remote, remote_branch, options.target_branch,
1944 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001945
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001946 change_desc = ChangeDescription(
1947 options.message or CreateDescriptionFromLog(args))
1948 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001949 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001950 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001951
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001952 if options.squash:
1953 # Try to get the message from a previous upload.
1954 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
1955 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
1956 if not message:
1957 if not options.force:
1958 change_desc.prompt()
1959
1960 if CHANGE_ID not in change_desc.description:
1961 # Run the commit-msg hook without modifying the head commit by writing
1962 # the commit message to a temporary file and running the hook over it,
1963 # then reading the file back in.
1964 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
1965 'commit-msg')
1966 file_handle, msg_file = tempfile.mkstemp(text=True,
1967 prefix='commit_msg')
1968 try:
1969 try:
1970 with os.fdopen(file_handle, 'w') as fileobj:
1971 fileobj.write(change_desc.description)
1972 finally:
1973 os.close(file_handle)
1974 RunCommand([commit_msg_hook, msg_file])
1975 change_desc.set_description(gclient_utils.FileRead(msg_file))
1976 finally:
1977 os.remove(msg_file)
1978
1979 if not change_desc.description:
1980 print "Description is empty; aborting."
1981 return 1
1982
1983 message = change_desc.description
1984
1985 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1986 if remote is '.':
1987 # If our upstream branch is local, we base our squashed commit on its
1988 # squashed version.
1989 parent = ('refs/heads/git_cl_uploads/' +
1990 scm.GIT.ShortBranchName(upstream_branch))
1991
1992 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
1993 # will create additional CLs when uploading.
1994 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
1995 RunGitSilent(['rev-parse', parent + ':'])):
1996 print 'Upload upstream branch ' + upstream_branch + ' first.'
1997 return 1
1998 else:
1999 parent = cl.GetCommonAncestorWithUpstream()
2000
2001 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2002 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2003 '-m', message]).strip()
2004 else:
2005 if CHANGE_ID not in change_desc.description:
2006 AddChangeIdToCommitMessage(options, args)
2007 ref_to_push = 'HEAD'
2008 parent = '%s/%s' % (gerrit_remote, branch)
2009
2010 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2011 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002012 if len(commits) > 1:
2013 print('WARNING: This will upload %d commits. Run the following command '
2014 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002015 print('git log %s..%s' % (parent, ref_to_push))
2016 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00002017 'commit.')
2018 ask_for_data('About to upload; enter to confirm.')
2019
piman@chromium.org336f9122014-09-04 02:16:55 +00002020 if options.reviewers or options.tbr_owners:
2021 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002022
ukai@chromium.orge8077812012-02-03 03:41:46 +00002023 receive_options = []
2024 cc = cl.GetCCList().split(',')
2025 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002026 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002027 cc = filter(None, cc)
2028 if cc:
2029 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002030 if change_desc.get_reviewers():
2031 receive_options.extend(
2032 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002033
ukai@chromium.orge8077812012-02-03 03:41:46 +00002034 git_command = ['push']
2035 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00002036 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00002037 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002038 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00002039 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002040
2041 if options.squash:
2042 head = RunGit(['rev-parse', 'HEAD']).strip()
2043 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
2044
ukai@chromium.orge8077812012-02-03 03:41:46 +00002045 # TODO(ukai): parse Change-Id: and set issue number?
2046 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002047
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002048
wittman@chromium.org455dc922015-01-26 20:15:50 +00002049def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
2050 """Computes the remote branch ref to use for the CL.
2051
2052 Args:
2053 remote (str): The git remote for the CL.
2054 remote_branch (str): The git remote branch for the CL.
2055 target_branch (str): The target branch specified by the user.
2056 pending_prefix (str): The pending prefix from the settings.
2057 """
2058 if not (remote and remote_branch):
2059 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002060
wittman@chromium.org455dc922015-01-26 20:15:50 +00002061 if target_branch:
2062 # Cannonicalize branch references to the equivalent local full symbolic
2063 # refs, which are then translated into the remote full symbolic refs
2064 # below.
2065 if '/' not in target_branch:
2066 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
2067 else:
2068 prefix_replacements = (
2069 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
2070 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
2071 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
2072 )
2073 match = None
2074 for regex, replacement in prefix_replacements:
2075 match = re.search(regex, target_branch)
2076 if match:
2077 remote_branch = target_branch.replace(match.group(0), replacement)
2078 break
2079 if not match:
2080 # This is a branch path but not one we recognize; use as-is.
2081 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00002082 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
2083 # Handle the refs that need to land in different refs.
2084 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002085
wittman@chromium.org455dc922015-01-26 20:15:50 +00002086 # Create the true path to the remote branch.
2087 # Does the following translation:
2088 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
2089 # * refs/remotes/origin/master -> refs/heads/master
2090 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
2091 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
2092 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
2093 elif remote_branch.startswith('refs/remotes/%s/' % remote):
2094 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
2095 'refs/heads/')
2096 elif remote_branch.startswith('refs/remotes/branch-heads'):
2097 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
2098 # If a pending prefix exists then replace refs/ with it.
2099 if pending_prefix:
2100 remote_branch = remote_branch.replace('refs/', pending_prefix)
2101 return remote_branch
2102
2103
piman@chromium.org336f9122014-09-04 02:16:55 +00002104def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002105 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002106 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2107 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002108 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002109 if options.emulate_svn_auto_props:
2110 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002111
2112 change_desc = None
2113
pgervais@chromium.org91141372014-01-09 23:27:20 +00002114 if options.email is not None:
2115 upload_args.extend(['--email', options.email])
2116
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002117 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002118 if options.title:
2119 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002120 if options.message:
2121 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002122 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002123 print ("This branch is associated with issue %s. "
2124 "Adding patch to that issue." % cl.GetIssue())
2125 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002126 if options.title:
2127 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002128 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002129 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002130 if options.reviewers or options.tbr_owners:
2131 change_desc.update_reviewers(options.reviewers,
2132 options.tbr_owners,
2133 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002134 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002135 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002136
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002137 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002138 print "Description is empty; aborting."
2139 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002140
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002141 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002142 if change_desc.get_reviewers():
2143 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002144 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002145 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002146 DieWithError("Must specify reviewers to send email.")
2147 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002148
2149 # We check this before applying rietveld.private assuming that in
2150 # rietveld.cc only addresses which we can send private CLs to are listed
2151 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2152 # --private is specified explicitly on the command line.
2153 if options.private:
2154 logging.warn('rietveld.cc is ignored since private flag is specified. '
2155 'You need to review and add them manually if necessary.')
2156 cc = cl.GetCCListWithoutDefault()
2157 else:
2158 cc = cl.GetCCList()
2159 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002160 if cc:
2161 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002162
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002163 if options.private or settings.GetDefaultPrivateFlag() == "True":
2164 upload_args.append('--private')
2165
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002166 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002167 if not options.find_copies:
2168 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002169
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002170 # Include the upstream repo's URL in the change -- this is useful for
2171 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002172 remote_url = cl.GetGitBaseUrlFromConfig()
2173 if not remote_url:
2174 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002175 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002176 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002177 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2178 remote_url = (cl.GetRemoteUrl() + '@'
2179 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002180 if remote_url:
2181 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002182 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002183 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2184 settings.GetPendingRefPrefix())
2185 if target_ref:
2186 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002187
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002188 project = settings.GetProject()
2189 if project:
2190 upload_args.extend(['--project', project])
2191
rmistry@google.comef966222015-04-07 11:15:01 +00002192 if options.cq_dry_run:
2193 upload_args.extend(['--cq_dry_run'])
2194
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002195 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002196 upload_args = ['upload'] + upload_args + args
2197 logging.info('upload.RealMain(%s)', upload_args)
2198 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002199 issue = int(issue)
2200 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002201 except KeyboardInterrupt:
2202 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002203 except:
2204 # If we got an exception after the user typed a description for their
2205 # change, back up the description before re-raising.
2206 if change_desc:
2207 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2208 print '\nGot exception while uploading -- saving description to %s\n' \
2209 % backup_path
2210 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002211 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002212 backup_file.close()
2213 raise
2214
2215 if not cl.GetIssue():
2216 cl.SetIssue(issue)
2217 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002218
2219 if options.use_commit_queue:
2220 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002221 return 0
2222
2223
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002224def cleanup_list(l):
2225 """Fixes a list so that comma separated items are put as individual items.
2226
2227 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2228 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2229 """
2230 items = sum((i.split(',') for i in l), [])
2231 stripped_items = (i.strip() for i in items)
2232 return sorted(filter(None, stripped_items))
2233
2234
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002235@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002236def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002237 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00002238 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2239 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002240 parser.add_option('--bypass-watchlists', action='store_true',
2241 dest='bypass_watchlists',
2242 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002243 parser.add_option('-f', action='store_true', dest='force',
2244 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002245 parser.add_option('-m', dest='message', help='message for patchset')
2246 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002247 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002248 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002249 help='reviewer email addresses')
2250 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002251 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002252 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002253 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002254 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002255 parser.add_option('--emulate_svn_auto_props',
2256 '--emulate-svn-auto-props',
2257 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002258 dest="emulate_svn_auto_props",
2259 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002260 parser.add_option('-c', '--use-commit-queue', action='store_true',
2261 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002262 parser.add_option('--private', action='store_true',
2263 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002264 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002265 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002266 metavar='TARGET',
2267 help='Apply CL to remote ref TARGET. ' +
2268 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002269 parser.add_option('--squash', action='store_true',
2270 help='Squash multiple commits into one (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002271 parser.add_option('--email', default=None,
2272 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002273 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2274 help='add a set of OWNERS to TBR')
rmistry@google.comef966222015-04-07 11:15:01 +00002275 parser.add_option('--cq-dry-run', dest='cq_dry_run', action='store_true',
2276 help='Send the patchset to do a CQ dry run right after '
2277 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00002278 parser.add_option('--dependencies', action='store_true',
2279 help='Uploads CLs of all the local branches that depend on '
2280 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002281
rmistry@google.com2dd99862015-06-22 12:22:18 +00002282 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002283 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002284 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002285 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002286 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002287
sbc@chromium.org71437c02015-04-09 19:29:40 +00002288 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002289 return 1
2290
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002291 options.reviewers = cleanup_list(options.reviewers)
2292 options.cc = cleanup_list(options.cc)
2293
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002294 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002295 if args:
2296 # TODO(ukai): is it ok for gerrit case?
2297 base_branch = args[0]
2298 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002299 if cl.GetBranch() is None:
2300 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2301
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002302 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002303 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002304 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002305
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002306 # Make sure authenticated to Rietveld before running expensive hooks. It is
2307 # a fast, best efforts check. Rietveld still can reject the authentication
2308 # during the actual upload.
2309 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2310 authenticator = auth.get_authenticator_for_host(
2311 cl.GetRietveldServer(), auth_config)
2312 if not authenticator.has_cached_credentials():
2313 raise auth.LoginRequiredError(cl.GetRietveldServer())
2314
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002315 # Apply watchlists on upload.
2316 change = cl.GetChange(base_branch, None)
2317 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2318 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002319 if not options.bypass_watchlists:
2320 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002321
ukai@chromium.orge8077812012-02-03 03:41:46 +00002322 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002323 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002324 # Set the reviewer list now so that presubmit checks can access it.
2325 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002326 change_description.update_reviewers(options.reviewers,
2327 options.tbr_owners,
2328 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002329 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002330 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002331 may_prompt=not options.force,
2332 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002333 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002334 if not hook_results.should_continue():
2335 return 1
2336 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002337 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002338
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002339 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002340 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002341 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002342 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002343 print ('The last upload made from this repository was patchset #%d but '
2344 'the most recent patchset on the server is #%d.'
2345 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002346 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2347 'from another machine or branch the patch you\'re uploading now '
2348 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002349 ask_for_data('About to upload; enter to confirm.')
2350
iannucci@chromium.org79540052012-10-19 23:15:26 +00002351 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002352 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00002353 return GerritUpload(options, args, cl, change)
2354 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002355 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002356 git_set_branch_value('last-upload-hash',
2357 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002358 # Run post upload hooks, if specified.
2359 if settings.GetRunPostUploadHook():
2360 presubmit_support.DoPostUploadExecuter(
2361 change,
2362 cl,
2363 settings.GetRoot(),
2364 options.verbose,
2365 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002366
rmistry@google.com2dd99862015-06-22 12:22:18 +00002367 # Upload all dependencies if specified.
2368 if options.dependencies:
2369 print
2370 print '--dependencies has been specified.'
2371 print 'All dependent local branches will be re-uploaded.'
2372 print
2373 # Remove the dependencies flag from args so that we do not end up in a
2374 # loop.
2375 orig_args.remove('--dependencies')
2376 upload_branch_deps(cl, orig_args)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002377 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002378
2379
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002380def IsSubmoduleMergeCommit(ref):
2381 # When submodules are added to the repo, we expect there to be a single
2382 # non-git-svn merge commit at remote HEAD with a signature comment.
2383 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002384 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002385 return RunGit(cmd) != ''
2386
2387
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002388def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002389 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002390
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002391 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002392 Updates changelog with metadata (e.g. pointer to review).
2393 Pushes/dcommits the code upstream.
2394 Updates review and closes.
2395 """
2396 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2397 help='bypass upload presubmit hook')
2398 parser.add_option('-m', dest='message',
2399 help="override review description")
2400 parser.add_option('-f', action='store_true', dest='force',
2401 help="force yes to questions (don't prompt)")
2402 parser.add_option('-c', dest='contributor',
2403 help="external contributor for patch (appended to " +
2404 "description and used as author for git). Should be " +
2405 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002406 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002407 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002408 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002409 auth_config = auth.extract_auth_config_from_options(options)
2410
2411 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002412
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002413 current = cl.GetBranch()
2414 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2415 if not settings.GetIsGitSvn() and remote == '.':
2416 print
2417 print 'Attempting to push branch %r into another local branch!' % current
2418 print
2419 print 'Either reparent this branch on top of origin/master:'
2420 print ' git reparent-branch --root'
2421 print
2422 print 'OR run `git rebase-update` if you think the parent branch is already'
2423 print 'committed.'
2424 print
2425 print ' Current parent: %r' % upstream_branch
2426 return 1
2427
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002428 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002429 # Default to merging against our best guess of the upstream branch.
2430 args = [cl.GetUpstreamBranch()]
2431
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002432 if options.contributor:
2433 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2434 print "Please provide contibutor as 'First Last <email@example.com>'"
2435 return 1
2436
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002437 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002438 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002439
sbc@chromium.org71437c02015-04-09 19:29:40 +00002440 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002441 return 1
2442
2443 # This rev-list syntax means "show all commits not in my branch that
2444 # are in base_branch".
2445 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2446 base_branch]).splitlines()
2447 if upstream_commits:
2448 print ('Base branch "%s" has %d commits '
2449 'not in this branch.' % (base_branch, len(upstream_commits)))
2450 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2451 return 1
2452
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002453 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002454 svn_head = None
2455 if cmd == 'dcommit' or base_has_submodules:
2456 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2457 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002458
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002459 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002460 # If the base_head is a submodule merge commit, the first parent of the
2461 # base_head should be a git-svn commit, which is what we're interested in.
2462 base_svn_head = base_branch
2463 if base_has_submodules:
2464 base_svn_head += '^1'
2465
2466 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002467 if extra_commits:
2468 print ('This branch has %d additional commits not upstreamed yet.'
2469 % len(extra_commits.splitlines()))
2470 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2471 'before attempting to %s.' % (base_branch, cmd))
2472 return 1
2473
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002474 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002475 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002476 author = None
2477 if options.contributor:
2478 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002479 hook_results = cl.RunHook(
2480 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002481 may_prompt=not options.force,
2482 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002483 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002484 if not hook_results.should_continue():
2485 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002486
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002487 # Check the tree status if the tree status URL is set.
2488 status = GetTreeStatus()
2489 if 'closed' == status:
2490 print('The tree is closed. Please wait for it to reopen. Use '
2491 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2492 return 1
2493 elif 'unknown' == status:
2494 print('Unable to determine tree status. Please verify manually and '
2495 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2496 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002497 else:
2498 breakpad.SendStack(
2499 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002500 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2501 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002502 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002503
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002504 change_desc = ChangeDescription(options.message)
2505 if not change_desc.description and cl.GetIssue():
2506 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002507
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002508 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002509 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002510 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002511 else:
2512 print 'No description set.'
2513 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2514 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002515
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002516 # Keep a separate copy for the commit message, because the commit message
2517 # contains the link to the Rietveld issue, while the Rietveld message contains
2518 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002519 # Keep a separate copy for the commit message.
2520 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002521 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002522
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002523 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002524 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002525 # Xcode won't linkify this URL unless there is a non-whitespace character
2526 # after it. Add a period on a new line to circumvent this.
2527 commit_desc.append_footer('Review URL: %s.' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002528 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002529 commit_desc.append_footer('Patch from %s.' % options.contributor)
2530
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002531 print('Description:')
2532 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002533
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002534 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002535 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002536 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002537
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002538 # We want to squash all this branch's commits into one commit with the proper
2539 # description. We do this by doing a "reset --soft" to the base branch (which
2540 # keeps the working copy the same), then dcommitting that. If origin/master
2541 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2542 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002543 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002544 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2545 # Delete the branches if they exist.
2546 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2547 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2548 result = RunGitWithCode(showref_cmd)
2549 if result[0] == 0:
2550 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002551
2552 # We might be in a directory that's present in this branch but not in the
2553 # trunk. Move up to the top of the tree so that git commands that expect a
2554 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002555 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002556 if rel_base_path:
2557 os.chdir(rel_base_path)
2558
2559 # Stuff our change into the merge branch.
2560 # We wrap in a try...finally block so if anything goes wrong,
2561 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002562 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002563 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002564 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002565 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002566 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002567 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002568 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002569 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002570 RunGit(
2571 [
2572 'commit', '--author', options.contributor,
2573 '-m', commit_desc.description,
2574 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002575 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002576 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002577 if base_has_submodules:
2578 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2579 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2580 RunGit(['checkout', CHERRY_PICK_BRANCH])
2581 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002582 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002583 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002584 pending_prefix = settings.GetPendingRefPrefix()
2585 if not pending_prefix or branch.startswith(pending_prefix):
2586 # If not using refs/pending/heads/* at all, or target ref is already set
2587 # to pending, then push to the target ref directly.
2588 retcode, output = RunGitWithCode(
2589 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002590 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002591 else:
2592 # Cherry-pick the change on top of pending ref and then push it.
2593 assert branch.startswith('refs/'), branch
2594 assert pending_prefix[-1] == '/', pending_prefix
2595 pending_ref = pending_prefix + branch[len('refs/'):]
2596 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002597 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002598 if retcode == 0:
2599 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002600 else:
2601 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002602 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002603 'svn', 'dcommit',
2604 '-C%s' % options.similarity,
2605 '--no-rebase', '--rmdir',
2606 ]
2607 if settings.GetForceHttpsCommitUrl():
2608 # Allow forcing https commit URLs for some projects that don't allow
2609 # committing to http URLs (like Google Code).
2610 remote_url = cl.GetGitSvnRemoteUrl()
2611 if urlparse.urlparse(remote_url).scheme == 'http':
2612 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002613 cmd_args.append('--commit-url=%s' % remote_url)
2614 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002615 if 'Committed r' in output:
2616 revision = re.match(
2617 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2618 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002619 finally:
2620 # And then swap back to the original branch and clean up.
2621 RunGit(['checkout', '-q', cl.GetBranch()])
2622 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002623 if base_has_submodules:
2624 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002625
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002626 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002627 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002628 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002629
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002630 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002631 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002632 try:
2633 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2634 # We set pushed_to_pending to False, since it made it all the way to the
2635 # real ref.
2636 pushed_to_pending = False
2637 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002638 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002639
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002640 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002641 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002642 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002643 if not to_pending:
2644 if viewvc_url and revision:
2645 change_desc.append_footer(
2646 'Committed: %s%s' % (viewvc_url, revision))
2647 elif revision:
2648 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002649 print ('Closing issue '
2650 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002651 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002652 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002653 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002654 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002655 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002656 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002657 if options.bypass_hooks:
2658 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2659 else:
2660 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002661 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002662 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002663
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002664 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002665 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2666 print 'The commit is in the pending queue (%s).' % pending_ref
2667 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002668 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002669 'footer.' % branch)
2670
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002671 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2672 if os.path.isfile(hook):
2673 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002674
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002675 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002676
2677
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002678def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2679 print
2680 print 'Waiting for commit to be landed on %s...' % real_ref
2681 print '(If you are impatient, you may Ctrl-C once without harm)'
2682 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2683 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2684
2685 loop = 0
2686 while True:
2687 sys.stdout.write('fetching (%d)... \r' % loop)
2688 sys.stdout.flush()
2689 loop += 1
2690
2691 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2692 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2693 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2694 for commit in commits.splitlines():
2695 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2696 print 'Found commit on %s' % real_ref
2697 return commit
2698
2699 current_rev = to_rev
2700
2701
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002702def PushToGitPending(remote, pending_ref, upstream_ref):
2703 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2704
2705 Returns:
2706 (retcode of last operation, output log of last operation).
2707 """
2708 assert pending_ref.startswith('refs/'), pending_ref
2709 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2710 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2711 code = 0
2712 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002713 max_attempts = 3
2714 attempts_left = max_attempts
2715 while attempts_left:
2716 if attempts_left != max_attempts:
2717 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2718 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002719
2720 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002721 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002722 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002723 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002724 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002725 print 'Fetch failed with exit code %d.' % code
2726 if out.strip():
2727 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002728 continue
2729
2730 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002731 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002732 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002733 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002734 if code:
2735 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002736 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2737 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002738 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2739 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002740 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002741 return code, out
2742
2743 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002744 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002745 code, out = RunGitWithCode(
2746 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2747 if code == 0:
2748 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002749 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002750 return code, out
2751
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002752 print 'Push failed with exit code %d.' % code
2753 if out.strip():
2754 print out.strip()
2755 if IsFatalPushFailure(out):
2756 print (
2757 'Fatal push error. Make sure your .netrc credentials and git '
2758 'user.email are correct and you have push access to the repo.')
2759 return code, out
2760
2761 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002762 return code, out
2763
2764
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002765def IsFatalPushFailure(push_stdout):
2766 """True if retrying push won't help."""
2767 return '(prohibited by Gerrit)' in push_stdout
2768
2769
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002770@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002771def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002772 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002773 if not settings.GetIsGitSvn():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002774 if get_footer_svn_id():
2775 # If it looks like previous commits were mirrored with git-svn.
2776 message = """This repository appears to be a git-svn mirror, but no
2777upstream SVN master is set. You probably need to run 'git auto-svn' once."""
2778 else:
2779 message = """This doesn't appear to be an SVN repository.
2780If your project has a true, writeable git repository, you probably want to run
2781'git cl land' instead.
2782If your project has a git mirror of an upstream SVN master, you probably need
2783to run 'git svn init'.
2784
2785Using the wrong command might cause your commit to appear to succeed, and the
2786review to be closed, without actually landing upstream. If you choose to
2787proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002788 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002789 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002790 return SendUpstream(parser, args, 'dcommit')
2791
2792
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002793@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002794def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002795 """Commits the current changelist via git."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002796 if settings.GetIsGitSvn() or get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002797 print('This appears to be an SVN repository.')
2798 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002799 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00002800 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002801 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002802
2803
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002804@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002805def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002806 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002807 parser.add_option('-b', dest='newbranch',
2808 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002809 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002810 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002811 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2812 help='Change to the directory DIR immediately, '
2813 'before doing anything else.')
2814 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002815 help='failed patches spew .rej files rather than '
2816 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002817 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2818 help="don't commit after patch applies")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002819 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002820 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002821 auth_config = auth.extract_auth_config_from_options(options)
2822
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002823 if len(args) != 1:
2824 parser.print_help()
2825 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002826 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002827
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002828 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002829 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002830 return 1
2831
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002832 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002833 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002834
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002835 if options.newbranch:
2836 if options.force:
2837 RunGit(['branch', '-D', options.newbranch],
2838 stderr=subprocess2.PIPE, error_ok=True)
2839 RunGit(['checkout', '-b', options.newbranch,
2840 Changelist().GetUpstreamBranch()])
2841
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002842 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002843 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002844
2845
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002846def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00002847 # PatchIssue should never be called with a dirty tree. It is up to the
2848 # caller to check this, but just in case we assert here since the
2849 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002850 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002851
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002852 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002853 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002854 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002855 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002856 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002857 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002858 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002859 # Assume it's a URL to the patch. Default to https.
2860 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00002861 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002862 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002863 DieWithError('Must pass an issue ID or full URL for '
2864 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00002865 issue = int(match.group(2))
2866 cl = Changelist(issue=issue, auth_config=auth_config)
2867 cl.rietveld_server = match.group(1)
2868 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002869 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002870
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002871 # Switch up to the top-level directory, if necessary, in preparation for
2872 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002873 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002874 if top:
2875 os.chdir(top)
2876
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002877 # Git patches have a/ at the beginning of source paths. We strip that out
2878 # with a sed script rather than the -p flag to patch so we can feed either
2879 # Git or svn-style patches into the same apply command.
2880 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002881 try:
2882 patch_data = subprocess2.check_output(
2883 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2884 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002885 DieWithError('Git patch mungling failed.')
2886 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002887
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002888 # We use "git apply" to apply the patch instead of "patch" so that we can
2889 # pick up file adds.
2890 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002891 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002892 if directory:
2893 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002894 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002895 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002896 elif IsGitVersionAtLeast('1.7.12'):
2897 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002898 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002899 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002900 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002901 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00002902 print 'Failed to apply the patch'
2903 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002904
2905 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002906 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00002907 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
2908 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00002909 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2910 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002911 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002912 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002913 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002914 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002915 else:
2916 print "Patch applied to index."
2917 return 0
2918
2919
2920def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002921 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002922 # Provide a wrapper for git svn rebase to help avoid accidental
2923 # git svn dcommit.
2924 # It's the only command that doesn't use parser at all since we just defer
2925 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002926
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002927 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002928
2929
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002930def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002931 """Fetches the tree status and returns either 'open', 'closed',
2932 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002933 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002934 if url:
2935 status = urllib2.urlopen(url).read().lower()
2936 if status.find('closed') != -1 or status == '0':
2937 return 'closed'
2938 elif status.find('open') != -1 or status == '1':
2939 return 'open'
2940 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002941 return 'unset'
2942
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002943
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002944def GetTreeStatusReason():
2945 """Fetches the tree status from a json url and returns the message
2946 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002947 url = settings.GetTreeStatusUrl()
2948 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002949 connection = urllib2.urlopen(json_url)
2950 status = json.loads(connection.read())
2951 connection.close()
2952 return status['message']
2953
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002954
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002955def GetBuilderMaster(bot_list):
2956 """For a given builder, fetch the master from AE if available."""
2957 map_url = 'https://builders-map.appspot.com/'
2958 try:
2959 master_map = json.load(urllib2.urlopen(map_url))
2960 except urllib2.URLError as e:
2961 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2962 (map_url, e))
2963 except ValueError as e:
2964 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2965 if not master_map:
2966 return None, 'Failed to build master map.'
2967
2968 result_master = ''
2969 for bot in bot_list:
2970 builder = bot.split(':', 1)[0]
2971 master_list = master_map.get(builder, [])
2972 if not master_list:
2973 return None, ('No matching master for builder %s.' % builder)
2974 elif len(master_list) > 1:
2975 return None, ('The builder name %s exists in multiple masters %s.' %
2976 (builder, master_list))
2977 else:
2978 cur_master = master_list[0]
2979 if not result_master:
2980 result_master = cur_master
2981 elif result_master != cur_master:
2982 return None, 'The builders do not belong to the same master.'
2983 return result_master, None
2984
2985
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002986def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002987 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002988 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002989 status = GetTreeStatus()
2990 if 'unset' == status:
2991 print 'You must configure your tree status URL by running "git cl config".'
2992 return 2
2993
2994 print "The tree is %s" % status
2995 print
2996 print GetTreeStatusReason()
2997 if status != 'open':
2998 return 1
2999 return 0
3000
3001
maruel@chromium.org15192402012-09-06 12:38:29 +00003002def CMDtry(parser, args):
3003 """Triggers a try job through Rietveld."""
3004 group = optparse.OptionGroup(parser, "Try job options")
3005 group.add_option(
3006 "-b", "--bot", action="append",
3007 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
3008 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003009 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00003010 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003011 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00003012 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003013 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00003014 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003015 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003016 "-r", "--revision",
3017 help="Revision to use for the try job; default: the "
3018 "revision will be determined by the try server; see "
3019 "its waterfall for more info")
3020 group.add_option(
3021 "-c", "--clobber", action="store_true", default=False,
3022 help="Force a clobber before building; e.g. don't do an "
3023 "incremental build")
3024 group.add_option(
3025 "--project",
3026 help="Override which project to use. Projects are defined "
3027 "server-side to define what default bot set to use")
3028 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00003029 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003030 group.add_option(
3031 "--use-buildbucket", action="store_true", default=False,
3032 help="Use buildbucket to trigger try jobs.")
maruel@chromium.org15192402012-09-06 12:38:29 +00003033 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003034 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00003035 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003036 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00003037
3038 if args:
3039 parser.error('Unknown arguments: %s' % args)
3040
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003041 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00003042 if not cl.GetIssue():
3043 parser.error('Need to upload first')
3044
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003045 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00003046 if props.get('closed'):
3047 parser.error('Cannot send tryjobs for a closed CL')
3048
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003049 if props.get('private'):
3050 parser.error('Cannot use trybots with private issue')
3051
maruel@chromium.org15192402012-09-06 12:38:29 +00003052 if not options.name:
3053 options.name = cl.GetBranch()
3054
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003055 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00003056 options.master, err_msg = GetBuilderMaster(options.bot)
3057 if err_msg:
3058 parser.error('Tryserver master cannot be found because: %s\n'
3059 'Please manually specify the tryserver master'
3060 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00003061
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003062 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003063 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003064 if not options.bot:
3065 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00003066
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003067 # Get try masters from PRESUBMIT.py files.
3068 masters = presubmit_support.DoGetTryMasters(
3069 change,
3070 change.LocalPaths(),
3071 settings.GetRoot(),
3072 None,
3073 None,
3074 options.verbose,
3075 sys.stdout)
3076 if masters:
3077 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00003078
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003079 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
3080 options.bot = presubmit_support.DoGetTrySlaves(
3081 change,
3082 change.LocalPaths(),
3083 settings.GetRoot(),
3084 None,
3085 None,
3086 options.verbose,
3087 sys.stdout)
3088 if not options.bot:
3089 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00003090
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003091 builders_and_tests = {}
3092 # TODO(machenbach): The old style command-line options don't support
3093 # multiple try masters yet.
3094 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3095 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3096
3097 for bot in old_style:
3098 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003099 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003100 elif ',' in bot:
3101 parser.error('Specify one bot per --bot flag')
3102 else:
3103 builders_and_tests.setdefault(bot, []).append('defaulttests')
3104
3105 for bot, tests in new_style:
3106 builders_and_tests.setdefault(bot, []).extend(tests)
3107
3108 # Return a master map with one master to be backwards compatible. The
3109 # master name defaults to an empty string, which will cause the master
3110 # not to be set on rietveld (deprecated).
3111 return {options.master: builders_and_tests}
3112
3113 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003114
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003115 for builders in masters.itervalues():
3116 if any('triggered' in b for b in builders):
3117 print >> sys.stderr, (
3118 'ERROR You are trying to send a job to a triggered bot. This type of'
3119 ' bot requires an\ninitial job from a parent (usually a builder). '
3120 'Instead send your job to the parent.\n'
3121 'Bot list: %s' % builders)
3122 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003123
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003124 patchset = cl.GetMostRecentPatchset()
3125 if patchset and patchset != cl.GetPatchset():
3126 print(
3127 '\nWARNING Mismatch between local config and server. Did a previous '
3128 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3129 'Continuing using\npatchset %s.\n' % patchset)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003130 if options.use_buildbucket:
3131 try:
3132 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3133 except BuildbucketResponseException as ex:
3134 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003135 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003136 except Exception as e:
3137 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3138 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3139 e, stacktrace)
3140 return 1
3141 else:
3142 try:
3143 cl.RpcServer().trigger_distributed_try_jobs(
3144 cl.GetIssue(), patchset, options.name, options.clobber,
3145 options.revision, masters)
3146 except urllib2.HTTPError as e:
3147 if e.code == 404:
3148 print('404 from rietveld; '
3149 'did you mean to use "git try" instead of "git cl try"?')
3150 return 1
3151 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003152
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003153 for (master, builders) in sorted(masters.iteritems()):
3154 if master:
3155 print 'Master: %s' % master
3156 length = max(len(builder) for builder in builders)
3157 for builder in sorted(builders):
3158 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003159 return 0
3160
3161
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003162@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003163def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003164 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003165 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003166 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003167 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003168
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003169 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003170 if args:
3171 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003172 branch = cl.GetBranch()
3173 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003174 cl = Changelist()
3175 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003176
3177 # Clear configured merge-base, if there is one.
3178 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003179 else:
3180 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003181 return 0
3182
3183
thestig@chromium.org00858c82013-12-02 23:08:03 +00003184def CMDweb(parser, args):
3185 """Opens the current CL in the web browser."""
3186 _, args = parser.parse_args(args)
3187 if args:
3188 parser.error('Unrecognized args: %s' % ' '.join(args))
3189
3190 issue_url = Changelist().GetIssueURL()
3191 if not issue_url:
3192 print >> sys.stderr, 'ERROR No issue to open'
3193 return 1
3194
3195 webbrowser.open(issue_url)
3196 return 0
3197
3198
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003199def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003200 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003201 auth.add_auth_options(parser)
3202 options, args = parser.parse_args(args)
3203 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003204 if args:
3205 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003206 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003207 props = cl.GetIssueProperties()
3208 if props.get('private'):
3209 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003210 cl.SetFlag('commit', '1')
3211 return 0
3212
3213
groby@chromium.org411034a2013-02-26 15:12:01 +00003214def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003215 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003216 auth.add_auth_options(parser)
3217 options, args = parser.parse_args(args)
3218 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003219 if args:
3220 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003221 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003222 # Ensure there actually is an issue to close.
3223 cl.GetDescription()
3224 cl.CloseIssue()
3225 return 0
3226
3227
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003228def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003229 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003230 auth.add_auth_options(parser)
3231 options, args = parser.parse_args(args)
3232 auth_config = auth.extract_auth_config_from_options(options)
3233 if args:
3234 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003235
3236 # Uncommitted (staged and unstaged) changes will be destroyed by
3237 # "git reset --hard" if there are merging conflicts in PatchIssue().
3238 # Staged changes would be committed along with the patch from last
3239 # upload, hence counted toward the "last upload" side in the final
3240 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003241 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003242 return 1
3243
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003244 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003245 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003246 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003247 if not issue:
3248 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003249 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003250 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003251
3252 # Create a new branch based on the merge-base
3253 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3254 try:
3255 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003256 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003257 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003258 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003259 return rtn
3260
wychen@chromium.org06928532015-02-03 02:11:29 +00003261 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003262 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003263 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003264 finally:
3265 RunGit(['checkout', '-q', branch])
3266 RunGit(['branch', '-D', TMP_BRANCH])
3267
3268 return 0
3269
3270
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003271def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003272 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003273 parser.add_option(
3274 '--no-color',
3275 action='store_true',
3276 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003277 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003278 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003279 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003280
3281 author = RunGit(['config', 'user.email']).strip() or None
3282
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003283 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003284
3285 if args:
3286 if len(args) > 1:
3287 parser.error('Unknown args')
3288 base_branch = args[0]
3289 else:
3290 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003291 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003292
3293 change = cl.GetChange(base_branch, None)
3294 return owners_finder.OwnersFinder(
3295 [f.LocalPath() for f in
3296 cl.GetChange(base_branch, None).AffectedFiles()],
3297 change.RepositoryRoot(), author,
3298 fopen=file, os_path=os.path, glob=glob.glob,
3299 disable_color=options.no_color).run()
3300
3301
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003302def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
3303 """Generates a diff command."""
3304 # Generate diff for the current branch's changes.
3305 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3306 upstream_commit, '--' ]
3307
3308 if args:
3309 for arg in args:
3310 if os.path.isdir(arg):
3311 diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
3312 elif os.path.isfile(arg):
3313 diff_cmd.append(arg)
3314 else:
3315 DieWithError('Argument "%s" is not a file or a directory' % arg)
3316 else:
3317 diff_cmd.extend('*' + ext for ext in extensions)
3318
3319 return diff_cmd
3320
3321
enne@chromium.org555cfe42014-01-29 18:21:39 +00003322@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003323def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003324 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003325 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003326 parser.add_option('--full', action='store_true',
3327 help='Reformat the full content of all touched files')
3328 parser.add_option('--dry-run', action='store_true',
3329 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003330 parser.add_option('--python', action='store_true',
3331 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003332 parser.add_option('--diff', action='store_true',
3333 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003334 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003335
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003336 # git diff generates paths against the root of the repository. Change
3337 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003338 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003339 if rel_base_path:
3340 os.chdir(rel_base_path)
3341
digit@chromium.org29e47272013-05-17 17:01:46 +00003342 # Grab the merge-base commit, i.e. the upstream commit of the current
3343 # branch when it was created or the last time it was rebased. This is
3344 # to cover the case where the user may have called "git fetch origin",
3345 # moving the origin branch to a newer commit, but hasn't rebased yet.
3346 upstream_commit = None
3347 cl = Changelist()
3348 upstream_branch = cl.GetUpstreamBranch()
3349 if upstream_branch:
3350 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3351 upstream_commit = upstream_commit.strip()
3352
3353 if not upstream_commit:
3354 DieWithError('Could not find base commit for this branch. '
3355 'Are you in detached state?')
3356
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003357 if opts.full:
3358 # Only list the names of modified files.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003359 diff_type = '--name-only'
enne@chromium.org555cfe42014-01-29 18:21:39 +00003360 else:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003361 # Only generate context-less patches.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003362 diff_type = '-U0'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003363
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003364 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, CLANG_EXTS)
digit@chromium.org29e47272013-05-17 17:01:46 +00003365 diff_output = RunGit(diff_cmd)
3366
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003367 top_dir = os.path.normpath(
3368 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3369
3370 # Locate the clang-format binary in the checkout
3371 try:
3372 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3373 except clang_format.NotFoundError, e:
3374 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003375
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003376 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3377 # formatted. This is used to block during the presubmit.
3378 return_value = 0
3379
digit@chromium.org29e47272013-05-17 17:01:46 +00003380 if opts.full:
3381 # diff_output is a list of files to send to clang-format.
3382 files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003383 if files:
3384 cmd = [clang_format_tool]
3385 if not opts.dry_run and not opts.diff:
3386 cmd.append('-i')
3387 stdout = RunCommand(cmd + files, cwd=top_dir)
3388 if opts.diff:
3389 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003390 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003391 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003392 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00003393 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003394 try:
3395 script = clang_format.FindClangFormatScriptInChromiumTree(
3396 'clang-format-diff.py')
3397 except clang_format.NotFoundError, e:
3398 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003399
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003400 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003401 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003402 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003403
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003404 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003405 if opts.diff:
3406 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003407 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003408 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003409
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003410 # Similar code to above, but using yapf on .py files rather than clang-format
3411 # on C/C++ files
3412 if opts.python:
3413 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, ['.py'])
3414 diff_output = RunGit(diff_cmd)
3415 yapf_tool = gclient_utils.FindExecutable('yapf')
3416 if yapf_tool is None:
3417 DieWithError('yapf not found in PATH')
3418
3419 if opts.full:
3420 files = diff_output.splitlines()
3421 if files:
3422 cmd = [yapf_tool]
3423 if not opts.dry_run and not opts.diff:
3424 cmd.append('-i')
3425 stdout = RunCommand(cmd + files, cwd=top_dir)
3426 if opts.diff:
3427 sys.stdout.write(stdout)
3428 else:
3429 # TODO(sbc): yapf --lines mode still has some issues.
3430 # https://github.com/google/yapf/issues/154
3431 DieWithError('--python currently only works with --full')
3432
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003433 # Build a diff command that only operates on dart files. dart's formatter
3434 # does not have the nice property of only operating on modified chunks, so
3435 # hard code full.
3436 dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3437 args, ['.dart'])
3438 dart_diff_output = RunGit(dart_diff_cmd)
3439 if dart_diff_output:
3440 try:
3441 command = [dart_format.FindDartFmtToolInChromiumTree()]
3442 if not opts.dry_run and not opts.diff:
3443 command.append('-w')
3444 command.extend(dart_diff_output.splitlines())
3445
3446 stdout = RunCommand(command, cwd=top_dir, env=env)
3447 if opts.dry_run and stdout:
3448 return_value = 2
3449 except dart_format.NotFoundError as e:
3450 print ('Unable to check dart code formatting. Dart SDK is not in ' +
3451 'this checkout.')
3452
3453 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003454
3455
maruel@chromium.org29404b52014-09-08 22:58:00 +00003456def CMDlol(parser, args):
3457 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003458 print zlib.decompress(base64.b64decode(
3459 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3460 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3461 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3462 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003463 return 0
3464
3465
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003466class OptionParser(optparse.OptionParser):
3467 """Creates the option parse and add --verbose support."""
3468 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003469 optparse.OptionParser.__init__(
3470 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003471 self.add_option(
3472 '-v', '--verbose', action='count', default=0,
3473 help='Use 2 times for more debugging info')
3474
3475 def parse_args(self, args=None, values=None):
3476 options, args = optparse.OptionParser.parse_args(self, args, values)
3477 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3478 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3479 return options, args
3480
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003481
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003482def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003483 if sys.hexversion < 0x02060000:
3484 print >> sys.stderr, (
3485 '\nYour python version %s is unsupported, please upgrade.\n' %
3486 sys.version.split(' ', 1)[0])
3487 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003488
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003489 # Reload settings.
3490 global settings
3491 settings = Settings()
3492
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003493 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003494 dispatcher = subcommand.CommandDispatcher(__name__)
3495 try:
3496 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003497 except auth.AuthenticationError as e:
3498 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003499 except urllib2.HTTPError, e:
3500 if e.code != 500:
3501 raise
3502 DieWithError(
3503 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3504 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003505 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003506
3507
3508if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003509 # These affect sys.stdout so do it outside of main() to simplify mocks in
3510 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003511 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003512 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003513 try:
3514 sys.exit(main(sys.argv[1:]))
3515 except KeyboardInterrupt:
3516 sys.stderr.write('interrupted\n')
3517 sys.exit(1)