blob: 50ba3432eb230dc484f2036e8a00bb641820e6f2 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000011from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000013import glob
sheyang@google.com6ebaf782015-05-12 19:17:54 +000014import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000015import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import logging
17import optparse
18import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000019import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000021import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000023import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000025import time
26import traceback
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000028import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000029import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000030import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031
32try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000033 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034except ImportError:
35 pass
36
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000037from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000038from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000039from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000040import auth
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000042import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000043import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000044import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000045import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000046import git_common
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +000047from git_footers import get_footer_svn_id
piman@chromium.org336f9122014-09-04 02:16:55 +000048import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000049import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000050import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000051import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000052import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000053import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000054import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000055import watchlists
56
maruel@chromium.org0633fb42013-08-16 20:06:14 +000057__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000059DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000060POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000061DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000062GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000063CHANGE_ID = 'Change-Id:'
rmistry@google.comc68112d2015-03-03 12:48:06 +000064REFS_THAT_ALIAS_TO_OTHER_REFS = {
65 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
66 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
67}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068
sheyang@google.com6ebaf782015-05-12 19:17:54 +000069# Buildbucket-related constants
70BUILDBUCKET_HOST = 'cr-buildbucket.appspot.com'
71
thestig@chromium.org44202a22014-03-11 19:22:18 +000072# Valid extensions for files we want to lint.
73DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
74DEFAULT_LINT_IGNORE_REGEX = r"$^"
75
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000076# Shortcut since it quickly becomes redundant.
77Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000078
maruel@chromium.orgddd59412011-11-30 14:20:38 +000079# Initialized in main()
80settings = None
81
82
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000083def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000084 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000085 sys.exit(1)
86
87
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000088def GetNoGitPagerEnv():
89 env = os.environ.copy()
90 # 'cat' is a magical git string that disables pagers on all platforms.
91 env['GIT_PAGER'] = 'cat'
92 return env
93
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000094
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000095def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000096 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000097 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000098 except subprocess2.CalledProcessError as e:
99 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000100 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000101 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000102 'Command "%s" failed.\n%s' % (
103 ' '.join(args), error_message or e.stdout or ''))
104 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000105
106
107def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000108 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000109 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000110
111
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000112def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000113 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000114 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000115 if suppress_stderr:
116 stderr = subprocess2.VOID
117 else:
118 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000119 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000120 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000121 stdout=subprocess2.PIPE,
122 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000123 return code, out[0]
124 except ValueError:
125 # When the subprocess fails, it returns None. That triggers a ValueError
126 # when trying to unpack the return value into (out, code).
127 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000128
129
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000130def RunGitSilent(args):
131 """Returns stdout, suppresses stderr and ingores the return code."""
132 return RunGitWithCode(args, suppress_stderr=True)[1]
133
134
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000135def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000136 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000137 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000138 return (version.startswith(prefix) and
139 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140
141
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000142def BranchExists(branch):
143 """Return True if specified branch exists."""
144 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
145 suppress_stderr=True)
146 return not code
147
148
maruel@chromium.org90541732011-04-01 17:54:18 +0000149def ask_for_data(prompt):
150 try:
151 return raw_input(prompt)
152 except KeyboardInterrupt:
153 # Hide the exception.
154 sys.exit(1)
155
156
iannucci@chromium.org79540052012-10-19 23:15:26 +0000157def git_set_branch_value(key, value):
158 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000159 if not branch:
160 return
161
162 cmd = ['config']
163 if isinstance(value, int):
164 cmd.append('--int')
165 git_key = 'branch.%s.%s' % (branch, key)
166 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000167
168
169def git_get_branch_default(key, default):
170 branch = Changelist().GetBranch()
171 if branch:
172 git_key = 'branch.%s.%s' % (branch, key)
173 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
174 try:
175 return int(stdout.strip())
176 except ValueError:
177 pass
178 return default
179
180
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000181def add_git_similarity(parser):
182 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000183 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000184 help='Sets the percentage that a pair of files need to match in order to'
185 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000186 parser.add_option(
187 '--find-copies', action='store_true',
188 help='Allows git to look for copies.')
189 parser.add_option(
190 '--no-find-copies', action='store_false', dest='find_copies',
191 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000192
193 old_parser_args = parser.parse_args
194 def Parse(args):
195 options, args = old_parser_args(args)
196
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000197 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000198 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000199 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000200 print('Note: Saving similarity of %d%% in git config.'
201 % options.similarity)
202 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000203
iannucci@chromium.org79540052012-10-19 23:15:26 +0000204 options.similarity = max(0, min(options.similarity, 100))
205
206 if options.find_copies is None:
207 options.find_copies = bool(
208 git_get_branch_default('git-find-copies', True))
209 else:
210 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000211
212 print('Using %d%% similarity for rename/copy detection. '
213 'Override with --similarity.' % options.similarity)
214
215 return options, args
216 parser.parse_args = Parse
217
218
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000219def _prefix_master(master):
220 """Convert user-specified master name to full master name.
221
222 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
223 name, while the developers always use shortened master name
224 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
225 function does the conversion for buildbucket migration.
226 """
227 prefix = 'master.'
228 if master.startswith(prefix):
229 return master
230 return '%s%s' % (prefix, master)
231
232
machenbach@chromium.org79e43ff2015-05-15 05:56:13 +0000233def trigger_try_jobs(auth_config, changelist, options, masters, category,
234 override_properties=None):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000235 rietveld_url = settings.GetDefaultServerUrl()
236 rietveld_host = urlparse.urlparse(rietveld_url).hostname
237 authenticator = auth.get_authenticator_for_host(rietveld_host, auth_config)
238 http = authenticator.authorize(httplib2.Http())
239 http.force_exception_to_status_code = True
240 issue_props = changelist.GetIssueProperties()
241 issue = changelist.GetIssue()
242 patchset = changelist.GetMostRecentPatchset()
243
244 buildbucket_put_url = (
245 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
246 hostname=BUILDBUCKET_HOST))
247 buildset = 'patch/rietveld/{hostname}/{issue}/{patch}'.format(
248 hostname=rietveld_host,
249 issue=issue,
250 patch=patchset)
251
252 batch_req_body = {'builds': []}
253 print_text = []
254 print_text.append('Tried jobs on:')
255 for master, builders_and_tests in sorted(masters.iteritems()):
256 print_text.append('Master: %s' % master)
257 bucket = _prefix_master(master)
258 for builder, tests in sorted(builders_and_tests.iteritems()):
259 print_text.append(' %s: %s' % (builder, tests))
260 parameters = {
261 'builder_name': builder,
262 'changes': [
263 {'author': {'email': issue_props['owner_email']}},
264 ],
265 'properties': {
266 'category': category,
267 'issue': issue,
268 'master': master,
269 'patch_project': issue_props['project'],
270 'patch_storage': 'rietveld',
271 'patchset': patchset,
272 'reason': options.name,
273 'revision': options.revision,
274 'rietveld': rietveld_url,
275 'testfilter': tests,
276 },
277 }
machenbach@chromium.org79e43ff2015-05-15 05:56:13 +0000278 if override_properties:
279 parameters['properties'].update(override_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000280 if options.clobber:
281 parameters['properties']['clobber'] = True
282 batch_req_body['builds'].append(
283 {
284 'bucket': bucket,
285 'parameters_json': json.dumps(parameters),
286 'tags': ['builder:%s' % builder,
287 'buildset:%s' % buildset,
288 'master:%s' % master,
289 'user_agent:git_cl_try']
290 }
291 )
292
293 for try_count in xrange(3):
294 response, content = http.request(
295 buildbucket_put_url,
296 'PUT',
297 body=json.dumps(batch_req_body),
298 headers={'Content-Type': 'application/json'},
299 )
300 content_json = None
301 try:
302 content_json = json.loads(content)
303 except ValueError:
304 pass
305
306 # Buildbucket could return an error even if status==200.
307 if content_json and content_json.get('error'):
308 msg = 'Error in response. Code: %d. Reason: %s. Message: %s.' % (
309 content_json['error'].get('code', ''),
310 content_json['error'].get('reason', ''),
311 content_json['error'].get('message', ''))
312 raise BuildbucketResponseException(msg)
313
314 if response.status == 200:
315 if not content_json:
316 raise BuildbucketResponseException(
317 'Buildbucket returns invalid json content: %s.\n'
318 'Please file bugs at crbug.com, label "Infra-BuildBucket".' %
319 content)
320 break
321 if response.status < 500 or try_count >= 2:
322 raise httplib2.HttpLib2Error(content)
323
324 # status >= 500 means transient failures.
325 logging.debug('Transient errors when triggering tryjobs. Will retry.')
326 time.sleep(0.5 + 1.5*try_count)
327
328 print '\n'.join(print_text)
kjellander@chromium.org44424542015-06-02 18:35:29 +0000329
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000330
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000331def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
332 """Return the corresponding git ref if |base_url| together with |glob_spec|
333 matches the full |url|.
334
335 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
336 """
337 fetch_suburl, as_ref = glob_spec.split(':')
338 if allow_wildcards:
339 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
340 if glob_match:
341 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
342 # "branches/{472,597,648}/src:refs/remotes/svn/*".
343 branch_re = re.escape(base_url)
344 if glob_match.group(1):
345 branch_re += '/' + re.escape(glob_match.group(1))
346 wildcard = glob_match.group(2)
347 if wildcard == '*':
348 branch_re += '([^/]*)'
349 else:
350 # Escape and replace surrounding braces with parentheses and commas
351 # with pipe symbols.
352 wildcard = re.escape(wildcard)
353 wildcard = re.sub('^\\\\{', '(', wildcard)
354 wildcard = re.sub('\\\\,', '|', wildcard)
355 wildcard = re.sub('\\\\}$', ')', wildcard)
356 branch_re += wildcard
357 if glob_match.group(3):
358 branch_re += re.escape(glob_match.group(3))
359 match = re.match(branch_re, url)
360 if match:
361 return re.sub('\*$', match.group(1), as_ref)
362
363 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
364 if fetch_suburl:
365 full_url = base_url + '/' + fetch_suburl
366 else:
367 full_url = base_url
368 if full_url == url:
369 return as_ref
370 return None
371
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000372
iannucci@chromium.org79540052012-10-19 23:15:26 +0000373def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000374 """Prints statistics about the change to the user."""
375 # --no-ext-diff is broken in some versions of Git, so try to work around
376 # this by overriding the environment (but there is still a problem if the
377 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000378 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000379 if 'GIT_EXTERNAL_DIFF' in env:
380 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000381
382 if find_copies:
383 similarity_options = ['--find-copies-harder', '-l100000',
384 '-C%s' % similarity]
385 else:
386 similarity_options = ['-M%s' % similarity]
387
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000388 try:
389 stdout = sys.stdout.fileno()
390 except AttributeError:
391 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000392 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000393 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000394 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000395 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000396
397
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000398class BuildbucketResponseException(Exception):
399 pass
400
401
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000402class Settings(object):
403 def __init__(self):
404 self.default_server = None
405 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000406 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000407 self.is_git_svn = None
408 self.svn_branch = None
409 self.tree_status_url = None
410 self.viewvc_url = None
411 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000412 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000413 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000414 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000415 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000416 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000417
418 def LazyUpdateIfNeeded(self):
419 """Updates the settings from a codereview.settings file, if available."""
420 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000421 # The only value that actually changes the behavior is
422 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000423 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000424 error_ok=True
425 ).strip().lower()
426
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000427 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000428 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000429 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000430 # set updated to True to avoid infinite calling loop
431 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000432 self.updated = True
433 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000434 self.updated = True
435
436 def GetDefaultServerUrl(self, error_ok=False):
437 if not self.default_server:
438 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000439 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000440 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000441 if error_ok:
442 return self.default_server
443 if not self.default_server:
444 error_message = ('Could not find settings file. You must configure '
445 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000446 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000447 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000448 return self.default_server
449
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000450 @staticmethod
451 def GetRelativeRoot():
452 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000453
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000454 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000455 if self.root is None:
456 self.root = os.path.abspath(self.GetRelativeRoot())
457 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000458
459 def GetIsGitSvn(self):
460 """Return true if this repo looks like it's using git-svn."""
461 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000462 if self.GetPendingRefPrefix():
463 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
464 self.is_git_svn = False
465 else:
466 # If you have any "svn-remote.*" config keys, we think you're using svn.
467 self.is_git_svn = RunGitWithCode(
468 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000469 return self.is_git_svn
470
471 def GetSVNBranch(self):
472 if self.svn_branch is None:
473 if not self.GetIsGitSvn():
474 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
475
476 # Try to figure out which remote branch we're based on.
477 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000478 # 1) iterate through our branch history and find the svn URL.
479 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000480
481 # regexp matching the git-svn line that contains the URL.
482 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
483
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000484 # We don't want to go through all of history, so read a line from the
485 # pipe at a time.
486 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000487 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000488 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
489 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000490 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000491 for line in proc.stdout:
492 match = git_svn_re.match(line)
493 if match:
494 url = match.group(1)
495 proc.stdout.close() # Cut pipe.
496 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000497
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000498 if url:
499 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
500 remotes = RunGit(['config', '--get-regexp',
501 r'^svn-remote\..*\.url']).splitlines()
502 for remote in remotes:
503 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000504 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000505 remote = match.group(1)
506 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000507 rewrite_root = RunGit(
508 ['config', 'svn-remote.%s.rewriteRoot' % remote],
509 error_ok=True).strip()
510 if rewrite_root:
511 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000512 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000513 ['config', 'svn-remote.%s.fetch' % remote],
514 error_ok=True).strip()
515 if fetch_spec:
516 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
517 if self.svn_branch:
518 break
519 branch_spec = RunGit(
520 ['config', 'svn-remote.%s.branches' % remote],
521 error_ok=True).strip()
522 if branch_spec:
523 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
524 if self.svn_branch:
525 break
526 tag_spec = RunGit(
527 ['config', 'svn-remote.%s.tags' % remote],
528 error_ok=True).strip()
529 if tag_spec:
530 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
531 if self.svn_branch:
532 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000533
534 if not self.svn_branch:
535 DieWithError('Can\'t guess svn branch -- try specifying it on the '
536 'command line')
537
538 return self.svn_branch
539
540 def GetTreeStatusUrl(self, error_ok=False):
541 if not self.tree_status_url:
542 error_message = ('You must configure your tree status URL by running '
543 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000544 self.tree_status_url = self._GetRietveldConfig(
545 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000546 return self.tree_status_url
547
548 def GetViewVCUrl(self):
549 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000550 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000551 return self.viewvc_url
552
rmistry@google.com90752582014-01-14 21:04:50 +0000553 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000554 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000555
rmistry@google.com5626a922015-02-26 14:03:30 +0000556 def GetRunPostUploadHook(self):
557 run_post_upload_hook = self._GetRietveldConfig(
558 'run-post-upload-hook', error_ok=True)
559 return run_post_upload_hook == "True"
560
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000561 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000562 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000563
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000564 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000565 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000566
ukai@chromium.orge8077812012-02-03 03:41:46 +0000567 def GetIsGerrit(self):
568 """Return true if this repo is assosiated with gerrit code review system."""
569 if self.is_gerrit is None:
570 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
571 return self.is_gerrit
572
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000573 def GetGitEditor(self):
574 """Return the editor specified in the git config, or None if none is."""
575 if self.git_editor is None:
576 self.git_editor = self._GetConfig('core.editor', error_ok=True)
577 return self.git_editor or None
578
thestig@chromium.org44202a22014-03-11 19:22:18 +0000579 def GetLintRegex(self):
580 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
581 DEFAULT_LINT_REGEX)
582
583 def GetLintIgnoreRegex(self):
584 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
585 DEFAULT_LINT_IGNORE_REGEX)
586
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000587 def GetProject(self):
588 if not self.project:
589 self.project = self._GetRietveldConfig('project', error_ok=True)
590 return self.project
591
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000592 def GetForceHttpsCommitUrl(self):
593 if not self.force_https_commit_url:
594 self.force_https_commit_url = self._GetRietveldConfig(
595 'force-https-commit-url', error_ok=True)
596 return self.force_https_commit_url
597
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000598 def GetPendingRefPrefix(self):
599 if not self.pending_ref_prefix:
600 self.pending_ref_prefix = self._GetRietveldConfig(
601 'pending-ref-prefix', error_ok=True)
602 return self.pending_ref_prefix
603
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000604 def _GetRietveldConfig(self, param, **kwargs):
605 return self._GetConfig('rietveld.' + param, **kwargs)
606
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000607 def _GetConfig(self, param, **kwargs):
608 self.LazyUpdateIfNeeded()
609 return RunGit(['config', param], **kwargs).strip()
610
611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000612def ShortBranchName(branch):
613 """Convert a name like 'refs/heads/foo' to just 'foo'."""
614 return branch.replace('refs/heads/', '')
615
616
617class Changelist(object):
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000618 def __init__(self, branchref=None, issue=None, auth_config=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000619 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000620 global settings
621 if not settings:
622 # Happens when git_cl.py is used as a utility library.
623 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000624 settings.GetDefaultServerUrl()
625 self.branchref = branchref
626 if self.branchref:
627 self.branch = ShortBranchName(self.branchref)
628 else:
629 self.branch = None
630 self.rietveld_server = None
631 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000632 self.lookedup_issue = False
633 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000634 self.has_description = False
635 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000636 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000637 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000638 self.cc = None
639 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000640 self._auth_config = auth_config
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000641 self._props = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +0000642 self._remote = None
643 self._rpc_server = None
644
645 @property
646 def auth_config(self):
647 return self._auth_config
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000648
649 def GetCCList(self):
650 """Return the users cc'd on this CL.
651
652 Return is a string suitable for passing to gcl with the --cc flag.
653 """
654 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000655 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000656 more_cc = ','.join(self.watchers)
657 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
658 return self.cc
659
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000660 def GetCCListWithoutDefault(self):
661 """Return the users cc'd on this CL excluding default ones."""
662 if self.cc is None:
663 self.cc = ','.join(self.watchers)
664 return self.cc
665
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000666 def SetWatchers(self, watchers):
667 """Set the list of email addresses that should be cc'd based on the changed
668 files in this CL.
669 """
670 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000671
672 def GetBranch(self):
673 """Returns the short branch name, e.g. 'master'."""
674 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000675 branchref = RunGit(['symbolic-ref', 'HEAD'],
676 stderr=subprocess2.VOID, error_ok=True).strip()
677 if not branchref:
678 return None
679 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000680 self.branch = ShortBranchName(self.branchref)
681 return self.branch
682
683 def GetBranchRef(self):
684 """Returns the full branch name, e.g. 'refs/heads/master'."""
685 self.GetBranch() # Poke the lazy loader.
686 return self.branchref
687
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000688 @staticmethod
689 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000690 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000691 e.g. 'origin', 'refs/heads/master'
692 """
693 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000694 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
695 error_ok=True).strip()
696 if upstream_branch:
697 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
698 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000699 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
700 error_ok=True).strip()
701 if upstream_branch:
702 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000703 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000704 # Fall back on trying a git-svn upstream branch.
705 if settings.GetIsGitSvn():
706 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000707 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000708 # Else, try to guess the origin remote.
709 remote_branches = RunGit(['branch', '-r']).split()
710 if 'origin/master' in remote_branches:
711 # Fall back on origin/master if it exits.
712 remote = 'origin'
713 upstream_branch = 'refs/heads/master'
714 elif 'origin/trunk' in remote_branches:
715 # Fall back on origin/trunk if it exists. Generally a shared
716 # git-svn clone
717 remote = 'origin'
718 upstream_branch = 'refs/heads/trunk'
719 else:
720 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000721Either pass complete "git diff"-style arguments, like
722 git cl upload origin/master
723or verify this branch is set up to track another (via the --track argument to
724"git checkout -b ...").""")
725
726 return remote, upstream_branch
727
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000728 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000729 upstream_branch = self.GetUpstreamBranch()
730 if not BranchExists(upstream_branch):
731 DieWithError('The upstream for the current branch (%s) does not exist '
732 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000733 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000734 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000735
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000736 def GetUpstreamBranch(self):
737 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000738 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000740 upstream_branch = upstream_branch.replace('refs/heads/',
741 'refs/remotes/%s/' % remote)
742 upstream_branch = upstream_branch.replace('refs/branch-heads/',
743 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000744 self.upstream_branch = upstream_branch
745 return self.upstream_branch
746
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000747 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000748 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000749 remote, branch = None, self.GetBranch()
750 seen_branches = set()
751 while branch not in seen_branches:
752 seen_branches.add(branch)
753 remote, branch = self.FetchUpstreamTuple(branch)
754 branch = ShortBranchName(branch)
755 if remote != '.' or branch.startswith('refs/remotes'):
756 break
757 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000758 remotes = RunGit(['remote'], error_ok=True).split()
759 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000760 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000761 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000762 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000763 logging.warning('Could not determine which remote this change is '
764 'associated with, so defaulting to "%s". This may '
765 'not be what you want. You may prevent this message '
766 'by running "git svn info" as documented here: %s',
767 self._remote,
768 GIT_INSTRUCTIONS_URL)
769 else:
770 logging.warn('Could not determine which remote this change is '
771 'associated with. You may prevent this message by '
772 'running "git svn info" as documented here: %s',
773 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000774 branch = 'HEAD'
775 if branch.startswith('refs/remotes'):
776 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000777 elif branch.startswith('refs/branch-heads/'):
778 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000779 else:
780 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000781 return self._remote
782
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000783 def GitSanityChecks(self, upstream_git_obj):
784 """Checks git repo status and ensures diff is from local commits."""
785
sbc@chromium.org79706062015-01-14 21:18:12 +0000786 if upstream_git_obj is None:
787 if self.GetBranch() is None:
788 print >> sys.stderr, (
789 'ERROR: unable to dertermine current branch (detached HEAD?)')
790 else:
791 print >> sys.stderr, (
792 'ERROR: no upstream branch')
793 return False
794
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000795 # Verify the commit we're diffing against is in our current branch.
796 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
797 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
798 if upstream_sha != common_ancestor:
799 print >> sys.stderr, (
800 'ERROR: %s is not in the current branch. You may need to rebase '
801 'your tracking branch' % upstream_sha)
802 return False
803
804 # List the commits inside the diff, and verify they are all local.
805 commits_in_diff = RunGit(
806 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
807 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
808 remote_branch = remote_branch.strip()
809 if code != 0:
810 _, remote_branch = self.GetRemoteBranch()
811
812 commits_in_remote = RunGit(
813 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
814
815 common_commits = set(commits_in_diff) & set(commits_in_remote)
816 if common_commits:
817 print >> sys.stderr, (
818 'ERROR: Your diff contains %d commits already in %s.\n'
819 'Run "git log --oneline %s..HEAD" to get a list of commits in '
820 'the diff. If you are using a custom git flow, you can override'
821 ' the reference used for this check with "git config '
822 'gitcl.remotebranch <git-ref>".' % (
823 len(common_commits), remote_branch, upstream_git_obj))
824 return False
825 return True
826
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000827 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000828 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000829
830 Returns None if it is not set.
831 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000832 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
833 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000834
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000835 def GetGitSvnRemoteUrl(self):
836 """Return the configured git-svn remote URL parsed from git svn info.
837
838 Returns None if it is not set.
839 """
840 # URL is dependent on the current directory.
841 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
842 if data:
843 keys = dict(line.split(': ', 1) for line in data.splitlines()
844 if ': ' in line)
845 return keys.get('URL', None)
846 return None
847
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848 def GetRemoteUrl(self):
849 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
850
851 Returns None if there is no remote.
852 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000853 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000854 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
855
856 # If URL is pointing to a local directory, it is probably a git cache.
857 if os.path.isdir(url):
858 url = RunGit(['config', 'remote.%s.url' % remote],
859 error_ok=True,
860 cwd=url).strip()
861 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000862
863 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000864 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000865 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000866 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000867 self.issue = int(issue) or None if issue else None
868 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000869 return self.issue
870
871 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000872 if not self.rietveld_server:
873 # If we're on a branch then get the server potentially associated
874 # with that branch.
875 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000876 rietveld_server_config = self._RietveldServer()
877 if rietveld_server_config:
878 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
879 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000880 if not self.rietveld_server:
881 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000882 return self.rietveld_server
883
884 def GetIssueURL(self):
885 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000886 if not self.GetIssue():
887 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000888 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
889
890 def GetDescription(self, pretty=False):
891 if not self.has_description:
892 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000893 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000894 try:
895 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000896 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000897 if e.code == 404:
898 DieWithError(
899 ('\nWhile fetching the description for issue %d, received a '
900 '404 (not found)\n'
901 'error. It is likely that you deleted this '
902 'issue on the server. If this is the\n'
903 'case, please run\n\n'
904 ' git cl issue 0\n\n'
905 'to clear the association with the deleted issue. Then run '
906 'this command again.') % issue)
907 else:
908 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000909 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000910 except urllib2.URLError as e:
911 print >> sys.stderr, (
912 'Warning: Failed to retrieve CL description due to network '
913 'failure.')
914 self.description = ''
915
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000916 self.has_description = True
917 if pretty:
918 wrapper = textwrap.TextWrapper()
919 wrapper.initial_indent = wrapper.subsequent_indent = ' '
920 return wrapper.fill(self.description)
921 return self.description
922
923 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000924 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000925 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926 patchset = RunGit(['config', self._PatchsetSetting()],
927 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000928 self.patchset = int(patchset) or None if patchset else None
929 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000930 return self.patchset
931
932 def SetPatchset(self, patchset):
933 """Set this branch's patchset. If patchset=0, clears the patchset."""
934 if patchset:
935 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000936 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000937 else:
938 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000939 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000940 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000941
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000942 def GetMostRecentPatchset(self):
943 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000944
945 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000946 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000947 '/download/issue%s_%s.diff' % (issue, patchset))
948
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000949 def GetIssueProperties(self):
950 if self._props is None:
951 issue = self.GetIssue()
952 if not issue:
953 self._props = {}
954 else:
955 self._props = self.RpcServer().get_issue_properties(issue, True)
956 return self._props
957
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000958 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000959 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000960
apavlov@chromium.orge4efd512014-11-05 09:05:29 +0000961 def AddComment(self, message):
962 return self.RpcServer().add_comment(self.GetIssue(), message)
963
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000964 def SetIssue(self, issue):
965 """Set this branch's issue. If issue=0, clears the issue."""
966 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000967 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000968 RunGit(['config', self._IssueSetting(), str(issue)])
969 if self.rietveld_server:
970 RunGit(['config', self._RietveldServer(), self.rietveld_server])
971 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000972 current_issue = self.GetIssue()
973 if current_issue:
974 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000975 self.issue = None
976 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000977
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000978 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000979 if not self.GitSanityChecks(upstream_branch):
980 DieWithError('\nGit sanity check failure')
981
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000982 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000983 if not root:
984 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000985 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000986
987 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000988 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000989 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000990 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000991 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000992 except subprocess2.CalledProcessError:
993 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000994 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000995 'This branch probably doesn\'t exist anymore. To reset the\n'
996 'tracking branch, please run\n'
997 ' git branch --set-upstream %s trunk\n'
998 'replacing trunk with origin/master or the relevant branch') %
999 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001000
maruel@chromium.org52424302012-08-29 15:14:30 +00001001 issue = self.GetIssue()
1002 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001003 if issue:
1004 description = self.GetDescription()
1005 else:
1006 # If the change was never uploaded, use the log messages of all commits
1007 # up to the branch point, as git cl upload will prefill the description
1008 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001009 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1010 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001011
1012 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001013 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001014 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001015 name,
1016 description,
1017 absroot,
1018 files,
1019 issue,
1020 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001021 author,
1022 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001023
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001024 def GetStatus(self):
1025 """Apply a rough heuristic to give a simple summary of an issue's review
1026 or CQ status, assuming adherence to a common workflow.
1027
1028 Returns None if no issue for this branch, or one of the following keywords:
1029 * 'error' - error from review tool (including deleted issues)
1030 * 'unsent' - not sent for review
1031 * 'waiting' - waiting for review
1032 * 'reply' - waiting for owner to reply to review
1033 * 'lgtm' - LGTM from at least one approved reviewer
1034 * 'commit' - in the commit queue
1035 * 'closed' - closed
1036 """
1037 if not self.GetIssue():
1038 return None
1039
1040 try:
1041 props = self.GetIssueProperties()
1042 except urllib2.HTTPError:
1043 return 'error'
1044
1045 if props.get('closed'):
1046 # Issue is closed.
1047 return 'closed'
1048 if props.get('commit'):
1049 # Issue is in the commit queue.
1050 return 'commit'
1051
1052 try:
1053 reviewers = self.GetApprovingReviewers()
1054 except urllib2.HTTPError:
1055 return 'error'
1056
1057 if reviewers:
1058 # Was LGTM'ed.
1059 return 'lgtm'
1060
1061 messages = props.get('messages') or []
1062
1063 if not messages:
1064 # No message was sent.
1065 return 'unsent'
1066 if messages[-1]['sender'] != props.get('owner_email'):
1067 # Non-LGTM reply from non-owner
1068 return 'reply'
1069 return 'waiting'
1070
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001071 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001072 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001073
1074 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001075 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001076 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001077 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +00001078 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001079 except presubmit_support.PresubmitFailure, e:
1080 DieWithError(
1081 ('%s\nMaybe your depot_tools is out of date?\n'
1082 'If all fails, contact maruel@') % e)
1083
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001084 def UpdateDescription(self, description):
1085 self.description = description
1086 return self.RpcServer().update_description(
1087 self.GetIssue(), self.description)
1088
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001089 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +00001090 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001091 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001093 def SetFlag(self, flag, value):
1094 """Patchset must match."""
1095 if not self.GetPatchset():
1096 DieWithError('The patchset needs to match. Send another patchset.')
1097 try:
1098 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +00001099 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001100 except urllib2.HTTPError, e:
1101 if e.code == 404:
1102 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
1103 if e.code == 403:
1104 DieWithError(
1105 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
1106 'match?') % (self.GetIssue(), self.GetPatchset()))
1107 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001108
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001109 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001110 """Returns an upload.RpcServer() to access this review's rietveld instance.
1111 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001112 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00001113 self._rpc_server = rietveld.CachingRietveld(
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001114 self.GetRietveldServer(),
1115 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001116 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001117
1118 def _IssueSetting(self):
1119 """Return the git setting that stores this change's issue."""
1120 return 'branch.%s.rietveldissue' % self.GetBranch()
1121
1122 def _PatchsetSetting(self):
1123 """Return the git setting that stores this change's most recent patchset."""
1124 return 'branch.%s.rietveldpatchset' % self.GetBranch()
1125
1126 def _RietveldServer(self):
1127 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001128 branch = self.GetBranch()
1129 if branch:
1130 return 'branch.%s.rietveldserver' % branch
1131 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132
1133
1134def GetCodereviewSettingsInteractively():
1135 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001136 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137 server = settings.GetDefaultServerUrl(error_ok=True)
1138 prompt = 'Rietveld server (host[:port])'
1139 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001140 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141 if not server and not newserver:
1142 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001143 if newserver:
1144 newserver = gclient_utils.UpgradeToHttps(newserver)
1145 if newserver != server:
1146 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001147
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001148 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149 prompt = caption
1150 if initial:
1151 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001152 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153 if new_val == 'x':
1154 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001155 elif new_val:
1156 if is_url:
1157 new_val = gclient_utils.UpgradeToHttps(new_val)
1158 if new_val != initial:
1159 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001161 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001162 SetProperty(settings.GetDefaultPrivateFlag(),
1163 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001164 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001165 'tree-status-url', False)
1166 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001167 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
rmistry@google.com5626a922015-02-26 14:03:30 +00001168 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
1169 'run-post-upload-hook', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170
1171 # TODO: configure a default branch to diff against, rather than this
1172 # svn-based hackery.
1173
1174
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001175class ChangeDescription(object):
1176 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001177 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001178 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001179
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001180 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001181 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001182
agable@chromium.org42c20792013-09-12 17:34:49 +00001183 @property # www.logilab.org/ticket/89786
1184 def description(self): # pylint: disable=E0202
1185 return '\n'.join(self._description_lines)
1186
1187 def set_description(self, desc):
1188 if isinstance(desc, basestring):
1189 lines = desc.splitlines()
1190 else:
1191 lines = [line.rstrip() for line in desc]
1192 while lines and not lines[0]:
1193 lines.pop(0)
1194 while lines and not lines[-1]:
1195 lines.pop(-1)
1196 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001197
piman@chromium.org336f9122014-09-04 02:16:55 +00001198 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001199 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001200 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001201 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001202 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001203 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001204
agable@chromium.org42c20792013-09-12 17:34:49 +00001205 # Get the set of R= and TBR= lines and remove them from the desciption.
1206 regexp = re.compile(self.R_LINE)
1207 matches = [regexp.match(line) for line in self._description_lines]
1208 new_desc = [l for i, l in enumerate(self._description_lines)
1209 if not matches[i]]
1210 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001211
agable@chromium.org42c20792013-09-12 17:34:49 +00001212 # Construct new unified R= and TBR= lines.
1213 r_names = []
1214 tbr_names = []
1215 for match in matches:
1216 if not match:
1217 continue
1218 people = cleanup_list([match.group(2).strip()])
1219 if match.group(1) == 'TBR':
1220 tbr_names.extend(people)
1221 else:
1222 r_names.extend(people)
1223 for name in r_names:
1224 if name not in reviewers:
1225 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001226 if add_owners_tbr:
1227 owners_db = owners.Database(change.RepositoryRoot(),
1228 fopen=file, os_path=os.path, glob=glob.glob)
1229 all_reviewers = set(tbr_names + reviewers)
1230 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1231 all_reviewers)
1232 tbr_names.extend(owners_db.reviewers_for(missing_files,
1233 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001234 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1235 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1236
1237 # Put the new lines in the description where the old first R= line was.
1238 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1239 if 0 <= line_loc < len(self._description_lines):
1240 if new_tbr_line:
1241 self._description_lines.insert(line_loc, new_tbr_line)
1242 if new_r_line:
1243 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001244 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001245 if new_r_line:
1246 self.append_footer(new_r_line)
1247 if new_tbr_line:
1248 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001249
1250 def prompt(self):
1251 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001252 self.set_description([
1253 '# Enter a description of the change.',
1254 '# This will be displayed on the codereview site.',
1255 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001256 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001257 '--------------------',
1258 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001259
agable@chromium.org42c20792013-09-12 17:34:49 +00001260 regexp = re.compile(self.BUG_LINE)
1261 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001262 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001263 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001264 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001265 if not content:
1266 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001267 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001268
1269 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001270 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1271 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001272 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001273 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001274
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001275 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001276 if self._description_lines:
1277 # Add an empty line if either the last line or the new line isn't a tag.
1278 last_line = self._description_lines[-1]
1279 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1280 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1281 self._description_lines.append('')
1282 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001283
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001284 def get_reviewers(self):
1285 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001286 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1287 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001288 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001289
1290
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001291def get_approving_reviewers(props):
1292 """Retrieves the reviewers that approved a CL from the issue properties with
1293 messages.
1294
1295 Note that the list may contain reviewers that are not committer, thus are not
1296 considered by the CQ.
1297 """
1298 return sorted(
1299 set(
1300 message['sender']
1301 for message in props['messages']
1302 if message['approval'] and message['sender'] in props['reviewers']
1303 )
1304 )
1305
1306
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001307def FindCodereviewSettingsFile(filename='codereview.settings'):
1308 """Finds the given file starting in the cwd and going up.
1309
1310 Only looks up to the top of the repository unless an
1311 'inherit-review-settings-ok' file exists in the root of the repository.
1312 """
1313 inherit_ok_file = 'inherit-review-settings-ok'
1314 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001315 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001316 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1317 root = '/'
1318 while True:
1319 if filename in os.listdir(cwd):
1320 if os.path.isfile(os.path.join(cwd, filename)):
1321 return open(os.path.join(cwd, filename))
1322 if cwd == root:
1323 break
1324 cwd = os.path.dirname(cwd)
1325
1326
1327def LoadCodereviewSettingsFromFile(fileobj):
1328 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001329 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001330
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001331 def SetProperty(name, setting, unset_error_ok=False):
1332 fullname = 'rietveld.' + name
1333 if setting in keyvals:
1334 RunGit(['config', fullname, keyvals[setting]])
1335 else:
1336 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1337
1338 SetProperty('server', 'CODE_REVIEW_SERVER')
1339 # Only server setting is required. Other settings can be absent.
1340 # In that case, we ignore errors raised during option deletion attempt.
1341 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001342 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001343 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1344 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001345 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001346 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001347 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1348 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001349 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001350 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001351 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00001352 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
1353 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001354
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001355 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001356 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001357
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001358 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1359 #should be of the form
1360 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1361 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1362 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1363 keyvals['ORIGIN_URL_CONFIG']])
1364
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001365
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001366def urlretrieve(source, destination):
1367 """urllib is broken for SSL connections via a proxy therefore we
1368 can't use urllib.urlretrieve()."""
1369 with open(destination, 'w') as f:
1370 f.write(urllib2.urlopen(source).read())
1371
1372
ukai@chromium.org712d6102013-11-27 00:52:58 +00001373def hasSheBang(fname):
1374 """Checks fname is a #! script."""
1375 with open(fname) as f:
1376 return f.read(2).startswith('#!')
1377
1378
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001379def DownloadHooks(force):
1380 """downloads hooks
1381
1382 Args:
1383 force: True to update hooks. False to install hooks if not present.
1384 """
1385 if not settings.GetIsGerrit():
1386 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001387 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001388 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1389 if not os.access(dst, os.X_OK):
1390 if os.path.exists(dst):
1391 if not force:
1392 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001393 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001394 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001395 if not hasSheBang(dst):
1396 DieWithError('Not a script: %s\n'
1397 'You need to download from\n%s\n'
1398 'into .git/hooks/commit-msg and '
1399 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001400 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1401 except Exception:
1402 if os.path.exists(dst):
1403 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001404 DieWithError('\nFailed to download hooks.\n'
1405 'You need to download from\n%s\n'
1406 'into .git/hooks/commit-msg and '
1407 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001408
1409
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001410@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001412 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001414 parser.add_option('--activate-update', action='store_true',
1415 help='activate auto-updating [rietveld] section in '
1416 '.git/config')
1417 parser.add_option('--deactivate-update', action='store_true',
1418 help='deactivate auto-updating [rietveld] section in '
1419 '.git/config')
1420 options, args = parser.parse_args(args)
1421
1422 if options.deactivate_update:
1423 RunGit(['config', 'rietveld.autoupdate', 'false'])
1424 return
1425
1426 if options.activate_update:
1427 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1428 return
1429
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001430 if len(args) == 0:
1431 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001432 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001433 return 0
1434
1435 url = args[0]
1436 if not url.endswith('codereview.settings'):
1437 url = os.path.join(url, 'codereview.settings')
1438
1439 # Load code review settings and download hooks (if available).
1440 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001441 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001442 return 0
1443
1444
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001445def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001446 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001447 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1448 branch = ShortBranchName(branchref)
1449 _, args = parser.parse_args(args)
1450 if not args:
1451 print("Current base-url:")
1452 return RunGit(['config', 'branch.%s.base-url' % branch],
1453 error_ok=False).strip()
1454 else:
1455 print("Setting base-url to %s" % args[0])
1456 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1457 error_ok=False).strip()
1458
1459
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001460def color_for_status(status):
1461 """Maps a Changelist status to color, for CMDstatus and other tools."""
1462 return {
1463 'unsent': Fore.RED,
1464 'waiting': Fore.BLUE,
1465 'reply': Fore.YELLOW,
1466 'lgtm': Fore.GREEN,
1467 'commit': Fore.MAGENTA,
1468 'closed': Fore.CYAN,
1469 'error': Fore.WHITE,
1470 }.get(status, Fore.WHITE)
1471
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001472def fetch_cl_status(branch, auth_config=None):
1473 """Fetches information for an issue and returns (branch, issue, status)."""
1474 cl = Changelist(branchref=branch, auth_config=auth_config)
1475 url = cl.GetIssueURL()
1476 status = cl.GetStatus()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001477
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001478 if url and (not status or status == 'error'):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001479 # The issue probably doesn't exist anymore.
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001480 url += ' (broken)'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001481
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001482 return (branch, url, status)
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001483
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001484def get_cl_statuses(
1485 branches, fine_grained, max_processes=None, auth_config=None):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001486 """Returns a blocking iterable of (branch, issue, color) for given branches.
1487
1488 If fine_grained is true, this will fetch CL statuses from the server.
1489 Otherwise, simply indicate if there's a matching url for the given branches.
1490
1491 If max_processes is specified, it is used as the maximum number of processes
1492 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
1493 spawned.
1494 """
1495 # Silence upload.py otherwise it becomes unwieldly.
1496 upload.verbosity = 0
1497
1498 if fine_grained:
1499 # Process one branch synchronously to work through authentication, then
1500 # spawn processes to process all the other branches in parallel.
1501 if branches:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001502 fetch = lambda branch: fetch_cl_status(branch, auth_config=auth_config)
1503 yield fetch(branches[0])
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001504
1505 branches_to_fetch = branches[1:]
1506 pool = ThreadPool(
1507 min(max_processes, len(branches_to_fetch))
1508 if max_processes is not None
1509 else len(branches_to_fetch))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001510 for x in pool.imap_unordered(fetch, branches_to_fetch):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001511 yield x
1512 else:
1513 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1514 for b in branches:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001515 cl = Changelist(branchref=b, auth_config=auth_config)
1516 url = cl.GetIssueURL()
1517 yield (b, url, 'waiting' if url else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001518
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001519def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001520 """Show status of changelists.
1521
1522 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001523 - Red not sent for review or broken
1524 - Blue waiting for review
1525 - Yellow waiting for you to reply to review
1526 - Green LGTM'ed
1527 - Magenta in the commit queue
1528 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001529
1530 Also see 'git cl comments'.
1531 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001532 parser.add_option('--field',
1533 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001534 parser.add_option('-f', '--fast', action='store_true',
1535 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001536 parser.add_option(
1537 '-j', '--maxjobs', action='store', type=int,
1538 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001539
1540 auth.add_auth_options(parser)
1541 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001542 if args:
1543 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001544 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001545
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001546 if options.field:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001547 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001548 if options.field.startswith('desc'):
1549 print cl.GetDescription()
1550 elif options.field == 'id':
1551 issueid = cl.GetIssue()
1552 if issueid:
1553 print issueid
1554 elif options.field == 'patch':
1555 patchset = cl.GetPatchset()
1556 if patchset:
1557 print patchset
1558 elif options.field == 'url':
1559 url = cl.GetIssueURL()
1560 if url:
1561 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001562 return 0
1563
1564 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1565 if not branches:
1566 print('No local branch found.')
1567 return 0
1568
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001569 changes = (
1570 Changelist(branchref=b, auth_config=auth_config)
1571 for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001572 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001573 alignment = max(5, max(len(b) for b in branches))
1574 print 'Branches associated with reviews:'
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001575 output = get_cl_statuses(branches,
1576 fine_grained=not options.fast,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001577 max_processes=options.maxjobs,
1578 auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001579
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001580 branch_statuses = {}
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001581 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001582 for branch in sorted(branches):
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00001583 while branch not in branch_statuses:
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001584 b, i, status = output.next()
1585 branch_statuses[b] = (i, status)
1586 issue_url, status = branch_statuses.pop(branch)
1587 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001588 reset = Fore.RESET
1589 if not sys.stdout.isatty():
1590 color = ''
1591 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00001592 status_str = '(%s)' % status if status else ''
1593 print ' %*s : %s%s %s%s' % (
1594 alignment, ShortBranchName(branch), color, issue_url, status_str,
1595 reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001596
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001597 cl = Changelist(auth_config=auth_config)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001598 print
1599 print 'Current branch:',
1600 if not cl.GetIssue():
1601 print 'no issue assigned.'
1602 return 0
1603 print cl.GetBranch()
1604 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001605 if not options.fast:
1606 print 'Issue description:'
1607 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001608 return 0
1609
1610
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001611def colorize_CMDstatus_doc():
1612 """To be called once in main() to add colors to git cl status help."""
1613 colors = [i for i in dir(Fore) if i[0].isupper()]
1614
1615 def colorize_line(line):
1616 for color in colors:
1617 if color in line.upper():
1618 # Extract whitespaces first and the leading '-'.
1619 indent = len(line) - len(line.lstrip(' ')) + 1
1620 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1621 return line
1622
1623 lines = CMDstatus.__doc__.splitlines()
1624 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1625
1626
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001627@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001628def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001629 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001630
1631 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001632 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00001633 parser.add_option('-r', '--reverse', action='store_true',
1634 help='Lookup the branch(es) for the specified issues. If '
1635 'no issues are specified, all branches with mapped '
1636 'issues will be listed.')
1637 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001638
dnj@chromium.org406c4402015-03-03 17:22:28 +00001639 if options.reverse:
1640 branches = RunGit(['for-each-ref', 'refs/heads',
1641 '--format=%(refname:short)']).splitlines()
1642
1643 # Reverse issue lookup.
1644 issue_branch_map = {}
1645 for branch in branches:
1646 cl = Changelist(branchref=branch)
1647 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
1648 if not args:
1649 args = sorted(issue_branch_map.iterkeys())
1650 for issue in args:
1651 if not issue:
1652 continue
1653 print 'Branch for issue number %s: %s' % (
1654 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',)))
1655 else:
1656 cl = Changelist()
1657 if len(args) > 0:
1658 try:
1659 issue = int(args[0])
1660 except ValueError:
1661 DieWithError('Pass a number to set the issue or none to list it.\n'
1662 'Maybe you want to run git cl status?')
1663 cl.SetIssue(issue)
1664 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001665 return 0
1666
1667
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001668def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001669 """Shows or posts review comments for any changelist."""
1670 parser.add_option('-a', '--add-comment', dest='comment',
1671 help='comment to add to an issue')
1672 parser.add_option('-i', dest='issue',
1673 help="review issue id (defaults to current issue)")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001674 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001675 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001676 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001677
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001678 issue = None
1679 if options.issue:
1680 try:
1681 issue = int(options.issue)
1682 except ValueError:
1683 DieWithError('A review issue id is expected to be a number')
1684
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001685 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001686
1687 if options.comment:
1688 cl.AddComment(options.comment)
1689 return 0
1690
1691 data = cl.GetIssueProperties()
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001692 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001693 if message['disapproval']:
1694 color = Fore.RED
1695 elif message['approval']:
1696 color = Fore.GREEN
1697 elif message['sender'] == data['owner_email']:
1698 color = Fore.MAGENTA
1699 else:
1700 color = Fore.BLUE
1701 print '\n%s%s %s%s' % (
1702 color, message['date'].split('.', 1)[0], message['sender'],
1703 Fore.RESET)
1704 if message['text'].strip():
1705 print '\n'.join(' ' + l for l in message['text'].splitlines())
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001706 return 0
1707
1708
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001709def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001710 """Brings up the editor for the current CL's description."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001711 auth.add_auth_options(parser)
1712 options, _ = parser.parse_args(args)
1713 auth_config = auth.extract_auth_config_from_options(options)
1714 cl = Changelist(auth_config=auth_config)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001715 if not cl.GetIssue():
1716 DieWithError('This branch has no associated changelist.')
1717 description = ChangeDescription(cl.GetDescription())
1718 description.prompt()
wychen@chromium.org063e4e52015-04-03 06:51:44 +00001719 if cl.GetDescription() != description.description:
1720 cl.UpdateDescription(description.description)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001721 return 0
1722
1723
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001724def CreateDescriptionFromLog(args):
1725 """Pulls out the commit log to use as a base for the CL description."""
1726 log_args = []
1727 if len(args) == 1 and not args[0].endswith('.'):
1728 log_args = [args[0] + '..']
1729 elif len(args) == 1 and args[0].endswith('...'):
1730 log_args = [args[0][:-1]]
1731 elif len(args) == 2:
1732 log_args = [args[0] + '..' + args[1]]
1733 else:
1734 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001735 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001736
1737
thestig@chromium.org44202a22014-03-11 19:22:18 +00001738def CMDlint(parser, args):
1739 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001740 parser.add_option('--filter', action='append', metavar='-x,+y',
1741 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001742 auth.add_auth_options(parser)
1743 options, args = parser.parse_args(args)
1744 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001745
1746 # Access to a protected member _XX of a client class
1747 # pylint: disable=W0212
1748 try:
1749 import cpplint
1750 import cpplint_chromium
1751 except ImportError:
1752 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1753 return 1
1754
1755 # Change the current working directory before calling lint so that it
1756 # shows the correct base.
1757 previous_cwd = os.getcwd()
1758 os.chdir(settings.GetRoot())
1759 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001760 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001761 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1762 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001763 if not files:
1764 print "Cannot lint an empty CL"
1765 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001766
1767 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001768 command = args + files
1769 if options.filter:
1770 command = ['--filter=' + ','.join(options.filter)] + command
1771 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001772
1773 white_regex = re.compile(settings.GetLintRegex())
1774 black_regex = re.compile(settings.GetLintIgnoreRegex())
1775 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1776 for filename in filenames:
1777 if white_regex.match(filename):
1778 if black_regex.match(filename):
1779 print "Ignoring file %s" % filename
1780 else:
1781 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1782 extra_check_functions)
1783 else:
1784 print "Skipping file %s" % filename
1785 finally:
1786 os.chdir(previous_cwd)
1787 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1788 if cpplint._cpplint_state.error_count != 0:
1789 return 1
1790 return 0
1791
1792
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001793def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001794 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001795 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001796 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001797 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001798 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001799 auth.add_auth_options(parser)
1800 options, args = parser.parse_args(args)
1801 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001802
sbc@chromium.org71437c02015-04-09 19:29:40 +00001803 if not options.force and git_common.is_dirty_git_tree('presubmit'):
ukai@chromium.org259e4682012-10-25 07:36:33 +00001804 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001805 return 1
1806
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001807 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001808 if args:
1809 base_branch = args[0]
1810 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001811 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001812 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001813
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001814 cl.RunHook(
1815 committing=not options.upload,
1816 may_prompt=False,
1817 verbose=options.verbose,
1818 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001819 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001820
1821
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001822def AddChangeIdToCommitMessage(options, args):
1823 """Re-commits using the current message, assumes the commit hook is in
1824 place.
1825 """
1826 log_desc = options.message or CreateDescriptionFromLog(args)
1827 git_command = ['commit', '--amend', '-m', log_desc]
1828 RunGit(git_command)
1829 new_log_desc = CreateDescriptionFromLog(args)
1830 if CHANGE_ID in new_log_desc:
1831 print 'git-cl: Added Change-Id to commit message.'
1832 else:
1833 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1834
1835
piman@chromium.org336f9122014-09-04 02:16:55 +00001836def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001837 """upload the current branch to gerrit."""
1838 # We assume the remote called "origin" is the one we want.
1839 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001840 gerrit_remote = 'origin'
luqui@chromium.org609f3952015-05-04 22:47:04 +00001841
1842 remote, remote_branch = cl.GetRemoteBranch()
1843 branch = GetTargetRef(remote, remote_branch, options.target_branch,
1844 pending_prefix='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001845
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001846 change_desc = ChangeDescription(
1847 options.message or CreateDescriptionFromLog(args))
1848 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001849 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001850 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001851
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001852 if options.squash:
1853 # Try to get the message from a previous upload.
1854 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
1855 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
1856 if not message:
1857 if not options.force:
1858 change_desc.prompt()
1859
1860 if CHANGE_ID not in change_desc.description:
1861 # Run the commit-msg hook without modifying the head commit by writing
1862 # the commit message to a temporary file and running the hook over it,
1863 # then reading the file back in.
1864 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
1865 'commit-msg')
1866 file_handle, msg_file = tempfile.mkstemp(text=True,
1867 prefix='commit_msg')
1868 try:
1869 try:
1870 with os.fdopen(file_handle, 'w') as fileobj:
1871 fileobj.write(change_desc.description)
1872 finally:
1873 os.close(file_handle)
1874 RunCommand([commit_msg_hook, msg_file])
1875 change_desc.set_description(gclient_utils.FileRead(msg_file))
1876 finally:
1877 os.remove(msg_file)
1878
1879 if not change_desc.description:
1880 print "Description is empty; aborting."
1881 return 1
1882
1883 message = change_desc.description
1884
1885 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1886 if remote is '.':
1887 # If our upstream branch is local, we base our squashed commit on its
1888 # squashed version.
1889 parent = ('refs/heads/git_cl_uploads/' +
1890 scm.GIT.ShortBranchName(upstream_branch))
1891
1892 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
1893 # will create additional CLs when uploading.
1894 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
1895 RunGitSilent(['rev-parse', parent + ':'])):
1896 print 'Upload upstream branch ' + upstream_branch + ' first.'
1897 return 1
1898 else:
1899 parent = cl.GetCommonAncestorWithUpstream()
1900
1901 tree = RunGit(['rev-parse', 'HEAD:']).strip()
1902 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
1903 '-m', message]).strip()
1904 else:
1905 if CHANGE_ID not in change_desc.description:
1906 AddChangeIdToCommitMessage(options, args)
1907 ref_to_push = 'HEAD'
1908 parent = '%s/%s' % (gerrit_remote, branch)
1909
1910 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
1911 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001912 if len(commits) > 1:
1913 print('WARNING: This will upload %d commits. Run the following command '
1914 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001915 print('git log %s..%s' % (parent, ref_to_push))
1916 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001917 'commit.')
1918 ask_for_data('About to upload; enter to confirm.')
1919
piman@chromium.org336f9122014-09-04 02:16:55 +00001920 if options.reviewers or options.tbr_owners:
1921 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001922
ukai@chromium.orge8077812012-02-03 03:41:46 +00001923 receive_options = []
1924 cc = cl.GetCCList().split(',')
1925 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001926 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001927 cc = filter(None, cc)
1928 if cc:
1929 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001930 if change_desc.get_reviewers():
1931 receive_options.extend(
1932 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001933
ukai@chromium.orge8077812012-02-03 03:41:46 +00001934 git_command = ['push']
1935 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001936 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001937 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001938 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00001939 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001940
1941 if options.squash:
1942 head = RunGit(['rev-parse', 'HEAD']).strip()
1943 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
1944
ukai@chromium.orge8077812012-02-03 03:41:46 +00001945 # TODO(ukai): parse Change-Id: and set issue number?
1946 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001947
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001948
wittman@chromium.org455dc922015-01-26 20:15:50 +00001949def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
1950 """Computes the remote branch ref to use for the CL.
1951
1952 Args:
1953 remote (str): The git remote for the CL.
1954 remote_branch (str): The git remote branch for the CL.
1955 target_branch (str): The target branch specified by the user.
1956 pending_prefix (str): The pending prefix from the settings.
1957 """
1958 if not (remote and remote_branch):
1959 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001960
wittman@chromium.org455dc922015-01-26 20:15:50 +00001961 if target_branch:
1962 # Cannonicalize branch references to the equivalent local full symbolic
1963 # refs, which are then translated into the remote full symbolic refs
1964 # below.
1965 if '/' not in target_branch:
1966 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
1967 else:
1968 prefix_replacements = (
1969 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
1970 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
1971 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
1972 )
1973 match = None
1974 for regex, replacement in prefix_replacements:
1975 match = re.search(regex, target_branch)
1976 if match:
1977 remote_branch = target_branch.replace(match.group(0), replacement)
1978 break
1979 if not match:
1980 # This is a branch path but not one we recognize; use as-is.
1981 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00001982 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
1983 # Handle the refs that need to land in different refs.
1984 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001985
wittman@chromium.org455dc922015-01-26 20:15:50 +00001986 # Create the true path to the remote branch.
1987 # Does the following translation:
1988 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
1989 # * refs/remotes/origin/master -> refs/heads/master
1990 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
1991 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
1992 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
1993 elif remote_branch.startswith('refs/remotes/%s/' % remote):
1994 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
1995 'refs/heads/')
1996 elif remote_branch.startswith('refs/remotes/branch-heads'):
1997 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
1998 # If a pending prefix exists then replace refs/ with it.
1999 if pending_prefix:
2000 remote_branch = remote_branch.replace('refs/', pending_prefix)
2001 return remote_branch
2002
2003
piman@chromium.org336f9122014-09-04 02:16:55 +00002004def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002005 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002006 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2007 upload_args.extend(['--server', cl.GetRietveldServer()])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002008 upload_args.extend(auth.auth_config_to_command_options(cl.auth_config))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002009 if options.emulate_svn_auto_props:
2010 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002011
2012 change_desc = None
2013
pgervais@chromium.org91141372014-01-09 23:27:20 +00002014 if options.email is not None:
2015 upload_args.extend(['--email', options.email])
2016
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002017 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002018 if options.title:
2019 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00002020 if options.message:
2021 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00002022 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002023 print ("This branch is associated with issue %s. "
2024 "Adding patch to that issue." % cl.GetIssue())
2025 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002026 if options.title:
2027 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00002028 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002029 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00002030 if options.reviewers or options.tbr_owners:
2031 change_desc.update_reviewers(options.reviewers,
2032 options.tbr_owners,
2033 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002034 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002035 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002036
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002037 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002038 print "Description is empty; aborting."
2039 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002040
maruel@chromium.org71e12a92012-02-14 02:34:15 +00002041 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002042 if change_desc.get_reviewers():
2043 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00002044 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002045 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00002046 DieWithError("Must specify reviewers to send email.")
2047 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00002048
2049 # We check this before applying rietveld.private assuming that in
2050 # rietveld.cc only addresses which we can send private CLs to are listed
2051 # if rietveld.private is set, and so we should ignore rietveld.cc only when
2052 # --private is specified explicitly on the command line.
2053 if options.private:
2054 logging.warn('rietveld.cc is ignored since private flag is specified. '
2055 'You need to review and add them manually if necessary.')
2056 cc = cl.GetCCListWithoutDefault()
2057 else:
2058 cc = cl.GetCCList()
2059 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00002060 if cc:
2061 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002062
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002063 if options.private or settings.GetDefaultPrivateFlag() == "True":
2064 upload_args.append('--private')
2065
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002066 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00002067 if not options.find_copies:
2068 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002069
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002070 # Include the upstream repo's URL in the change -- this is useful for
2071 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002072 remote_url = cl.GetGitBaseUrlFromConfig()
2073 if not remote_url:
2074 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002075 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00002076 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00002077 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
2078 remote_url = (cl.GetRemoteUrl() + '@'
2079 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002080 if remote_url:
2081 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00002082 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00002083 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2084 settings.GetPendingRefPrefix())
2085 if target_ref:
2086 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002087
rmistry@google.comc2b9bd02015-06-22 12:19:22 +00002088 # Look for dependent patchsets. See crbug.com/480453 for more details.
2089 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2090 upstream_branch = ShortBranchName(upstream_branch)
2091 if remote is '.':
2092 # A local branch is being tracked.
2093 local_branch = ShortBranchName(upstream_branch)
2094 auth_config = auth.extract_auth_config_from_options(options)
2095 branch_cl = Changelist(branchref=local_branch, auth_config=auth_config)
2096 branch_cl_issue_url = branch_cl.GetIssueURL()
2097 branch_cl_issue = branch_cl.GetIssue()
2098 branch_cl_patchset = branch_cl.GetPatchset()
2099 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2100 upload_args.extend(
2101 ['--depends_on_patchset', '%s:%s' % (
2102 branch_cl_issue, branch_cl_patchset)])
2103 print
2104 print ('The current branch (%s) is tracking a local branch (%s) with '
2105 'an open CL.') % (cl.GetBranch(), local_branch)
2106 print 'Adding %s/#ps%s as a dependency patchset.' % (
2107 branch_cl_issue_url, branch_cl_patchset)
2108 print
2109
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002110 project = settings.GetProject()
2111 if project:
2112 upload_args.extend(['--project', project])
2113
rmistry@google.comef966222015-04-07 11:15:01 +00002114 if options.cq_dry_run:
2115 upload_args.extend(['--cq_dry_run'])
2116
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002117 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002118 upload_args = ['upload'] + upload_args + args
2119 logging.info('upload.RealMain(%s)', upload_args)
2120 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002121 issue = int(issue)
2122 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002123 except KeyboardInterrupt:
2124 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002125 except:
2126 # If we got an exception after the user typed a description for their
2127 # change, back up the description before re-raising.
2128 if change_desc:
2129 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2130 print '\nGot exception while uploading -- saving description to %s\n' \
2131 % backup_path
2132 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002133 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002134 backup_file.close()
2135 raise
2136
2137 if not cl.GetIssue():
2138 cl.SetIssue(issue)
2139 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002140
2141 if options.use_commit_queue:
2142 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002143 return 0
2144
2145
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002146def cleanup_list(l):
2147 """Fixes a list so that comma separated items are put as individual items.
2148
2149 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2150 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2151 """
2152 items = sum((i.split(',') for i in l), [])
2153 stripped_items = (i.strip() for i in items)
2154 return sorted(filter(None, stripped_items))
2155
2156
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002157@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002158def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002159 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00002160 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2161 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002162 parser.add_option('--bypass-watchlists', action='store_true',
2163 dest='bypass_watchlists',
2164 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002165 parser.add_option('-f', action='store_true', dest='force',
2166 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002167 parser.add_option('-m', dest='message', help='message for patchset')
2168 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002169 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002170 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002171 help='reviewer email addresses')
2172 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002173 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002174 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002175 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002176 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002177 parser.add_option('--emulate_svn_auto_props',
2178 '--emulate-svn-auto-props',
2179 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002180 dest="emulate_svn_auto_props",
2181 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002182 parser.add_option('-c', '--use-commit-queue', action='store_true',
2183 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002184 parser.add_option('--private', action='store_true',
2185 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002186 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002187 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002188 metavar='TARGET',
2189 help='Apply CL to remote ref TARGET. ' +
2190 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002191 parser.add_option('--squash', action='store_true',
2192 help='Squash multiple commits into one (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002193 parser.add_option('--email', default=None,
2194 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002195 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2196 help='add a set of OWNERS to TBR')
rmistry@google.comef966222015-04-07 11:15:01 +00002197 parser.add_option('--cq-dry-run', dest='cq_dry_run', action='store_true',
2198 help='Send the patchset to do a CQ dry run right after '
2199 'upload.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002200
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002201 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002202 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002203 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002204 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002205
sbc@chromium.org71437c02015-04-09 19:29:40 +00002206 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002207 return 1
2208
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002209 options.reviewers = cleanup_list(options.reviewers)
2210 options.cc = cleanup_list(options.cc)
2211
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002212 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002213 if args:
2214 # TODO(ukai): is it ok for gerrit case?
2215 base_branch = args[0]
2216 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002217 if cl.GetBranch() is None:
2218 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2219
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002220 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002221 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002222 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002223
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002224 # Make sure authenticated to Rietveld before running expensive hooks. It is
2225 # a fast, best efforts check. Rietveld still can reject the authentication
2226 # during the actual upload.
2227 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2228 authenticator = auth.get_authenticator_for_host(
2229 cl.GetRietveldServer(), auth_config)
2230 if not authenticator.has_cached_credentials():
2231 raise auth.LoginRequiredError(cl.GetRietveldServer())
2232
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002233 # Apply watchlists on upload.
2234 change = cl.GetChange(base_branch, None)
2235 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2236 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002237 if not options.bypass_watchlists:
2238 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002239
ukai@chromium.orge8077812012-02-03 03:41:46 +00002240 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002241 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002242 # Set the reviewer list now so that presubmit checks can access it.
2243 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002244 change_description.update_reviewers(options.reviewers,
2245 options.tbr_owners,
2246 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002247 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002248 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002249 may_prompt=not options.force,
2250 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002251 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002252 if not hook_results.should_continue():
2253 return 1
2254 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002255 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002256
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002257 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002258 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002259 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002260 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002261 print ('The last upload made from this repository was patchset #%d but '
2262 'the most recent patchset on the server is #%d.'
2263 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002264 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2265 'from another machine or branch the patch you\'re uploading now '
2266 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002267 ask_for_data('About to upload; enter to confirm.')
2268
iannucci@chromium.org79540052012-10-19 23:15:26 +00002269 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002270 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00002271 return GerritUpload(options, args, cl, change)
2272 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002273 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002274 git_set_branch_value('last-upload-hash',
2275 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002276 # Run post upload hooks, if specified.
2277 if settings.GetRunPostUploadHook():
2278 presubmit_support.DoPostUploadExecuter(
2279 change,
2280 cl,
2281 settings.GetRoot(),
2282 options.verbose,
2283 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002284
2285 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002286
2287
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002288def IsSubmoduleMergeCommit(ref):
2289 # When submodules are added to the repo, we expect there to be a single
2290 # non-git-svn merge commit at remote HEAD with a signature comment.
2291 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002292 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002293 return RunGit(cmd) != ''
2294
2295
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002296def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002297 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002298
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002299 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002300 Updates changelog with metadata (e.g. pointer to review).
2301 Pushes/dcommits the code upstream.
2302 Updates review and closes.
2303 """
2304 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2305 help='bypass upload presubmit hook')
2306 parser.add_option('-m', dest='message',
2307 help="override review description")
2308 parser.add_option('-f', action='store_true', dest='force',
2309 help="force yes to questions (don't prompt)")
2310 parser.add_option('-c', dest='contributor',
2311 help="external contributor for patch (appended to " +
2312 "description and used as author for git). Should be " +
2313 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002314 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002315 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002316 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002317 auth_config = auth.extract_auth_config_from_options(options)
2318
2319 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002320
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002321 current = cl.GetBranch()
2322 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2323 if not settings.GetIsGitSvn() and remote == '.':
2324 print
2325 print 'Attempting to push branch %r into another local branch!' % current
2326 print
2327 print 'Either reparent this branch on top of origin/master:'
2328 print ' git reparent-branch --root'
2329 print
2330 print 'OR run `git rebase-update` if you think the parent branch is already'
2331 print 'committed.'
2332 print
2333 print ' Current parent: %r' % upstream_branch
2334 return 1
2335
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002336 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002337 # Default to merging against our best guess of the upstream branch.
2338 args = [cl.GetUpstreamBranch()]
2339
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002340 if options.contributor:
2341 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2342 print "Please provide contibutor as 'First Last <email@example.com>'"
2343 return 1
2344
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002345 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002346 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002347
sbc@chromium.org71437c02015-04-09 19:29:40 +00002348 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002349 return 1
2350
2351 # This rev-list syntax means "show all commits not in my branch that
2352 # are in base_branch".
2353 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2354 base_branch]).splitlines()
2355 if upstream_commits:
2356 print ('Base branch "%s" has %d commits '
2357 'not in this branch.' % (base_branch, len(upstream_commits)))
2358 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2359 return 1
2360
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002361 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002362 svn_head = None
2363 if cmd == 'dcommit' or base_has_submodules:
2364 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2365 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002366
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002367 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002368 # If the base_head is a submodule merge commit, the first parent of the
2369 # base_head should be a git-svn commit, which is what we're interested in.
2370 base_svn_head = base_branch
2371 if base_has_submodules:
2372 base_svn_head += '^1'
2373
2374 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002375 if extra_commits:
2376 print ('This branch has %d additional commits not upstreamed yet.'
2377 % len(extra_commits.splitlines()))
2378 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2379 'before attempting to %s.' % (base_branch, cmd))
2380 return 1
2381
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002382 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002383 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002384 author = None
2385 if options.contributor:
2386 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002387 hook_results = cl.RunHook(
2388 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002389 may_prompt=not options.force,
2390 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002391 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002392 if not hook_results.should_continue():
2393 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002394
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002395 # Check the tree status if the tree status URL is set.
2396 status = GetTreeStatus()
2397 if 'closed' == status:
2398 print('The tree is closed. Please wait for it to reopen. Use '
2399 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2400 return 1
2401 elif 'unknown' == status:
2402 print('Unable to determine tree status. Please verify manually and '
2403 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2404 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002405 else:
2406 breakpad.SendStack(
2407 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002408 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2409 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002410 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002411
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002412 change_desc = ChangeDescription(options.message)
2413 if not change_desc.description and cl.GetIssue():
2414 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002415
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002416 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002417 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002418 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002419 else:
2420 print 'No description set.'
2421 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2422 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002423
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002424 # Keep a separate copy for the commit message, because the commit message
2425 # contains the link to the Rietveld issue, while the Rietveld message contains
2426 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002427 # Keep a separate copy for the commit message.
2428 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002429 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002430
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002431 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002432 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002433 # Xcode won't linkify this URL unless there is a non-whitespace character
2434 # after it. Add a period on a new line to circumvent this.
2435 commit_desc.append_footer('Review URL: %s.' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002436 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002437 commit_desc.append_footer('Patch from %s.' % options.contributor)
2438
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002439 print('Description:')
2440 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002441
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002442 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002443 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002444 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002445
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002446 # We want to squash all this branch's commits into one commit with the proper
2447 # description. We do this by doing a "reset --soft" to the base branch (which
2448 # keeps the working copy the same), then dcommitting that. If origin/master
2449 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2450 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002451 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002452 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2453 # Delete the branches if they exist.
2454 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2455 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2456 result = RunGitWithCode(showref_cmd)
2457 if result[0] == 0:
2458 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002459
2460 # We might be in a directory that's present in this branch but not in the
2461 # trunk. Move up to the top of the tree so that git commands that expect a
2462 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002463 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002464 if rel_base_path:
2465 os.chdir(rel_base_path)
2466
2467 # Stuff our change into the merge branch.
2468 # We wrap in a try...finally block so if anything goes wrong,
2469 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002470 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002471 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002472 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002473 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002474 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002475 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002476 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002477 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002478 RunGit(
2479 [
2480 'commit', '--author', options.contributor,
2481 '-m', commit_desc.description,
2482 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002483 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002484 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002485 if base_has_submodules:
2486 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2487 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2488 RunGit(['checkout', CHERRY_PICK_BRANCH])
2489 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002490 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002491 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002492 pending_prefix = settings.GetPendingRefPrefix()
2493 if not pending_prefix or branch.startswith(pending_prefix):
2494 # If not using refs/pending/heads/* at all, or target ref is already set
2495 # to pending, then push to the target ref directly.
2496 retcode, output = RunGitWithCode(
2497 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002498 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002499 else:
2500 # Cherry-pick the change on top of pending ref and then push it.
2501 assert branch.startswith('refs/'), branch
2502 assert pending_prefix[-1] == '/', pending_prefix
2503 pending_ref = pending_prefix + branch[len('refs/'):]
2504 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002505 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002506 if retcode == 0:
2507 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002508 else:
2509 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002510 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002511 'svn', 'dcommit',
2512 '-C%s' % options.similarity,
2513 '--no-rebase', '--rmdir',
2514 ]
2515 if settings.GetForceHttpsCommitUrl():
2516 # Allow forcing https commit URLs for some projects that don't allow
2517 # committing to http URLs (like Google Code).
2518 remote_url = cl.GetGitSvnRemoteUrl()
2519 if urlparse.urlparse(remote_url).scheme == 'http':
2520 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002521 cmd_args.append('--commit-url=%s' % remote_url)
2522 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002523 if 'Committed r' in output:
2524 revision = re.match(
2525 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2526 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002527 finally:
2528 # And then swap back to the original branch and clean up.
2529 RunGit(['checkout', '-q', cl.GetBranch()])
2530 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002531 if base_has_submodules:
2532 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002533
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002534 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002535 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002536 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002537
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002538 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002539 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002540 try:
2541 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2542 # We set pushed_to_pending to False, since it made it all the way to the
2543 # real ref.
2544 pushed_to_pending = False
2545 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002546 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002547
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002548 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002549 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002550 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002551 if not to_pending:
2552 if viewvc_url and revision:
2553 change_desc.append_footer(
2554 'Committed: %s%s' % (viewvc_url, revision))
2555 elif revision:
2556 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002557 print ('Closing issue '
2558 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002559 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002560 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002561 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002562 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002563 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002564 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002565 if options.bypass_hooks:
2566 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2567 else:
2568 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002569 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002570 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002571
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002572 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002573 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2574 print 'The commit is in the pending queue (%s).' % pending_ref
2575 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002576 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002577 'footer.' % branch)
2578
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002579 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2580 if os.path.isfile(hook):
2581 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002582
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002583 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002584
2585
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002586def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2587 print
2588 print 'Waiting for commit to be landed on %s...' % real_ref
2589 print '(If you are impatient, you may Ctrl-C once without harm)'
2590 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2591 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2592
2593 loop = 0
2594 while True:
2595 sys.stdout.write('fetching (%d)... \r' % loop)
2596 sys.stdout.flush()
2597 loop += 1
2598
2599 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2600 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2601 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2602 for commit in commits.splitlines():
2603 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2604 print 'Found commit on %s' % real_ref
2605 return commit
2606
2607 current_rev = to_rev
2608
2609
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002610def PushToGitPending(remote, pending_ref, upstream_ref):
2611 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2612
2613 Returns:
2614 (retcode of last operation, output log of last operation).
2615 """
2616 assert pending_ref.startswith('refs/'), pending_ref
2617 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2618 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2619 code = 0
2620 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002621 max_attempts = 3
2622 attempts_left = max_attempts
2623 while attempts_left:
2624 if attempts_left != max_attempts:
2625 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2626 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002627
2628 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002629 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002630 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002631 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002632 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002633 print 'Fetch failed with exit code %d.' % code
2634 if out.strip():
2635 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002636 continue
2637
2638 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002639 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002640 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002641 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002642 if code:
2643 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002644 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2645 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002646 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2647 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002648 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002649 return code, out
2650
2651 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002652 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002653 code, out = RunGitWithCode(
2654 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2655 if code == 0:
2656 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002657 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002658 return code, out
2659
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002660 print 'Push failed with exit code %d.' % code
2661 if out.strip():
2662 print out.strip()
2663 if IsFatalPushFailure(out):
2664 print (
2665 'Fatal push error. Make sure your .netrc credentials and git '
2666 'user.email are correct and you have push access to the repo.')
2667 return code, out
2668
2669 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002670 return code, out
2671
2672
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002673def IsFatalPushFailure(push_stdout):
2674 """True if retrying push won't help."""
2675 return '(prohibited by Gerrit)' in push_stdout
2676
2677
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002678@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002679def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002680 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002681 if not settings.GetIsGitSvn():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002682 if get_footer_svn_id():
2683 # If it looks like previous commits were mirrored with git-svn.
2684 message = """This repository appears to be a git-svn mirror, but no
2685upstream SVN master is set. You probably need to run 'git auto-svn' once."""
2686 else:
2687 message = """This doesn't appear to be an SVN repository.
2688If your project has a true, writeable git repository, you probably want to run
2689'git cl land' instead.
2690If your project has a git mirror of an upstream SVN master, you probably need
2691to run 'git svn init'.
2692
2693Using the wrong command might cause your commit to appear to succeed, and the
2694review to be closed, without actually landing upstream. If you choose to
2695proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002696 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002697 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002698 return SendUpstream(parser, args, 'dcommit')
2699
2700
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002701@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002702def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002703 """Commits the current changelist via git."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002704 if settings.GetIsGitSvn() or get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002705 print('This appears to be an SVN repository.')
2706 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002707 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00002708 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002709 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002710
2711
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002712@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002713def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002714 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002715 parser.add_option('-b', dest='newbranch',
2716 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002717 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002718 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002719 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2720 help='Change to the directory DIR immediately, '
2721 'before doing anything else.')
2722 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002723 help='failed patches spew .rej files rather than '
2724 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002725 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2726 help="don't commit after patch applies")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002727 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002728 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002729 auth_config = auth.extract_auth_config_from_options(options)
2730
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002731 if len(args) != 1:
2732 parser.print_help()
2733 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002734 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002735
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002736 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002737 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002738 return 1
2739
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002740 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002741 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002742
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002743 if options.newbranch:
2744 if options.force:
2745 RunGit(['branch', '-D', options.newbranch],
2746 stderr=subprocess2.PIPE, error_ok=True)
2747 RunGit(['checkout', '-b', options.newbranch,
2748 Changelist().GetUpstreamBranch()])
2749
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002750 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002751 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002752
2753
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002754def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00002755 # PatchIssue should never be called with a dirty tree. It is up to the
2756 # caller to check this, but just in case we assert here since the
2757 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002758 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002759
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002760 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002761 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002762 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002763 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002764 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002765 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002766 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002767 # Assume it's a URL to the patch. Default to https.
2768 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00002769 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002770 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002771 DieWithError('Must pass an issue ID or full URL for '
2772 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00002773 issue = int(match.group(2))
2774 cl = Changelist(issue=issue, auth_config=auth_config)
2775 cl.rietveld_server = match.group(1)
2776 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002777 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002778
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002779 # Switch up to the top-level directory, if necessary, in preparation for
2780 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002781 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002782 if top:
2783 os.chdir(top)
2784
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002785 # Git patches have a/ at the beginning of source paths. We strip that out
2786 # with a sed script rather than the -p flag to patch so we can feed either
2787 # Git or svn-style patches into the same apply command.
2788 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002789 try:
2790 patch_data = subprocess2.check_output(
2791 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2792 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002793 DieWithError('Git patch mungling failed.')
2794 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002795
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002796 # We use "git apply" to apply the patch instead of "patch" so that we can
2797 # pick up file adds.
2798 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002799 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002800 if directory:
2801 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002802 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002803 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002804 elif IsGitVersionAtLeast('1.7.12'):
2805 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002806 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002807 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002808 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002809 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00002810 print 'Failed to apply the patch'
2811 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002812
2813 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002814 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00002815 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
2816 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00002817 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2818 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002819 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002820 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002821 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002822 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002823 else:
2824 print "Patch applied to index."
2825 return 0
2826
2827
2828def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002829 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002830 # Provide a wrapper for git svn rebase to help avoid accidental
2831 # git svn dcommit.
2832 # It's the only command that doesn't use parser at all since we just defer
2833 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002834
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002835 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002836
2837
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002838def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002839 """Fetches the tree status and returns either 'open', 'closed',
2840 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002841 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002842 if url:
2843 status = urllib2.urlopen(url).read().lower()
2844 if status.find('closed') != -1 or status == '0':
2845 return 'closed'
2846 elif status.find('open') != -1 or status == '1':
2847 return 'open'
2848 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002849 return 'unset'
2850
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002851
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002852def GetTreeStatusReason():
2853 """Fetches the tree status from a json url and returns the message
2854 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002855 url = settings.GetTreeStatusUrl()
2856 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002857 connection = urllib2.urlopen(json_url)
2858 status = json.loads(connection.read())
2859 connection.close()
2860 return status['message']
2861
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002862
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002863def GetBuilderMaster(bot_list):
2864 """For a given builder, fetch the master from AE if available."""
2865 map_url = 'https://builders-map.appspot.com/'
2866 try:
2867 master_map = json.load(urllib2.urlopen(map_url))
2868 except urllib2.URLError as e:
2869 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2870 (map_url, e))
2871 except ValueError as e:
2872 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2873 if not master_map:
2874 return None, 'Failed to build master map.'
2875
2876 result_master = ''
2877 for bot in bot_list:
2878 builder = bot.split(':', 1)[0]
2879 master_list = master_map.get(builder, [])
2880 if not master_list:
2881 return None, ('No matching master for builder %s.' % builder)
2882 elif len(master_list) > 1:
2883 return None, ('The builder name %s exists in multiple masters %s.' %
2884 (builder, master_list))
2885 else:
2886 cur_master = master_list[0]
2887 if not result_master:
2888 result_master = cur_master
2889 elif result_master != cur_master:
2890 return None, 'The builders do not belong to the same master.'
2891 return result_master, None
2892
2893
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002894def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002895 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002896 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002897 status = GetTreeStatus()
2898 if 'unset' == status:
2899 print 'You must configure your tree status URL by running "git cl config".'
2900 return 2
2901
2902 print "The tree is %s" % status
2903 print
2904 print GetTreeStatusReason()
2905 if status != 'open':
2906 return 1
2907 return 0
2908
2909
maruel@chromium.org15192402012-09-06 12:38:29 +00002910def CMDtry(parser, args):
2911 """Triggers a try job through Rietveld."""
2912 group = optparse.OptionGroup(parser, "Try job options")
2913 group.add_option(
2914 "-b", "--bot", action="append",
2915 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2916 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002917 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002918 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002919 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00002920 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002921 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002922 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002923 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002924 "-r", "--revision",
2925 help="Revision to use for the try job; default: the "
2926 "revision will be determined by the try server; see "
2927 "its waterfall for more info")
2928 group.add_option(
2929 "-c", "--clobber", action="store_true", default=False,
2930 help="Force a clobber before building; e.g. don't do an "
2931 "incremental build")
2932 group.add_option(
2933 "--project",
2934 help="Override which project to use. Projects are defined "
2935 "server-side to define what default bot set to use")
2936 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002937 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00002938 group.add_option(
2939 "--use-buildbucket", action="store_true", default=False,
2940 help="Use buildbucket to trigger try jobs.")
maruel@chromium.org15192402012-09-06 12:38:29 +00002941 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002942 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00002943 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002944 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00002945
2946 if args:
2947 parser.error('Unknown arguments: %s' % args)
2948
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002949 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00002950 if not cl.GetIssue():
2951 parser.error('Need to upload first')
2952
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002953 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002954 if props.get('closed'):
2955 parser.error('Cannot send tryjobs for a closed CL')
2956
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002957 if props.get('private'):
2958 parser.error('Cannot use trybots with private issue')
2959
maruel@chromium.org15192402012-09-06 12:38:29 +00002960 if not options.name:
2961 options.name = cl.GetBranch()
2962
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002963 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002964 options.master, err_msg = GetBuilderMaster(options.bot)
2965 if err_msg:
2966 parser.error('Tryserver master cannot be found because: %s\n'
2967 'Please manually specify the tryserver master'
2968 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002969
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002970 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002971 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002972 if not options.bot:
2973 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002974
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002975 # Get try masters from PRESUBMIT.py files.
2976 masters = presubmit_support.DoGetTryMasters(
2977 change,
2978 change.LocalPaths(),
2979 settings.GetRoot(),
2980 None,
2981 None,
2982 options.verbose,
2983 sys.stdout)
2984 if masters:
2985 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002986
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002987 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2988 options.bot = presubmit_support.DoGetTrySlaves(
2989 change,
2990 change.LocalPaths(),
2991 settings.GetRoot(),
2992 None,
2993 None,
2994 options.verbose,
2995 sys.stdout)
2996 if not options.bot:
2997 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002998
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002999 builders_and_tests = {}
3000 # TODO(machenbach): The old style command-line options don't support
3001 # multiple try masters yet.
3002 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
3003 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
3004
3005 for bot in old_style:
3006 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00003007 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003008 elif ',' in bot:
3009 parser.error('Specify one bot per --bot flag')
3010 else:
3011 builders_and_tests.setdefault(bot, []).append('defaulttests')
3012
3013 for bot, tests in new_style:
3014 builders_and_tests.setdefault(bot, []).extend(tests)
3015
3016 # Return a master map with one master to be backwards compatible. The
3017 # master name defaults to an empty string, which will cause the master
3018 # not to be set on rietveld (deprecated).
3019 return {options.master: builders_and_tests}
3020
3021 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003022
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003023 for builders in masters.itervalues():
3024 if any('triggered' in b for b in builders):
3025 print >> sys.stderr, (
3026 'ERROR You are trying to send a job to a triggered bot. This type of'
3027 ' bot requires an\ninitial job from a parent (usually a builder). '
3028 'Instead send your job to the parent.\n'
3029 'Bot list: %s' % builders)
3030 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003031
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003032 patchset = cl.GetMostRecentPatchset()
3033 if patchset and patchset != cl.GetPatchset():
3034 print(
3035 '\nWARNING Mismatch between local config and server. Did a previous '
3036 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3037 'Continuing using\npatchset %s.\n' % patchset)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003038 if options.use_buildbucket:
3039 try:
3040 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3041 except BuildbucketResponseException as ex:
3042 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003043 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003044 except Exception as e:
3045 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3046 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3047 e, stacktrace)
3048 return 1
3049 else:
3050 try:
3051 cl.RpcServer().trigger_distributed_try_jobs(
3052 cl.GetIssue(), patchset, options.name, options.clobber,
3053 options.revision, masters)
3054 except urllib2.HTTPError as e:
3055 if e.code == 404:
3056 print('404 from rietveld; '
3057 'did you mean to use "git try" instead of "git cl try"?')
3058 return 1
3059 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003060
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003061 for (master, builders) in sorted(masters.iteritems()):
3062 if master:
3063 print 'Master: %s' % master
3064 length = max(len(builder) for builder in builders)
3065 for builder in sorted(builders):
3066 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003067 return 0
3068
3069
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003070@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003071def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003072 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003073 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003074 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003075 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003076
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003077 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003078 if args:
3079 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003080 branch = cl.GetBranch()
3081 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003082 cl = Changelist()
3083 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003084
3085 # Clear configured merge-base, if there is one.
3086 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003087 else:
3088 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003089 return 0
3090
3091
thestig@chromium.org00858c82013-12-02 23:08:03 +00003092def CMDweb(parser, args):
3093 """Opens the current CL in the web browser."""
3094 _, args = parser.parse_args(args)
3095 if args:
3096 parser.error('Unrecognized args: %s' % ' '.join(args))
3097
3098 issue_url = Changelist().GetIssueURL()
3099 if not issue_url:
3100 print >> sys.stderr, 'ERROR No issue to open'
3101 return 1
3102
3103 webbrowser.open(issue_url)
3104 return 0
3105
3106
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003107def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003108 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003109 auth.add_auth_options(parser)
3110 options, args = parser.parse_args(args)
3111 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003112 if args:
3113 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003114 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003115 props = cl.GetIssueProperties()
3116 if props.get('private'):
3117 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003118 cl.SetFlag('commit', '1')
3119 return 0
3120
3121
groby@chromium.org411034a2013-02-26 15:12:01 +00003122def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003123 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003124 auth.add_auth_options(parser)
3125 options, args = parser.parse_args(args)
3126 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003127 if args:
3128 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003129 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003130 # Ensure there actually is an issue to close.
3131 cl.GetDescription()
3132 cl.CloseIssue()
3133 return 0
3134
3135
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003136def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003137 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003138 auth.add_auth_options(parser)
3139 options, args = parser.parse_args(args)
3140 auth_config = auth.extract_auth_config_from_options(options)
3141 if args:
3142 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003143
3144 # Uncommitted (staged and unstaged) changes will be destroyed by
3145 # "git reset --hard" if there are merging conflicts in PatchIssue().
3146 # Staged changes would be committed along with the patch from last
3147 # upload, hence counted toward the "last upload" side in the final
3148 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003149 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003150 return 1
3151
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003152 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003153 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003154 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003155 if not issue:
3156 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003157 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003158 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003159
3160 # Create a new branch based on the merge-base
3161 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3162 try:
3163 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003164 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003165 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003166 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003167 return rtn
3168
wychen@chromium.org06928532015-02-03 02:11:29 +00003169 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003170 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003171 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003172 finally:
3173 RunGit(['checkout', '-q', branch])
3174 RunGit(['branch', '-D', TMP_BRANCH])
3175
3176 return 0
3177
3178
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003179def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003180 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003181 parser.add_option(
3182 '--no-color',
3183 action='store_true',
3184 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003185 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003186 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003187 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003188
3189 author = RunGit(['config', 'user.email']).strip() or None
3190
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003191 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003192
3193 if args:
3194 if len(args) > 1:
3195 parser.error('Unknown args')
3196 base_branch = args[0]
3197 else:
3198 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003199 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003200
3201 change = cl.GetChange(base_branch, None)
3202 return owners_finder.OwnersFinder(
3203 [f.LocalPath() for f in
3204 cl.GetChange(base_branch, None).AffectedFiles()],
3205 change.RepositoryRoot(), author,
3206 fopen=file, os_path=os.path, glob=glob.glob,
3207 disable_color=options.no_color).run()
3208
3209
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003210def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
3211 """Generates a diff command."""
3212 # Generate diff for the current branch's changes.
3213 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3214 upstream_commit, '--' ]
3215
3216 if args:
3217 for arg in args:
3218 if os.path.isdir(arg):
3219 diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
3220 elif os.path.isfile(arg):
3221 diff_cmd.append(arg)
3222 else:
3223 DieWithError('Argument "%s" is not a file or a directory' % arg)
3224 else:
3225 diff_cmd.extend('*' + ext for ext in extensions)
3226
3227 return diff_cmd
3228
3229
enne@chromium.org555cfe42014-01-29 18:21:39 +00003230@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003231def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003232 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003233 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003234 parser.add_option('--full', action='store_true',
3235 help='Reformat the full content of all touched files')
3236 parser.add_option('--dry-run', action='store_true',
3237 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003238 parser.add_option('--python', action='store_true',
3239 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003240 parser.add_option('--diff', action='store_true',
3241 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003242 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003243
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003244 # git diff generates paths against the root of the repository. Change
3245 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003246 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003247 if rel_base_path:
3248 os.chdir(rel_base_path)
3249
digit@chromium.org29e47272013-05-17 17:01:46 +00003250 # Grab the merge-base commit, i.e. the upstream commit of the current
3251 # branch when it was created or the last time it was rebased. This is
3252 # to cover the case where the user may have called "git fetch origin",
3253 # moving the origin branch to a newer commit, but hasn't rebased yet.
3254 upstream_commit = None
3255 cl = Changelist()
3256 upstream_branch = cl.GetUpstreamBranch()
3257 if upstream_branch:
3258 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3259 upstream_commit = upstream_commit.strip()
3260
3261 if not upstream_commit:
3262 DieWithError('Could not find base commit for this branch. '
3263 'Are you in detached state?')
3264
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003265 if opts.full:
3266 # Only list the names of modified files.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003267 diff_type = '--name-only'
enne@chromium.org555cfe42014-01-29 18:21:39 +00003268 else:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003269 # Only generate context-less patches.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003270 diff_type = '-U0'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003271
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003272 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, CLANG_EXTS)
digit@chromium.org29e47272013-05-17 17:01:46 +00003273 diff_output = RunGit(diff_cmd)
3274
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003275 top_dir = os.path.normpath(
3276 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3277
3278 # Locate the clang-format binary in the checkout
3279 try:
3280 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3281 except clang_format.NotFoundError, e:
3282 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003283
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003284 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3285 # formatted. This is used to block during the presubmit.
3286 return_value = 0
3287
digit@chromium.org29e47272013-05-17 17:01:46 +00003288 if opts.full:
3289 # diff_output is a list of files to send to clang-format.
3290 files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003291 if files:
3292 cmd = [clang_format_tool]
3293 if not opts.dry_run and not opts.diff:
3294 cmd.append('-i')
3295 stdout = RunCommand(cmd + files, cwd=top_dir)
3296 if opts.diff:
3297 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003298 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003299 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003300 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00003301 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003302 try:
3303 script = clang_format.FindClangFormatScriptInChromiumTree(
3304 'clang-format-diff.py')
3305 except clang_format.NotFoundError, e:
3306 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003307
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003308 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003309 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003310 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003311
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003312 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003313 if opts.diff:
3314 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003315 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003316 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003317
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003318 # Similar code to above, but using yapf on .py files rather than clang-format
3319 # on C/C++ files
3320 if opts.python:
3321 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, ['.py'])
3322 diff_output = RunGit(diff_cmd)
3323 yapf_tool = gclient_utils.FindExecutable('yapf')
3324 if yapf_tool is None:
3325 DieWithError('yapf not found in PATH')
3326
3327 if opts.full:
3328 files = diff_output.splitlines()
3329 if files:
3330 cmd = [yapf_tool]
3331 if not opts.dry_run and not opts.diff:
3332 cmd.append('-i')
3333 stdout = RunCommand(cmd + files, cwd=top_dir)
3334 if opts.diff:
3335 sys.stdout.write(stdout)
3336 else:
3337 # TODO(sbc): yapf --lines mode still has some issues.
3338 # https://github.com/google/yapf/issues/154
3339 DieWithError('--python currently only works with --full')
3340
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003341 # Build a diff command that only operates on dart files. dart's formatter
3342 # does not have the nice property of only operating on modified chunks, so
3343 # hard code full.
3344 dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3345 args, ['.dart'])
3346 dart_diff_output = RunGit(dart_diff_cmd)
3347 if dart_diff_output:
3348 try:
3349 command = [dart_format.FindDartFmtToolInChromiumTree()]
3350 if not opts.dry_run and not opts.diff:
3351 command.append('-w')
3352 command.extend(dart_diff_output.splitlines())
3353
3354 stdout = RunCommand(command, cwd=top_dir, env=env)
3355 if opts.dry_run and stdout:
3356 return_value = 2
3357 except dart_format.NotFoundError as e:
3358 print ('Unable to check dart code formatting. Dart SDK is not in ' +
3359 'this checkout.')
3360
3361 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003362
3363
maruel@chromium.org29404b52014-09-08 22:58:00 +00003364def CMDlol(parser, args):
3365 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003366 print zlib.decompress(base64.b64decode(
3367 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3368 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3369 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3370 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003371 return 0
3372
3373
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003374class OptionParser(optparse.OptionParser):
3375 """Creates the option parse and add --verbose support."""
3376 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003377 optparse.OptionParser.__init__(
3378 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003379 self.add_option(
3380 '-v', '--verbose', action='count', default=0,
3381 help='Use 2 times for more debugging info')
3382
3383 def parse_args(self, args=None, values=None):
3384 options, args = optparse.OptionParser.parse_args(self, args, values)
3385 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3386 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3387 return options, args
3388
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003389
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003390def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003391 if sys.hexversion < 0x02060000:
3392 print >> sys.stderr, (
3393 '\nYour python version %s is unsupported, please upgrade.\n' %
3394 sys.version.split(' ', 1)[0])
3395 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003396
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003397 # Reload settings.
3398 global settings
3399 settings = Settings()
3400
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003401 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003402 dispatcher = subcommand.CommandDispatcher(__name__)
3403 try:
3404 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003405 except auth.AuthenticationError as e:
3406 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003407 except urllib2.HTTPError, e:
3408 if e.code != 500:
3409 raise
3410 DieWithError(
3411 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3412 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003413 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003414
3415
3416if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003417 # These affect sys.stdout so do it outside of main() to simplify mocks in
3418 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003419 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003420 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003421 try:
3422 sys.exit(main(sys.argv[1:]))
3423 except KeyboardInterrupt:
3424 sys.stderr.write('interrupted\n')
3425 sys.exit(1)