blob: 60fd1ce71156b0c2415eb998aa33aef0b13ff2db [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
sheyang@chromium.org152cf832014-06-11 21:37:49 +00002088 project = settings.GetProject()
2089 if project:
2090 upload_args.extend(['--project', project])
2091
rmistry@google.comef966222015-04-07 11:15:01 +00002092 if options.cq_dry_run:
2093 upload_args.extend(['--cq_dry_run'])
2094
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002095 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00002096 upload_args = ['upload'] + upload_args + args
2097 logging.info('upload.RealMain(%s)', upload_args)
2098 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00002099 issue = int(issue)
2100 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00002101 except KeyboardInterrupt:
2102 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002103 except:
2104 # If we got an exception after the user typed a description for their
2105 # change, back up the description before re-raising.
2106 if change_desc:
2107 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2108 print '\nGot exception while uploading -- saving description to %s\n' \
2109 % backup_path
2110 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002111 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002112 backup_file.close()
2113 raise
2114
2115 if not cl.GetIssue():
2116 cl.SetIssue(issue)
2117 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002118
2119 if options.use_commit_queue:
2120 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002121 return 0
2122
2123
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002124def cleanup_list(l):
2125 """Fixes a list so that comma separated items are put as individual items.
2126
2127 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
2128 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
2129 """
2130 items = sum((i.split(',') for i in l), [])
2131 stripped_items = (i.strip() for i in items)
2132 return sorted(filter(None, stripped_items))
2133
2134
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002135@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002136def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002137 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00002138 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2139 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002140 parser.add_option('--bypass-watchlists', action='store_true',
2141 dest='bypass_watchlists',
2142 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002143 parser.add_option('-f', action='store_true', dest='force',
2144 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00002145 parser.add_option('-m', dest='message', help='message for patchset')
2146 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002147 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002148 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002149 help='reviewer email addresses')
2150 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002151 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00002152 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00002153 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00002154 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002155 parser.add_option('--emulate_svn_auto_props',
2156 '--emulate-svn-auto-props',
2157 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00002158 dest="emulate_svn_auto_props",
2159 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00002160 parser.add_option('-c', '--use-commit-queue', action='store_true',
2161 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00002162 parser.add_option('--private', action='store_true',
2163 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00002164 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00002165 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00002166 metavar='TARGET',
2167 help='Apply CL to remote ref TARGET. ' +
2168 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00002169 parser.add_option('--squash', action='store_true',
2170 help='Squash multiple commits into one (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002171 parser.add_option('--email', default=None,
2172 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00002173 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
2174 help='add a set of OWNERS to TBR')
rmistry@google.comef966222015-04-07 11:15:01 +00002175 parser.add_option('--cq-dry-run', dest='cq_dry_run', action='store_true',
2176 help='Send the patchset to do a CQ dry run right after '
2177 'upload.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00002178
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002179 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002180 auth.add_auth_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002181 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002182 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002183
sbc@chromium.org71437c02015-04-09 19:29:40 +00002184 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00002185 return 1
2186
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002187 options.reviewers = cleanup_list(options.reviewers)
2188 options.cc = cleanup_list(options.cc)
2189
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002190 cl = Changelist(auth_config=auth_config)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002191 if args:
2192 # TODO(ukai): is it ok for gerrit case?
2193 base_branch = args[0]
2194 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00002195 if cl.GetBranch() is None:
2196 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
2197
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002198 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002199 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00002200 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00002201
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00002202 # Make sure authenticated to Rietveld before running expensive hooks. It is
2203 # a fast, best efforts check. Rietveld still can reject the authentication
2204 # during the actual upload.
2205 if not settings.GetIsGerrit() and auth_config.use_oauth2:
2206 authenticator = auth.get_authenticator_for_host(
2207 cl.GetRietveldServer(), auth_config)
2208 if not authenticator.has_cached_credentials():
2209 raise auth.LoginRequiredError(cl.GetRietveldServer())
2210
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002211 # Apply watchlists on upload.
2212 change = cl.GetChange(base_branch, None)
2213 watchlist = watchlists.Watchlists(change.RepositoryRoot())
2214 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00002215 if not options.bypass_watchlists:
2216 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002217
ukai@chromium.orge8077812012-02-03 03:41:46 +00002218 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002219 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002220 # Set the reviewer list now so that presubmit checks can access it.
2221 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002222 change_description.update_reviewers(options.reviewers,
2223 options.tbr_owners,
2224 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002225 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002226 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002227 may_prompt=not options.force,
2228 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002229 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002230 if not hook_results.should_continue():
2231 return 1
2232 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002233 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002234
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002235 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002236 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002237 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002238 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002239 print ('The last upload made from this repository was patchset #%d but '
2240 'the most recent patchset on the server is #%d.'
2241 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002242 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2243 'from another machine or branch the patch you\'re uploading now '
2244 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002245 ask_for_data('About to upload; enter to confirm.')
2246
iannucci@chromium.org79540052012-10-19 23:15:26 +00002247 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002248 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00002249 return GerritUpload(options, args, cl, change)
2250 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002251 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002252 git_set_branch_value('last-upload-hash',
2253 RunGit(['rev-parse', 'HEAD']).strip())
rmistry@google.com5626a922015-02-26 14:03:30 +00002254 # Run post upload hooks, if specified.
2255 if settings.GetRunPostUploadHook():
2256 presubmit_support.DoPostUploadExecuter(
2257 change,
2258 cl,
2259 settings.GetRoot(),
2260 options.verbose,
2261 sys.stdout)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002262
2263 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002264
2265
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002266def IsSubmoduleMergeCommit(ref):
2267 # When submodules are added to the repo, we expect there to be a single
2268 # non-git-svn merge commit at remote HEAD with a signature comment.
2269 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002270 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002271 return RunGit(cmd) != ''
2272
2273
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002274def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002275 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002276
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002277 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002278 Updates changelog with metadata (e.g. pointer to review).
2279 Pushes/dcommits the code upstream.
2280 Updates review and closes.
2281 """
2282 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2283 help='bypass upload presubmit hook')
2284 parser.add_option('-m', dest='message',
2285 help="override review description")
2286 parser.add_option('-f', action='store_true', dest='force',
2287 help="force yes to questions (don't prompt)")
2288 parser.add_option('-c', dest='contributor',
2289 help="external contributor for patch (appended to " +
2290 "description and used as author for git). Should be " +
2291 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002292 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002293 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002294 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002295 auth_config = auth.extract_auth_config_from_options(options)
2296
2297 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002298
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002299 current = cl.GetBranch()
2300 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2301 if not settings.GetIsGitSvn() and remote == '.':
2302 print
2303 print 'Attempting to push branch %r into another local branch!' % current
2304 print
2305 print 'Either reparent this branch on top of origin/master:'
2306 print ' git reparent-branch --root'
2307 print
2308 print 'OR run `git rebase-update` if you think the parent branch is already'
2309 print 'committed.'
2310 print
2311 print ' Current parent: %r' % upstream_branch
2312 return 1
2313
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002314 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002315 # Default to merging against our best guess of the upstream branch.
2316 args = [cl.GetUpstreamBranch()]
2317
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002318 if options.contributor:
2319 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2320 print "Please provide contibutor as 'First Last <email@example.com>'"
2321 return 1
2322
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002323 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002324 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002325
sbc@chromium.org71437c02015-04-09 19:29:40 +00002326 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002327 return 1
2328
2329 # This rev-list syntax means "show all commits not in my branch that
2330 # are in base_branch".
2331 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2332 base_branch]).splitlines()
2333 if upstream_commits:
2334 print ('Base branch "%s" has %d commits '
2335 'not in this branch.' % (base_branch, len(upstream_commits)))
2336 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2337 return 1
2338
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002339 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002340 svn_head = None
2341 if cmd == 'dcommit' or base_has_submodules:
2342 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2343 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002344
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002345 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002346 # If the base_head is a submodule merge commit, the first parent of the
2347 # base_head should be a git-svn commit, which is what we're interested in.
2348 base_svn_head = base_branch
2349 if base_has_submodules:
2350 base_svn_head += '^1'
2351
2352 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002353 if extra_commits:
2354 print ('This branch has %d additional commits not upstreamed yet.'
2355 % len(extra_commits.splitlines()))
2356 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2357 'before attempting to %s.' % (base_branch, cmd))
2358 return 1
2359
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002360 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002361 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002362 author = None
2363 if options.contributor:
2364 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002365 hook_results = cl.RunHook(
2366 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002367 may_prompt=not options.force,
2368 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002369 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002370 if not hook_results.should_continue():
2371 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002372
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002373 # Check the tree status if the tree status URL is set.
2374 status = GetTreeStatus()
2375 if 'closed' == status:
2376 print('The tree is closed. Please wait for it to reopen. Use '
2377 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2378 return 1
2379 elif 'unknown' == status:
2380 print('Unable to determine tree status. Please verify manually and '
2381 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2382 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002383 else:
2384 breakpad.SendStack(
2385 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002386 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2387 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002388 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002389
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002390 change_desc = ChangeDescription(options.message)
2391 if not change_desc.description and cl.GetIssue():
2392 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002393
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002394 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002395 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002396 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002397 else:
2398 print 'No description set.'
2399 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2400 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002401
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002402 # Keep a separate copy for the commit message, because the commit message
2403 # contains the link to the Rietveld issue, while the Rietveld message contains
2404 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002405 # Keep a separate copy for the commit message.
2406 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002407 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002408
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002409 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002410 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00002411 # Xcode won't linkify this URL unless there is a non-whitespace character
2412 # after it. Add a period on a new line to circumvent this.
2413 commit_desc.append_footer('Review URL: %s.' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002414 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002415 commit_desc.append_footer('Patch from %s.' % options.contributor)
2416
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002417 print('Description:')
2418 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002419
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002420 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002421 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002422 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002423
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002424 # We want to squash all this branch's commits into one commit with the proper
2425 # description. We do this by doing a "reset --soft" to the base branch (which
2426 # keeps the working copy the same), then dcommitting that. If origin/master
2427 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2428 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002429 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002430 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2431 # Delete the branches if they exist.
2432 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2433 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2434 result = RunGitWithCode(showref_cmd)
2435 if result[0] == 0:
2436 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002437
2438 # We might be in a directory that's present in this branch but not in the
2439 # trunk. Move up to the top of the tree so that git commands that expect a
2440 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002441 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002442 if rel_base_path:
2443 os.chdir(rel_base_path)
2444
2445 # Stuff our change into the merge branch.
2446 # We wrap in a try...finally block so if anything goes wrong,
2447 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002448 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002449 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002450 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002451 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002452 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002453 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002454 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002455 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002456 RunGit(
2457 [
2458 'commit', '--author', options.contributor,
2459 '-m', commit_desc.description,
2460 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002461 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002462 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002463 if base_has_submodules:
2464 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2465 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2466 RunGit(['checkout', CHERRY_PICK_BRANCH])
2467 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002468 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002469 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002470 pending_prefix = settings.GetPendingRefPrefix()
2471 if not pending_prefix or branch.startswith(pending_prefix):
2472 # If not using refs/pending/heads/* at all, or target ref is already set
2473 # to pending, then push to the target ref directly.
2474 retcode, output = RunGitWithCode(
2475 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002476 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002477 else:
2478 # Cherry-pick the change on top of pending ref and then push it.
2479 assert branch.startswith('refs/'), branch
2480 assert pending_prefix[-1] == '/', pending_prefix
2481 pending_ref = pending_prefix + branch[len('refs/'):]
2482 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002483 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002484 if retcode == 0:
2485 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002486 else:
2487 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002488 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002489 'svn', 'dcommit',
2490 '-C%s' % options.similarity,
2491 '--no-rebase', '--rmdir',
2492 ]
2493 if settings.GetForceHttpsCommitUrl():
2494 # Allow forcing https commit URLs for some projects that don't allow
2495 # committing to http URLs (like Google Code).
2496 remote_url = cl.GetGitSvnRemoteUrl()
2497 if urlparse.urlparse(remote_url).scheme == 'http':
2498 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002499 cmd_args.append('--commit-url=%s' % remote_url)
2500 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002501 if 'Committed r' in output:
2502 revision = re.match(
2503 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2504 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002505 finally:
2506 # And then swap back to the original branch and clean up.
2507 RunGit(['checkout', '-q', cl.GetBranch()])
2508 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002509 if base_has_submodules:
2510 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002511
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002512 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002513 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002514 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002515
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002516 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002517 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002518 try:
2519 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2520 # We set pushed_to_pending to False, since it made it all the way to the
2521 # real ref.
2522 pushed_to_pending = False
2523 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002524 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002525
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002526 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002527 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002528 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002529 if not to_pending:
2530 if viewvc_url and revision:
2531 change_desc.append_footer(
2532 'Committed: %s%s' % (viewvc_url, revision))
2533 elif revision:
2534 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002535 print ('Closing issue '
2536 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002537 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002538 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002539 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002540 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002541 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002542 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002543 if options.bypass_hooks:
2544 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2545 else:
2546 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002547 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002548 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002549
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002550 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002551 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2552 print 'The commit is in the pending queue (%s).' % pending_ref
2553 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002554 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002555 'footer.' % branch)
2556
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002557 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2558 if os.path.isfile(hook):
2559 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002560
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002561 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002562
2563
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002564def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2565 print
2566 print 'Waiting for commit to be landed on %s...' % real_ref
2567 print '(If you are impatient, you may Ctrl-C once without harm)'
2568 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2569 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2570
2571 loop = 0
2572 while True:
2573 sys.stdout.write('fetching (%d)... \r' % loop)
2574 sys.stdout.flush()
2575 loop += 1
2576
2577 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2578 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2579 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2580 for commit in commits.splitlines():
2581 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2582 print 'Found commit on %s' % real_ref
2583 return commit
2584
2585 current_rev = to_rev
2586
2587
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002588def PushToGitPending(remote, pending_ref, upstream_ref):
2589 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2590
2591 Returns:
2592 (retcode of last operation, output log of last operation).
2593 """
2594 assert pending_ref.startswith('refs/'), pending_ref
2595 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2596 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2597 code = 0
2598 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002599 max_attempts = 3
2600 attempts_left = max_attempts
2601 while attempts_left:
2602 if attempts_left != max_attempts:
2603 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2604 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002605
2606 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002607 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002608 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002609 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002610 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002611 print 'Fetch failed with exit code %d.' % code
2612 if out.strip():
2613 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002614 continue
2615
2616 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002617 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002618 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002619 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002620 if code:
2621 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002622 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2623 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002624 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2625 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002626 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002627 return code, out
2628
2629 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002630 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002631 code, out = RunGitWithCode(
2632 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2633 if code == 0:
2634 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002635 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002636 return code, out
2637
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002638 print 'Push failed with exit code %d.' % code
2639 if out.strip():
2640 print out.strip()
2641 if IsFatalPushFailure(out):
2642 print (
2643 'Fatal push error. Make sure your .netrc credentials and git '
2644 'user.email are correct and you have push access to the repo.')
2645 return code, out
2646
2647 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002648 return code, out
2649
2650
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002651def IsFatalPushFailure(push_stdout):
2652 """True if retrying push won't help."""
2653 return '(prohibited by Gerrit)' in push_stdout
2654
2655
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002656@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002657def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002658 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002659 if not settings.GetIsGitSvn():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002660 if get_footer_svn_id():
2661 # If it looks like previous commits were mirrored with git-svn.
2662 message = """This repository appears to be a git-svn mirror, but no
2663upstream SVN master is set. You probably need to run 'git auto-svn' once."""
2664 else:
2665 message = """This doesn't appear to be an SVN repository.
2666If your project has a true, writeable git repository, you probably want to run
2667'git cl land' instead.
2668If your project has a git mirror of an upstream SVN master, you probably need
2669to run 'git svn init'.
2670
2671Using the wrong command might cause your commit to appear to succeed, and the
2672review to be closed, without actually landing upstream. If you choose to
2673proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002674 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002675 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002676 return SendUpstream(parser, args, 'dcommit')
2677
2678
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002679@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002680def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002681 """Commits the current changelist via git."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002682 if settings.GetIsGitSvn() or get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002683 print('This appears to be an SVN repository.')
2684 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00002685 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00002686 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002687 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002688
2689
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002690@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002691def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002692 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002693 parser.add_option('-b', dest='newbranch',
2694 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002695 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002696 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002697 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2698 help='Change to the directory DIR immediately, '
2699 'before doing anything else.')
2700 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002701 help='failed patches spew .rej files rather than '
2702 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002703 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2704 help="don't commit after patch applies")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002705 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002706 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002707 auth_config = auth.extract_auth_config_from_options(options)
2708
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002709 if len(args) != 1:
2710 parser.print_help()
2711 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002712 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002713
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002714 # We don't want uncommitted changes mixed up with the patch.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002715 if git_common.is_dirty_git_tree('patch'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002716 return 1
2717
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002718 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002719 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002720
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002721 if options.newbranch:
2722 if options.force:
2723 RunGit(['branch', '-D', options.newbranch],
2724 stderr=subprocess2.PIPE, error_ok=True)
2725 RunGit(['checkout', '-b', options.newbranch,
2726 Changelist().GetUpstreamBranch()])
2727
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002728 return PatchIssue(issue_arg, options.reject, options.nocommit,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002729 options.directory, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002730
2731
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002732def PatchIssue(issue_arg, reject, nocommit, directory, auth_config):
wychen@chromium.orga872e752015-04-28 23:42:18 +00002733 # PatchIssue should never be called with a dirty tree. It is up to the
2734 # caller to check this, but just in case we assert here since the
2735 # consequences of the caller not checking this could be dire.
sbc@chromium.org71437c02015-04-09 19:29:40 +00002736 assert(not git_common.is_dirty_git_tree('apply'))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00002737
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002738 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002739 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002740 issue = int(issue_arg)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002741 cl = Changelist(issue=issue, auth_config=auth_config)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002742 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002743 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002744 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002745 # Assume it's a URL to the patch. Default to https.
2746 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
kjellander@chromium.org44424542015-06-02 18:35:29 +00002747 match = re.match(r'(.*?)/download/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002748 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002749 DieWithError('Must pass an issue ID or full URL for '
2750 '\'Download raw patch set\'')
kjellander@chromium.org44424542015-06-02 18:35:29 +00002751 issue = int(match.group(2))
2752 cl = Changelist(issue=issue, auth_config=auth_config)
2753 cl.rietveld_server = match.group(1)
2754 patchset = int(match.group(3))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002755 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002756
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002757 # Switch up to the top-level directory, if necessary, in preparation for
2758 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002759 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002760 if top:
2761 os.chdir(top)
2762
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002763 # Git patches have a/ at the beginning of source paths. We strip that out
2764 # with a sed script rather than the -p flag to patch so we can feed either
2765 # Git or svn-style patches into the same apply command.
2766 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002767 try:
2768 patch_data = subprocess2.check_output(
2769 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2770 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002771 DieWithError('Git patch mungling failed.')
2772 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002773
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002774 # We use "git apply" to apply the patch instead of "patch" so that we can
2775 # pick up file adds.
2776 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002777 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002778 if directory:
2779 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002780 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002781 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002782 elif IsGitVersionAtLeast('1.7.12'):
2783 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002784 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002785 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002786 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002787 except subprocess2.CalledProcessError:
wychen@chromium.orga872e752015-04-28 23:42:18 +00002788 print 'Failed to apply the patch'
2789 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002790
2791 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002792 if not nocommit:
wychen@chromium.org5b3bebb2015-05-28 21:41:43 +00002793 RunGit(['commit', '-m', (cl.GetDescription() + '\n\n' +
2794 'patch from issue %(i)s at patchset '
carlosk@chromium.org71284d92014-11-14 18:12:50 +00002795 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2796 % {'i': issue, 'p': patchset})])
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002797 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002798 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002799 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002800 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002801 else:
2802 print "Patch applied to index."
2803 return 0
2804
2805
2806def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002807 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002808 # Provide a wrapper for git svn rebase to help avoid accidental
2809 # git svn dcommit.
2810 # It's the only command that doesn't use parser at all since we just defer
2811 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002812
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002813 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002814
2815
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002816def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002817 """Fetches the tree status and returns either 'open', 'closed',
2818 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002819 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002820 if url:
2821 status = urllib2.urlopen(url).read().lower()
2822 if status.find('closed') != -1 or status == '0':
2823 return 'closed'
2824 elif status.find('open') != -1 or status == '1':
2825 return 'open'
2826 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002827 return 'unset'
2828
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002829
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002830def GetTreeStatusReason():
2831 """Fetches the tree status from a json url and returns the message
2832 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002833 url = settings.GetTreeStatusUrl()
2834 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002835 connection = urllib2.urlopen(json_url)
2836 status = json.loads(connection.read())
2837 connection.close()
2838 return status['message']
2839
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002840
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002841def GetBuilderMaster(bot_list):
2842 """For a given builder, fetch the master from AE if available."""
2843 map_url = 'https://builders-map.appspot.com/'
2844 try:
2845 master_map = json.load(urllib2.urlopen(map_url))
2846 except urllib2.URLError as e:
2847 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2848 (map_url, e))
2849 except ValueError as e:
2850 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2851 if not master_map:
2852 return None, 'Failed to build master map.'
2853
2854 result_master = ''
2855 for bot in bot_list:
2856 builder = bot.split(':', 1)[0]
2857 master_list = master_map.get(builder, [])
2858 if not master_list:
2859 return None, ('No matching master for builder %s.' % builder)
2860 elif len(master_list) > 1:
2861 return None, ('The builder name %s exists in multiple masters %s.' %
2862 (builder, master_list))
2863 else:
2864 cur_master = master_list[0]
2865 if not result_master:
2866 result_master = cur_master
2867 elif result_master != cur_master:
2868 return None, 'The builders do not belong to the same master.'
2869 return result_master, None
2870
2871
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002872def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002873 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002874 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002875 status = GetTreeStatus()
2876 if 'unset' == status:
2877 print 'You must configure your tree status URL by running "git cl config".'
2878 return 2
2879
2880 print "The tree is %s" % status
2881 print
2882 print GetTreeStatusReason()
2883 if status != 'open':
2884 return 1
2885 return 0
2886
2887
maruel@chromium.org15192402012-09-06 12:38:29 +00002888def CMDtry(parser, args):
2889 """Triggers a try job through Rietveld."""
2890 group = optparse.OptionGroup(parser, "Try job options")
2891 group.add_option(
2892 "-b", "--bot", action="append",
2893 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2894 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002895 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002896 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002897 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00002898 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002899 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002900 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002901 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002902 "-r", "--revision",
2903 help="Revision to use for the try job; default: the "
2904 "revision will be determined by the try server; see "
2905 "its waterfall for more info")
2906 group.add_option(
2907 "-c", "--clobber", action="store_true", default=False,
2908 help="Force a clobber before building; e.g. don't do an "
2909 "incremental build")
2910 group.add_option(
2911 "--project",
2912 help="Override which project to use. Projects are defined "
2913 "server-side to define what default bot set to use")
2914 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002915 "-n", "--name", help="Try job name; default to current branch name")
sheyang@google.com6ebaf782015-05-12 19:17:54 +00002916 group.add_option(
2917 "--use-buildbucket", action="store_true", default=False,
2918 help="Use buildbucket to trigger try jobs.")
maruel@chromium.org15192402012-09-06 12:38:29 +00002919 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002920 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00002921 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002922 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00002923
2924 if args:
2925 parser.error('Unknown arguments: %s' % args)
2926
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002927 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00002928 if not cl.GetIssue():
2929 parser.error('Need to upload first')
2930
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002931 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002932 if props.get('closed'):
2933 parser.error('Cannot send tryjobs for a closed CL')
2934
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002935 if props.get('private'):
2936 parser.error('Cannot use trybots with private issue')
2937
maruel@chromium.org15192402012-09-06 12:38:29 +00002938 if not options.name:
2939 options.name = cl.GetBranch()
2940
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002941 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002942 options.master, err_msg = GetBuilderMaster(options.bot)
2943 if err_msg:
2944 parser.error('Tryserver master cannot be found because: %s\n'
2945 'Please manually specify the tryserver master'
2946 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002947
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002948 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002949 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002950 if not options.bot:
2951 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002952
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002953 # Get try masters from PRESUBMIT.py files.
2954 masters = presubmit_support.DoGetTryMasters(
2955 change,
2956 change.LocalPaths(),
2957 settings.GetRoot(),
2958 None,
2959 None,
2960 options.verbose,
2961 sys.stdout)
2962 if masters:
2963 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002964
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002965 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2966 options.bot = presubmit_support.DoGetTrySlaves(
2967 change,
2968 change.LocalPaths(),
2969 settings.GetRoot(),
2970 None,
2971 None,
2972 options.verbose,
2973 sys.stdout)
2974 if not options.bot:
2975 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002976
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002977 builders_and_tests = {}
2978 # TODO(machenbach): The old style command-line options don't support
2979 # multiple try masters yet.
2980 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2981 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2982
2983 for bot in old_style:
2984 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002985 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002986 elif ',' in bot:
2987 parser.error('Specify one bot per --bot flag')
2988 else:
2989 builders_and_tests.setdefault(bot, []).append('defaulttests')
2990
2991 for bot, tests in new_style:
2992 builders_and_tests.setdefault(bot, []).extend(tests)
2993
2994 # Return a master map with one master to be backwards compatible. The
2995 # master name defaults to an empty string, which will cause the master
2996 # not to be set on rietveld (deprecated).
2997 return {options.master: builders_and_tests}
2998
2999 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00003000
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003001 for builders in masters.itervalues():
3002 if any('triggered' in b for b in builders):
3003 print >> sys.stderr, (
3004 'ERROR You are trying to send a job to a triggered bot. This type of'
3005 ' bot requires an\ninitial job from a parent (usually a builder). '
3006 'Instead send your job to the parent.\n'
3007 'Bot list: %s' % builders)
3008 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00003009
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00003010 patchset = cl.GetMostRecentPatchset()
3011 if patchset and patchset != cl.GetPatchset():
3012 print(
3013 '\nWARNING Mismatch between local config and server. Did a previous '
3014 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
3015 'Continuing using\npatchset %s.\n' % patchset)
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003016 if options.use_buildbucket:
3017 try:
3018 trigger_try_jobs(auth_config, cl, options, masters, 'git_cl_try')
3019 except BuildbucketResponseException as ex:
3020 print 'ERROR: %s' % ex
fischman@chromium.orgd246c972013-12-21 22:47:38 +00003021 return 1
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003022 except Exception as e:
3023 stacktrace = (''.join(traceback.format_stack()) + traceback.format_exc())
3024 print 'ERROR: Exception when trying to trigger tryjobs: %s\n%s' % (
3025 e, stacktrace)
3026 return 1
3027 else:
3028 try:
3029 cl.RpcServer().trigger_distributed_try_jobs(
3030 cl.GetIssue(), patchset, options.name, options.clobber,
3031 options.revision, masters)
3032 except urllib2.HTTPError as e:
3033 if e.code == 404:
3034 print('404 from rietveld; '
3035 'did you mean to use "git try" instead of "git cl try"?')
3036 return 1
3037 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00003038
sheyang@google.com6ebaf782015-05-12 19:17:54 +00003039 for (master, builders) in sorted(masters.iteritems()):
3040 if master:
3041 print 'Master: %s' % master
3042 length = max(len(builder) for builder in builders)
3043 for builder in sorted(builders):
3044 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00003045 return 0
3046
3047
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003048@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003049def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003050 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00003051 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003052 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003053 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003054
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003055 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003056 if args:
3057 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003058 branch = cl.GetBranch()
3059 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003060 cl = Changelist()
3061 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00003062
3063 # Clear configured merge-base, if there is one.
3064 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00003065 else:
3066 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003067 return 0
3068
3069
thestig@chromium.org00858c82013-12-02 23:08:03 +00003070def CMDweb(parser, args):
3071 """Opens the current CL in the web browser."""
3072 _, args = parser.parse_args(args)
3073 if args:
3074 parser.error('Unrecognized args: %s' % ' '.join(args))
3075
3076 issue_url = Changelist().GetIssueURL()
3077 if not issue_url:
3078 print >> sys.stderr, 'ERROR No issue to open'
3079 return 1
3080
3081 webbrowser.open(issue_url)
3082 return 0
3083
3084
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003085def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003086 """Sets the commit bit to trigger the Commit Queue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003087 auth.add_auth_options(parser)
3088 options, args = parser.parse_args(args)
3089 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003090 if args:
3091 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003092 cl = Changelist(auth_config=auth_config)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00003093 props = cl.GetIssueProperties()
3094 if props.get('private'):
3095 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00003096 cl.SetFlag('commit', '1')
3097 return 0
3098
3099
groby@chromium.org411034a2013-02-26 15:12:01 +00003100def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003101 """Closes the issue."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003102 auth.add_auth_options(parser)
3103 options, args = parser.parse_args(args)
3104 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00003105 if args:
3106 parser.error('Unrecognized args: %s' % ' '.join(args))
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003107 cl = Changelist(auth_config=auth_config)
groby@chromium.org411034a2013-02-26 15:12:01 +00003108 # Ensure there actually is an issue to close.
3109 cl.GetDescription()
3110 cl.CloseIssue()
3111 return 0
3112
3113
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003114def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003115 """Shows differences between local tree and last upload."""
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003116 auth.add_auth_options(parser)
3117 options, args = parser.parse_args(args)
3118 auth_config = auth.extract_auth_config_from_options(options)
3119 if args:
3120 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003121
3122 # Uncommitted (staged and unstaged) changes will be destroyed by
3123 # "git reset --hard" if there are merging conflicts in PatchIssue().
3124 # Staged changes would be committed along with the patch from last
3125 # upload, hence counted toward the "last upload" side in the final
3126 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00003127 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00003128 return 1
3129
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003130 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003131 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003132 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00003133 if not issue:
3134 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003135 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003136 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003137
3138 # Create a new branch based on the merge-base
3139 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
3140 try:
3141 # Patch in the latest changes from rietveld.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003142 rtn = PatchIssue(issue, False, False, None, auth_config)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003143 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00003144 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003145 return rtn
3146
wychen@chromium.org06928532015-02-03 02:11:29 +00003147 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003148 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00003149 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00003150 finally:
3151 RunGit(['checkout', '-q', branch])
3152 RunGit(['branch', '-D', TMP_BRANCH])
3153
3154 return 0
3155
3156
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003157def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00003158 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003159 parser.add_option(
3160 '--no-color',
3161 action='store_true',
3162 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003163 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003164 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003165 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003166
3167 author = RunGit(['config', 'user.email']).strip() or None
3168
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003169 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003170
3171 if args:
3172 if len(args) > 1:
3173 parser.error('Unknown args')
3174 base_branch = args[0]
3175 else:
3176 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003177 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00003178
3179 change = cl.GetChange(base_branch, None)
3180 return owners_finder.OwnersFinder(
3181 [f.LocalPath() for f in
3182 cl.GetChange(base_branch, None).AffectedFiles()],
3183 change.RepositoryRoot(), author,
3184 fopen=file, os_path=os.path, glob=glob.glob,
3185 disable_color=options.no_color).run()
3186
3187
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003188def BuildGitDiffCmd(diff_type, upstream_commit, args, extensions):
3189 """Generates a diff command."""
3190 # Generate diff for the current branch's changes.
3191 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
3192 upstream_commit, '--' ]
3193
3194 if args:
3195 for arg in args:
3196 if os.path.isdir(arg):
3197 diff_cmd.extend(os.path.join(arg, '*' + ext) for ext in extensions)
3198 elif os.path.isfile(arg):
3199 diff_cmd.append(arg)
3200 else:
3201 DieWithError('Argument "%s" is not a file or a directory' % arg)
3202 else:
3203 diff_cmd.extend('*' + ext for ext in extensions)
3204
3205 return diff_cmd
3206
3207
enne@chromium.org555cfe42014-01-29 18:21:39 +00003208@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003209def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003210 """Runs auto-formatting tools (clang-format etc.) on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00003211 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003212 parser.add_option('--full', action='store_true',
3213 help='Reformat the full content of all touched files')
3214 parser.add_option('--dry-run', action='store_true',
3215 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003216 parser.add_option('--python', action='store_true',
3217 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003218 parser.add_option('--diff', action='store_true',
3219 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003220 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003221
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003222 # git diff generates paths against the root of the repository. Change
3223 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003224 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00003225 if rel_base_path:
3226 os.chdir(rel_base_path)
3227
digit@chromium.org29e47272013-05-17 17:01:46 +00003228 # Grab the merge-base commit, i.e. the upstream commit of the current
3229 # branch when it was created or the last time it was rebased. This is
3230 # to cover the case where the user may have called "git fetch origin",
3231 # moving the origin branch to a newer commit, but hasn't rebased yet.
3232 upstream_commit = None
3233 cl = Changelist()
3234 upstream_branch = cl.GetUpstreamBranch()
3235 if upstream_branch:
3236 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
3237 upstream_commit = upstream_commit.strip()
3238
3239 if not upstream_commit:
3240 DieWithError('Could not find base commit for this branch. '
3241 'Are you in detached state?')
3242
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003243 if opts.full:
3244 # Only list the names of modified files.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003245 diff_type = '--name-only'
enne@chromium.org555cfe42014-01-29 18:21:39 +00003246 else:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003247 # Only generate context-less patches.
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003248 diff_type = '-U0'
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003249
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003250 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, CLANG_EXTS)
digit@chromium.org29e47272013-05-17 17:01:46 +00003251 diff_output = RunGit(diff_cmd)
3252
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003253 top_dir = os.path.normpath(
3254 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
3255
3256 # Locate the clang-format binary in the checkout
3257 try:
3258 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
3259 except clang_format.NotFoundError, e:
3260 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00003261
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003262 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
3263 # formatted. This is used to block during the presubmit.
3264 return_value = 0
3265
digit@chromium.org29e47272013-05-17 17:01:46 +00003266 if opts.full:
3267 # diff_output is a list of files to send to clang-format.
3268 files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003269 if files:
3270 cmd = [clang_format_tool]
3271 if not opts.dry_run and not opts.diff:
3272 cmd.append('-i')
3273 stdout = RunCommand(cmd + files, cwd=top_dir)
3274 if opts.diff:
3275 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003276 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003277 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00003278 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00003279 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00003280 try:
3281 script = clang_format.FindClangFormatScriptInChromiumTree(
3282 'clang-format-diff.py')
3283 except clang_format.NotFoundError, e:
3284 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003285
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003286 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003287 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003288 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00003289
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003290 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00003291 if opts.diff:
3292 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00003293 if opts.dry_run and len(stdout) > 0:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003294 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003295
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00003296 # Similar code to above, but using yapf on .py files rather than clang-format
3297 # on C/C++ files
3298 if opts.python:
3299 diff_cmd = BuildGitDiffCmd(diff_type, upstream_commit, args, ['.py'])
3300 diff_output = RunGit(diff_cmd)
3301 yapf_tool = gclient_utils.FindExecutable('yapf')
3302 if yapf_tool is None:
3303 DieWithError('yapf not found in PATH')
3304
3305 if opts.full:
3306 files = diff_output.splitlines()
3307 if files:
3308 cmd = [yapf_tool]
3309 if not opts.dry_run and not opts.diff:
3310 cmd.append('-i')
3311 stdout = RunCommand(cmd + files, cwd=top_dir)
3312 if opts.diff:
3313 sys.stdout.write(stdout)
3314 else:
3315 # TODO(sbc): yapf --lines mode still has some issues.
3316 # https://github.com/google/yapf/issues/154
3317 DieWithError('--python currently only works with --full')
3318
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00003319 # Build a diff command that only operates on dart files. dart's formatter
3320 # does not have the nice property of only operating on modified chunks, so
3321 # hard code full.
3322 dart_diff_cmd = BuildGitDiffCmd('--name-only', upstream_commit,
3323 args, ['.dart'])
3324 dart_diff_output = RunGit(dart_diff_cmd)
3325 if dart_diff_output:
3326 try:
3327 command = [dart_format.FindDartFmtToolInChromiumTree()]
3328 if not opts.dry_run and not opts.diff:
3329 command.append('-w')
3330 command.extend(dart_diff_output.splitlines())
3331
3332 stdout = RunCommand(command, cwd=top_dir, env=env)
3333 if opts.dry_run and stdout:
3334 return_value = 2
3335 except dart_format.NotFoundError as e:
3336 print ('Unable to check dart code formatting. Dart SDK is not in ' +
3337 'this checkout.')
3338
3339 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00003340
3341
maruel@chromium.org29404b52014-09-08 22:58:00 +00003342def CMDlol(parser, args):
3343 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003344 print zlib.decompress(base64.b64decode(
3345 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3346 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3347 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3348 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003349 return 0
3350
3351
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003352class OptionParser(optparse.OptionParser):
3353 """Creates the option parse and add --verbose support."""
3354 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003355 optparse.OptionParser.__init__(
3356 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003357 self.add_option(
3358 '-v', '--verbose', action='count', default=0,
3359 help='Use 2 times for more debugging info')
3360
3361 def parse_args(self, args=None, values=None):
3362 options, args = optparse.OptionParser.parse_args(self, args, values)
3363 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3364 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3365 return options, args
3366
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003367
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003368def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003369 if sys.hexversion < 0x02060000:
3370 print >> sys.stderr, (
3371 '\nYour python version %s is unsupported, please upgrade.\n' %
3372 sys.version.split(' ', 1)[0])
3373 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003374
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003375 # Reload settings.
3376 global settings
3377 settings = Settings()
3378
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003379 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003380 dispatcher = subcommand.CommandDispatcher(__name__)
3381 try:
3382 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00003383 except auth.AuthenticationError as e:
3384 DieWithError(str(e))
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003385 except urllib2.HTTPError, e:
3386 if e.code != 500:
3387 raise
3388 DieWithError(
3389 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3390 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00003391 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003392
3393
3394if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003395 # These affect sys.stdout so do it outside of main() to simplify mocks in
3396 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003397 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003398 colorama.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00003399 try:
3400 sys.exit(main(sys.argv[1:]))
3401 except KeyboardInterrupt:
3402 sys.stderr.write('interrupted\n')
3403 sys.exit(1)