blob: 51577048d07216e3cba064aec652e30fd31fd772 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +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
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +010016import contextlib
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010017import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000018import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000019import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000021import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import optparse
23import os
24import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010025import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000026import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import textwrap
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000029import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000031import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000033import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000034import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000035
36try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080037 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038except ImportError:
39 pass
40
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000041from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000042from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000044import auth
skobes6468b902016-10-24 08:45:10 -070045import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000046import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000047import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000048import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000049import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000050import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000051import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000052import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000054import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000055import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000057import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000058import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000060import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import watchlists
63
tandrii7400cf02016-06-21 08:48:07 -070064__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000065
tandrii9d2c7a32016-06-22 03:42:45 -070066COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070067DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080068POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
borenet6c0efe62016-10-19 08:13:29 -070079# Buildbucket master name prefix.
80MASTER_PREFIX = 'master.'
81
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000082# Shortcut since it quickly becomes redundant.
83Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000084
maruel@chromium.orgddd59412011-11-30 14:20:38 +000085# Initialized in main()
86settings = None
87
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010088# Used by tests/git_cl_test.py to add extra logging.
89# Inside the weirdly failing test, add this:
90# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
91# And scroll up to see the strack trace printed.
92_IS_BEING_TESTED = False
93
maruel@chromium.orgddd59412011-11-30 14:20:38 +000094
Christopher Lamf732cd52017-01-24 12:40:11 +110095def DieWithError(message, change_desc=None):
96 if change_desc:
97 SaveDescriptionBackup(change_desc)
98
vapiera7fbd5a2016-06-16 09:17:49 -070099 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000100 sys.exit(1)
101
102
Christopher Lamf732cd52017-01-24 12:40:11 +1100103def SaveDescriptionBackup(change_desc):
104 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
105 print('\nError after CL description prompt -- saving description to %s\n' %
106 backup_path)
107 backup_file = open(backup_path, 'w')
108 backup_file.write(change_desc.description)
109 backup_file.close()
110
111
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000112def GetNoGitPagerEnv():
113 env = os.environ.copy()
114 # 'cat' is a magical git string that disables pagers on all platforms.
115 env['GIT_PAGER'] = 'cat'
116 return env
117
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000118
bsep@chromium.org627d9002016-04-29 00:00:52 +0000119def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000120 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000121 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000122 except subprocess2.CalledProcessError as e:
123 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000124 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000125 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000126 'Command "%s" failed.\n%s' % (
127 ' '.join(args), error_message or e.stdout or ''))
128 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000129
130
131def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000132 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000133 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000134
135
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000136def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000137 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700138 if suppress_stderr:
139 stderr = subprocess2.VOID
140 else:
141 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000142 try:
tandrii5d48c322016-08-18 16:19:37 -0700143 (out, _), code = subprocess2.communicate(['git'] + args,
144 env=GetNoGitPagerEnv(),
145 stdout=subprocess2.PIPE,
146 stderr=stderr)
147 return code, out
148 except subprocess2.CalledProcessError as e:
149 logging.debug('Failed running %s', args)
150 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000151
152
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000153def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000154 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000155 return RunGitWithCode(args, suppress_stderr=True)[1]
156
157
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000158def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000159 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000160 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000161 return (version.startswith(prefix) and
162 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000163
164
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000165def BranchExists(branch):
166 """Return True if specified branch exists."""
167 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
168 suppress_stderr=True)
169 return not code
170
171
tandrii2a16b952016-10-19 07:09:44 -0700172def time_sleep(seconds):
173 # Use this so that it can be mocked in tests without interfering with python
174 # system machinery.
175 import time # Local import to discourage others from importing time globally.
176 return time.sleep(seconds)
177
178
maruel@chromium.org90541732011-04-01 17:54:18 +0000179def ask_for_data(prompt):
180 try:
181 return raw_input(prompt)
182 except KeyboardInterrupt:
183 # Hide the exception.
184 sys.exit(1)
185
186
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100187def confirm_or_exit(prefix='', action='confirm'):
188 """Asks user to press enter to continue or press Ctrl+C to abort."""
189 if not prefix or prefix.endswith('\n'):
190 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100191 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100192 mid = ' Press'
193 elif prefix.endswith(' '):
194 mid = 'press'
195 else:
196 mid = ' press'
197 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
198
199
200def ask_for_explicit_yes(prompt):
201 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
202 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
203 while True:
204 if 'yes'.startswith(result):
205 return True
206 if 'no'.startswith(result):
207 return False
208 result = ask_for_data('Please, type yes or no: ').lower()
209
210
tandrii5d48c322016-08-18 16:19:37 -0700211def _git_branch_config_key(branch, key):
212 """Helper method to return Git config key for a branch."""
213 assert branch, 'branch name is required to set git config for it'
214 return 'branch.%s.%s' % (branch, key)
215
216
217def _git_get_branch_config_value(key, default=None, value_type=str,
218 branch=False):
219 """Returns git config value of given or current branch if any.
220
221 Returns default in all other cases.
222 """
223 assert value_type in (int, str, bool)
224 if branch is False: # Distinguishing default arg value from None.
225 branch = GetCurrentBranch()
226
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000227 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700228 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000229
tandrii5d48c322016-08-18 16:19:37 -0700230 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700231 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700232 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700233 # git config also has --int, but apparently git config suffers from integer
234 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700235 args.append(_git_branch_config_key(branch, key))
236 code, out = RunGitWithCode(args)
237 if code == 0:
238 value = out.strip()
239 if value_type == int:
240 return int(value)
241 if value_type == bool:
242 return bool(value.lower() == 'true')
243 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000244 return default
245
246
tandrii5d48c322016-08-18 16:19:37 -0700247def _git_set_branch_config_value(key, value, branch=None, **kwargs):
248 """Sets the value or unsets if it's None of a git branch config.
249
250 Valid, though not necessarily existing, branch must be provided,
251 otherwise currently checked out branch is used.
252 """
253 if not branch:
254 branch = GetCurrentBranch()
255 assert branch, 'a branch name OR currently checked out branch is required'
256 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700257 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700258 if value is None:
259 args.append('--unset')
260 elif isinstance(value, bool):
261 args.append('--bool')
262 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700263 else:
tandrii33a46ff2016-08-23 05:53:40 -0700264 # git config also has --int, but apparently git config suffers from integer
265 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700266 value = str(value)
267 args.append(_git_branch_config_key(branch, key))
268 if value is not None:
269 args.append(value)
270 RunGit(args, **kwargs)
271
272
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100273def _get_committer_timestamp(commit):
274 """Returns unix timestamp as integer of a committer in a commit.
275
276 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
277 """
278 # Git also stores timezone offset, but it only affects visual display,
279 # actual point in time is defined by this timestamp only.
280 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
281
282
283def _git_amend_head(message, committer_timestamp):
284 """Amends commit with new message and desired committer_timestamp.
285
286 Sets committer timezone to UTC.
287 """
288 env = os.environ.copy()
289 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
290 return RunGit(['commit', '--amend', '-m', message], env=env)
291
292
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000293def add_git_similarity(parser):
294 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700295 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000296 help='Sets the percentage that a pair of files need to match in order to'
297 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000298 parser.add_option(
299 '--find-copies', action='store_true',
300 help='Allows git to look for copies.')
301 parser.add_option(
302 '--no-find-copies', action='store_false', dest='find_copies',
303 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000304
305 old_parser_args = parser.parse_args
306 def Parse(args):
307 options, args = old_parser_args(args)
308
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000309 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700310 options.similarity = _git_get_branch_config_value(
311 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000312 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000313 print('Note: Saving similarity of %d%% in git config.'
314 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700315 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000316
iannucci@chromium.org79540052012-10-19 23:15:26 +0000317 options.similarity = max(0, min(options.similarity, 100))
318
319 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700320 options.find_copies = _git_get_branch_config_value(
321 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000322 else:
tandrii5d48c322016-08-18 16:19:37 -0700323 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000324
325 print('Using %d%% similarity for rename/copy detection. '
326 'Override with --similarity.' % options.similarity)
327
328 return options, args
329 parser.parse_args = Parse
330
331
machenbach@chromium.org45453142015-09-15 08:45:22 +0000332def _get_properties_from_options(options):
333 properties = dict(x.split('=', 1) for x in options.properties)
334 for key, val in properties.iteritems():
335 try:
336 properties[key] = json.loads(val)
337 except ValueError:
338 pass # If a value couldn't be evaluated, treat it as a string.
339 return properties
340
341
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000342def _prefix_master(master):
343 """Convert user-specified master name to full master name.
344
345 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
346 name, while the developers always use shortened master name
347 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
348 function does the conversion for buildbucket migration.
349 """
borenet6c0efe62016-10-19 08:13:29 -0700350 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000351 return master
borenet6c0efe62016-10-19 08:13:29 -0700352 return '%s%s' % (MASTER_PREFIX, master)
353
354
355def _unprefix_master(bucket):
356 """Convert bucket name to shortened master name.
357
358 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
359 name, while the developers always use shortened master name
360 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
361 function does the conversion for buildbucket migration.
362 """
363 if bucket.startswith(MASTER_PREFIX):
364 return bucket[len(MASTER_PREFIX):]
365 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000366
367
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000368def _buildbucket_retry(operation_name, http, *args, **kwargs):
369 """Retries requests to buildbucket service and returns parsed json content."""
370 try_count = 0
371 while True:
372 response, content = http.request(*args, **kwargs)
373 try:
374 content_json = json.loads(content)
375 except ValueError:
376 content_json = None
377
378 # Buildbucket could return an error even if status==200.
379 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000380 error = content_json.get('error')
381 if error.get('code') == 403:
382 raise BuildbucketResponseException(
383 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000384 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000385 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000386 raise BuildbucketResponseException(msg)
387
388 if response.status == 200:
389 if not content_json:
390 raise BuildbucketResponseException(
391 'Buildbucket returns invalid json content: %s.\n'
392 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
393 content)
394 return content_json
395 if response.status < 500 or try_count >= 2:
396 raise httplib2.HttpLib2Error(content)
397
398 # status >= 500 means transient failures.
399 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700400 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000401 try_count += 1
402 assert False, 'unreachable'
403
404
qyearsley1fdfcb62016-10-24 13:22:03 -0700405def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700406 """Returns a dict mapping bucket names to builders and tests,
407 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700408 """
qyearsleydd49f942016-10-28 11:57:22 -0700409 # If no bots are listed, we try to get a set of builders and tests based
410 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700411 if not options.bot:
412 change = changelist.GetChange(
413 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700414 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700415 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700416 change=change,
417 changed_files=change.LocalPaths(),
418 repository_root=settings.GetRoot(),
419 default_presubmit=None,
420 project=None,
421 verbose=options.verbose,
422 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700423 if masters is None:
424 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100425 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700426
qyearsley1fdfcb62016-10-24 13:22:03 -0700427 if options.bucket:
428 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700429 if options.master:
430 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700431
qyearsleydd49f942016-10-28 11:57:22 -0700432 # If bots are listed but no master or bucket, then we need to find out
433 # the corresponding master for each bot.
434 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
435 if error_message:
436 option_parser.error(
437 'Tryserver master cannot be found because: %s\n'
438 'Please manually specify the tryserver master, e.g. '
439 '"-m tryserver.chromium.linux".' % error_message)
440 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700441
442
qyearsley123a4682016-10-26 09:12:17 -0700443def _get_bucket_map_for_builders(builders):
444 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700445 map_url = 'https://builders-map.appspot.com/'
446 try:
qyearsley123a4682016-10-26 09:12:17 -0700447 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700448 except urllib2.URLError as e:
449 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
450 (map_url, e))
451 except ValueError as e:
452 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700453 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700454 return None, 'Failed to build master map.'
455
qyearsley123a4682016-10-26 09:12:17 -0700456 bucket_map = {}
457 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700458 masters = builders_map.get(builder, [])
459 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700460 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700461 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700462 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700463 (builder, masters))
464 bucket = _prefix_master(masters[0])
465 bucket_map.setdefault(bucket, {})[builder] = []
466
467 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700468
469
borenet6c0efe62016-10-19 08:13:29 -0700470def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700471 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700472 """Sends a request to Buildbucket to trigger try jobs for a changelist.
473
474 Args:
475 auth_config: AuthConfig for Rietveld.
476 changelist: Changelist that the try jobs are associated with.
477 buckets: A nested dict mapping bucket names to builders to tests.
478 options: Command-line options.
479 """
tandriide281ae2016-10-12 06:02:30 -0700480 assert changelist.GetIssue(), 'CL must be uploaded first'
481 codereview_url = changelist.GetCodereviewServer()
482 assert codereview_url, 'CL must be uploaded first'
483 patchset = patchset or changelist.GetMostRecentPatchset()
484 assert patchset, 'CL must be uploaded first'
485
486 codereview_host = urlparse.urlparse(codereview_url).hostname
487 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000488 http = authenticator.authorize(httplib2.Http())
489 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700490
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000491 buildbucket_put_url = (
492 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000493 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700494 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
495 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
496 hostname=codereview_host,
497 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000498 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700499
500 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
501 shared_parameters_properties['category'] = category
502 if options.clobber:
503 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700504 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700505 if extra_properties:
506 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000507
508 batch_req_body = {'builds': []}
509 print_text = []
510 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700511 for bucket, builders_and_tests in sorted(buckets.iteritems()):
512 print_text.append('Bucket: %s' % bucket)
513 master = None
514 if bucket.startswith(MASTER_PREFIX):
515 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000516 for builder, tests in sorted(builders_and_tests.iteritems()):
517 print_text.append(' %s: %s' % (builder, tests))
518 parameters = {
519 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000520 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100521 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000522 'revision': options.revision,
523 }],
tandrii8c5a3532016-11-04 07:52:02 -0700524 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000525 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000526 if 'presubmit' in builder.lower():
527 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000528 if tests:
529 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700530
531 tags = [
532 'builder:%s' % builder,
533 'buildset:%s' % buildset,
534 'user_agent:git_cl_try',
535 ]
536 if master:
537 parameters['properties']['master'] = master
538 tags.append('master:%s' % master)
539
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000540 batch_req_body['builds'].append(
541 {
542 'bucket': bucket,
543 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000544 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700545 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000546 }
547 )
548
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000549 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700550 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000551 http,
552 buildbucket_put_url,
553 'PUT',
554 body=json.dumps(batch_req_body),
555 headers={'Content-Type': 'application/json'}
556 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000557 print_text.append('To see results here, run: git cl try-results')
558 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700559 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000560
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000561
tandrii221ab252016-10-06 08:12:04 -0700562def fetch_try_jobs(auth_config, changelist, buildbucket_host,
563 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700564 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000565
qyearsley53f48a12016-09-01 10:45:13 -0700566 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000567 """
tandrii221ab252016-10-06 08:12:04 -0700568 assert buildbucket_host
569 assert changelist.GetIssue(), 'CL must be uploaded first'
570 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
571 patchset = patchset or changelist.GetMostRecentPatchset()
572 assert patchset, 'CL must be uploaded first'
573
574 codereview_url = changelist.GetCodereviewServer()
575 codereview_host = urlparse.urlparse(codereview_url).hostname
576 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000577 if authenticator.has_cached_credentials():
578 http = authenticator.authorize(httplib2.Http())
579 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700580 print('Warning: Some results might be missing because %s' %
581 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700582 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000583 http = httplib2.Http()
584
585 http.force_exception_to_status_code = True
586
tandrii221ab252016-10-06 08:12:04 -0700587 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
588 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
589 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000590 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700591 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000592 params = {'tag': 'buildset:%s' % buildset}
593
594 builds = {}
595 while True:
596 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700597 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000598 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700599 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000600 for build in content.get('builds', []):
601 builds[build['id']] = build
602 if 'next_cursor' in content:
603 params['start_cursor'] = content['next_cursor']
604 else:
605 break
606 return builds
607
608
qyearsleyeab3c042016-08-24 09:18:28 -0700609def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000610 """Prints nicely result of fetch_try_jobs."""
611 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700612 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000613 return
614
615 # Make a copy, because we'll be modifying builds dictionary.
616 builds = builds.copy()
617 builder_names_cache = {}
618
619 def get_builder(b):
620 try:
621 return builder_names_cache[b['id']]
622 except KeyError:
623 try:
624 parameters = json.loads(b['parameters_json'])
625 name = parameters['builder_name']
626 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700627 print('WARNING: failed to get builder name for build %s: %s' % (
628 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000629 name = None
630 builder_names_cache[b['id']] = name
631 return name
632
633 def get_bucket(b):
634 bucket = b['bucket']
635 if bucket.startswith('master.'):
636 return bucket[len('master.'):]
637 return bucket
638
639 if options.print_master:
640 name_fmt = '%%-%ds %%-%ds' % (
641 max(len(str(get_bucket(b))) for b in builds.itervalues()),
642 max(len(str(get_builder(b))) for b in builds.itervalues()))
643 def get_name(b):
644 return name_fmt % (get_bucket(b), get_builder(b))
645 else:
646 name_fmt = '%%-%ds' % (
647 max(len(str(get_builder(b))) for b in builds.itervalues()))
648 def get_name(b):
649 return name_fmt % get_builder(b)
650
651 def sort_key(b):
652 return b['status'], b.get('result'), get_name(b), b.get('url')
653
654 def pop(title, f, color=None, **kwargs):
655 """Pop matching builds from `builds` dict and print them."""
656
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000657 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000658 colorize = str
659 else:
660 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
661
662 result = []
663 for b in builds.values():
664 if all(b.get(k) == v for k, v in kwargs.iteritems()):
665 builds.pop(b['id'])
666 result.append(b)
667 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700668 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000669 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700670 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000671
672 total = len(builds)
673 pop(status='COMPLETED', result='SUCCESS',
674 title='Successes:', color=Fore.GREEN,
675 f=lambda b: (get_name(b), b.get('url')))
676 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
677 title='Infra Failures:', color=Fore.MAGENTA,
678 f=lambda b: (get_name(b), b.get('url')))
679 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
680 title='Failures:', color=Fore.RED,
681 f=lambda b: (get_name(b), b.get('url')))
682 pop(status='COMPLETED', result='CANCELED',
683 title='Canceled:', color=Fore.MAGENTA,
684 f=lambda b: (get_name(b),))
685 pop(status='COMPLETED', result='FAILURE',
686 failure_reason='INVALID_BUILD_DEFINITION',
687 title='Wrong master/builder name:', color=Fore.MAGENTA,
688 f=lambda b: (get_name(b),))
689 pop(status='COMPLETED', result='FAILURE',
690 title='Other failures:',
691 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
692 pop(status='COMPLETED',
693 title='Other finished:',
694 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
695 pop(status='STARTED',
696 title='Started:', color=Fore.YELLOW,
697 f=lambda b: (get_name(b), b.get('url')))
698 pop(status='SCHEDULED',
699 title='Scheduled:',
700 f=lambda b: (get_name(b), 'id=%s' % b['id']))
701 # The last section is just in case buildbucket API changes OR there is a bug.
702 pop(title='Other:',
703 f=lambda b: (get_name(b), 'id=%s' % b['id']))
704 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700705 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000706
707
qyearsley53f48a12016-09-01 10:45:13 -0700708def write_try_results_json(output_file, builds):
709 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
710
711 The input |builds| dict is assumed to be generated by Buildbucket.
712 Buildbucket documentation: http://goo.gl/G0s101
713 """
714
715 def convert_build_dict(build):
716 return {
717 'buildbucket_id': build.get('id'),
718 'status': build.get('status'),
719 'result': build.get('result'),
720 'bucket': build.get('bucket'),
721 'builder_name': json.loads(
722 build.get('parameters_json', '{}')).get('builder_name'),
723 'failure_reason': build.get('failure_reason'),
724 'url': build.get('url'),
725 }
726
727 converted = []
728 for _, build in sorted(builds.items()):
729 converted.append(convert_build_dict(build))
730 write_json(output_file, converted)
731
732
iannucci@chromium.org79540052012-10-19 23:15:26 +0000733def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000734 """Prints statistics about the change to the user."""
735 # --no-ext-diff is broken in some versions of Git, so try to work around
736 # this by overriding the environment (but there is still a problem if the
737 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000738 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000739 if 'GIT_EXTERNAL_DIFF' in env:
740 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000741
742 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800743 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000744 else:
745 similarity_options = ['-M%s' % similarity]
746
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000747 try:
748 stdout = sys.stdout.fileno()
749 except AttributeError:
750 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000751 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000752 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000753 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000754 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000755
756
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000757class BuildbucketResponseException(Exception):
758 pass
759
760
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000761class Settings(object):
762 def __init__(self):
763 self.default_server = None
764 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000765 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000766 self.tree_status_url = None
767 self.viewvc_url = None
768 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000769 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000770 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000771 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000772 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000773 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000774 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000775
776 def LazyUpdateIfNeeded(self):
777 """Updates the settings from a codereview.settings file, if available."""
778 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000779 # The only value that actually changes the behavior is
780 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000781 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000782 error_ok=True
783 ).strip().lower()
784
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000785 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000786 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000787 LoadCodereviewSettingsFromFile(cr_settings_file)
788 self.updated = True
789
790 def GetDefaultServerUrl(self, error_ok=False):
791 if not self.default_server:
792 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000793 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000794 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795 if error_ok:
796 return self.default_server
797 if not self.default_server:
798 error_message = ('Could not find settings file. You must configure '
799 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000800 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000801 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000802 return self.default_server
803
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000804 @staticmethod
805 def GetRelativeRoot():
806 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000807
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000808 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000809 if self.root is None:
810 self.root = os.path.abspath(self.GetRelativeRoot())
811 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000812
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000813 def GetGitMirror(self, remote='origin'):
814 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000815 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000816 if not os.path.isdir(local_url):
817 return None
818 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
819 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100820 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100821 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000822 if mirror.exists():
823 return mirror
824 return None
825
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826 def GetTreeStatusUrl(self, error_ok=False):
827 if not self.tree_status_url:
828 error_message = ('You must configure your tree status URL by running '
829 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000830 self.tree_status_url = self._GetRietveldConfig(
831 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000832 return self.tree_status_url
833
834 def GetViewVCUrl(self):
835 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000836 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000837 return self.viewvc_url
838
Mark Mentovai57c47212017-03-09 11:14:09 -0500839 def GetBugLineFormat(self):
840 # rietveld.bug-line-format should have a %s where the list of bugs should
841 # go. This is a bit of a quirk, because normal people will always want the
842 # bug list to go right after a prefix like BUG= or Bug:. The %s format
843 # approach is used strictly because there isn't a great way to carry the
844 # desired space after Bug: all the way from codereview.settings to here
845 # without treating : specially or inventing a quoting scheme.
846 bug_line_format = self._GetRietveldConfig('bug-line-format', error_ok=True)
847 if not bug_line_format:
848 # TODO(tandrii): change this to 'Bug: %s' to be a proper Gerrit footer.
849 bug_line_format = 'BUG=%s'
850 return bug_line_format
851
rmistry@google.com90752582014-01-14 21:04:50 +0000852 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000853 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000854
rmistry@google.com78948ed2015-07-08 23:09:57 +0000855 def GetIsSkipDependencyUpload(self, branch_name):
856 """Returns true if specified branch should skip dep uploads."""
857 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
858 error_ok=True)
859
rmistry@google.com5626a922015-02-26 14:03:30 +0000860 def GetRunPostUploadHook(self):
861 run_post_upload_hook = self._GetRietveldConfig(
862 'run-post-upload-hook', error_ok=True)
863 return run_post_upload_hook == "True"
864
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000865 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000866 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000867
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000868 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000869 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000870
ukai@chromium.orge8077812012-02-03 03:41:46 +0000871 def GetIsGerrit(self):
872 """Return true if this repo is assosiated with gerrit code review system."""
873 if self.is_gerrit is None:
874 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
875 return self.is_gerrit
876
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000877 def GetSquashGerritUploads(self):
878 """Return true if uploads to Gerrit should be squashed by default."""
879 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700880 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
881 if self.squash_gerrit_uploads is None:
882 # Default is squash now (http://crbug.com/611892#c23).
883 self.squash_gerrit_uploads = not (
884 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
885 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000886 return self.squash_gerrit_uploads
887
tandriia60502f2016-06-20 02:01:53 -0700888 def GetSquashGerritUploadsOverride(self):
889 """Return True or False if codereview.settings should be overridden.
890
891 Returns None if no override has been defined.
892 """
893 # See also http://crbug.com/611892#c23
894 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
895 error_ok=True).strip()
896 if result == 'true':
897 return True
898 if result == 'false':
899 return False
900 return None
901
tandrii@chromium.org28253532016-04-14 13:46:56 +0000902 def GetGerritSkipEnsureAuthenticated(self):
903 """Return True if EnsureAuthenticated should not be done for Gerrit
904 uploads."""
905 if self.gerrit_skip_ensure_authenticated is None:
906 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000907 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000908 error_ok=True).strip() == 'true')
909 return self.gerrit_skip_ensure_authenticated
910
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000911 def GetGitEditor(self):
912 """Return the editor specified in the git config, or None if none is."""
913 if self.git_editor is None:
914 self.git_editor = self._GetConfig('core.editor', error_ok=True)
915 return self.git_editor or None
916
thestig@chromium.org44202a22014-03-11 19:22:18 +0000917 def GetLintRegex(self):
918 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
919 DEFAULT_LINT_REGEX)
920
921 def GetLintIgnoreRegex(self):
922 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
923 DEFAULT_LINT_IGNORE_REGEX)
924
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000925 def GetProject(self):
926 if not self.project:
927 self.project = self._GetRietveldConfig('project', error_ok=True)
928 return self.project
929
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000930 def _GetRietveldConfig(self, param, **kwargs):
931 return self._GetConfig('rietveld.' + param, **kwargs)
932
rmistry@google.com78948ed2015-07-08 23:09:57 +0000933 def _GetBranchConfig(self, branch_name, param, **kwargs):
934 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
935
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000936 def _GetConfig(self, param, **kwargs):
937 self.LazyUpdateIfNeeded()
938 return RunGit(['config', param], **kwargs).strip()
939
940
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100941@contextlib.contextmanager
942def _get_gerrit_project_config_file(remote_url):
943 """Context manager to fetch and store Gerrit's project.config from
944 refs/meta/config branch and store it in temp file.
945
946 Provides a temporary filename or None if there was error.
947 """
948 error, _ = RunGitWithCode([
949 'fetch', remote_url,
950 '+refs/meta/config:refs/git_cl/meta/config'])
951 if error:
952 # Ref doesn't exist or isn't accessible to current user.
953 print('WARNING: failed to fetch project config for %s: %s' %
954 (remote_url, error))
955 yield None
956 return
957
958 error, project_config_data = RunGitWithCode(
959 ['show', 'refs/git_cl/meta/config:project.config'])
960 if error:
961 print('WARNING: project.config file not found')
962 yield None
963 return
964
965 with gclient_utils.temporary_directory() as tempdir:
966 project_config_file = os.path.join(tempdir, 'project.config')
967 gclient_utils.FileWrite(project_config_file, project_config_data)
968 yield project_config_file
969
970
971def _is_git_numberer_enabled(remote_url, remote_ref):
972 """Returns True if Git Numberer is enabled on this ref."""
973 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100974 KNOWN_PROJECTS_WHITELIST = [
975 'chromium/src',
976 'external/webrtc',
977 'v8/v8',
978 ]
979
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100980 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
981 url_parts = urlparse.urlparse(remote_url)
982 project_name = url_parts.path.lstrip('/').rstrip('git./')
983 for known in KNOWN_PROJECTS_WHITELIST:
984 if project_name.endswith(known):
985 break
986 else:
987 # Early exit to avoid extra fetches for repos that aren't using Git
988 # Numberer.
989 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100990
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100991 with _get_gerrit_project_config_file(remote_url) as project_config_file:
992 if project_config_file is None:
993 # Failed to fetch project.config, which shouldn't happen on open source
994 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100995 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100996 def get_opts(x):
997 code, out = RunGitWithCode(
998 ['config', '-f', project_config_file, '--get-all',
999 'plugin.git-numberer.validate-%s-refglob' % x])
1000 if code == 0:
1001 return out.strip().splitlines()
1002 return []
1003 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001004
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001005 logging.info('validator config enabled %s disabled %s refglobs for '
1006 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +00001007
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001008 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001009 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001010 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001011 return True
1012 return False
1013
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001014 if match_refglobs(disabled):
1015 return False
1016 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001017
1018
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001019def ShortBranchName(branch):
1020 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001021 return branch.replace('refs/heads/', '', 1)
1022
1023
1024def GetCurrentBranchRef():
1025 """Returns branch ref (e.g., refs/heads/master) or None."""
1026 return RunGit(['symbolic-ref', 'HEAD'],
1027 stderr=subprocess2.VOID, error_ok=True).strip() or None
1028
1029
1030def GetCurrentBranch():
1031 """Returns current branch or None.
1032
1033 For refs/heads/* branches, returns just last part. For others, full ref.
1034 """
1035 branchref = GetCurrentBranchRef()
1036 if branchref:
1037 return ShortBranchName(branchref)
1038 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001039
1040
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001041class _CQState(object):
1042 """Enum for states of CL with respect to Commit Queue."""
1043 NONE = 'none'
1044 DRY_RUN = 'dry_run'
1045 COMMIT = 'commit'
1046
1047 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1048
1049
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001050class _ParsedIssueNumberArgument(object):
1051 def __init__(self, issue=None, patchset=None, hostname=None):
1052 self.issue = issue
1053 self.patchset = patchset
1054 self.hostname = hostname
1055
1056 @property
1057 def valid(self):
1058 return self.issue is not None
1059
1060
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001061def ParseIssueNumberArgument(arg):
1062 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1063 fail_result = _ParsedIssueNumberArgument()
1064
1065 if arg.isdigit():
1066 return _ParsedIssueNumberArgument(issue=int(arg))
1067 if not arg.startswith('http'):
1068 return fail_result
1069 url = gclient_utils.UpgradeToHttps(arg)
1070 try:
1071 parsed_url = urlparse.urlparse(url)
1072 except ValueError:
1073 return fail_result
1074 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1075 tmp = cls.ParseIssueURL(parsed_url)
1076 if tmp is not None:
1077 return tmp
1078 return fail_result
1079
1080
Aaron Gablea45ee112016-11-22 15:14:38 -08001081class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001082 def __init__(self, issue, url):
1083 self.issue = issue
1084 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001085 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001086
1087 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001088 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001089 self.issue, self.url)
1090
1091
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001093 """Changelist works with one changelist in local branch.
1094
1095 Supports two codereview backends: Rietveld or Gerrit, selected at object
1096 creation.
1097
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001098 Notes:
1099 * Not safe for concurrent multi-{thread,process} use.
1100 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001101 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001102 """
1103
1104 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1105 """Create a new ChangeList instance.
1106
1107 If issue is given, the codereview must be given too.
1108
1109 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1110 Otherwise, it's decided based on current configuration of the local branch,
1111 with default being 'rietveld' for backwards compatibility.
1112 See _load_codereview_impl for more details.
1113
1114 **kwargs will be passed directly to codereview implementation.
1115 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001116 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001117 global settings
1118 if not settings:
1119 # Happens when git_cl.py is used as a utility library.
1120 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001121
1122 if issue:
1123 assert codereview, 'codereview must be known, if issue is known'
1124
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001125 self.branchref = branchref
1126 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001127 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128 self.branch = ShortBranchName(self.branchref)
1129 else:
1130 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001131 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001132 self.lookedup_issue = False
1133 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134 self.has_description = False
1135 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001136 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001138 self.cc = None
1139 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001140 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001141
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001142 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001143 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001144 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001145 assert self._codereview_impl
1146 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001147
1148 def _load_codereview_impl(self, codereview=None, **kwargs):
1149 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001150 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1151 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1152 self._codereview = codereview
1153 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001154 return
1155
1156 # Automatic selection based on issue number set for a current branch.
1157 # Rietveld takes precedence over Gerrit.
1158 assert not self.issue
1159 # Whether we find issue or not, we are doing the lookup.
1160 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001161 if self.GetBranch():
1162 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1163 issue = _git_get_branch_config_value(
1164 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1165 if issue:
1166 self._codereview = codereview
1167 self._codereview_impl = cls(self, **kwargs)
1168 self.issue = int(issue)
1169 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001170
1171 # No issue is set for this branch, so decide based on repo-wide settings.
1172 return self._load_codereview_impl(
1173 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1174 **kwargs)
1175
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001176 def IsGerrit(self):
1177 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001178
1179 def GetCCList(self):
1180 """Return the users cc'd on this CL.
1181
agable92bec4f2016-08-24 09:27:27 -07001182 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001183 """
1184 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001185 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001186 more_cc = ','.join(self.watchers)
1187 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1188 return self.cc
1189
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001190 def GetCCListWithoutDefault(self):
1191 """Return the users cc'd on this CL excluding default ones."""
1192 if self.cc is None:
1193 self.cc = ','.join(self.watchers)
1194 return self.cc
1195
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001196 def SetWatchers(self, watchers):
1197 """Set the list of email addresses that should be cc'd based on the changed
1198 files in this CL.
1199 """
1200 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201
1202 def GetBranch(self):
1203 """Returns the short branch name, e.g. 'master'."""
1204 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001205 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001206 if not branchref:
1207 return None
1208 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001209 self.branch = ShortBranchName(self.branchref)
1210 return self.branch
1211
1212 def GetBranchRef(self):
1213 """Returns the full branch name, e.g. 'refs/heads/master'."""
1214 self.GetBranch() # Poke the lazy loader.
1215 return self.branchref
1216
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001217 def ClearBranch(self):
1218 """Clears cached branch data of this object."""
1219 self.branch = self.branchref = None
1220
tandrii5d48c322016-08-18 16:19:37 -07001221 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1222 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1223 kwargs['branch'] = self.GetBranch()
1224 return _git_get_branch_config_value(key, default, **kwargs)
1225
1226 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1227 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1228 assert self.GetBranch(), (
1229 'this CL must have an associated branch to %sset %s%s' %
1230 ('un' if value is None else '',
1231 key,
1232 '' if value is None else ' to %r' % value))
1233 kwargs['branch'] = self.GetBranch()
1234 return _git_set_branch_config_value(key, value, **kwargs)
1235
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001236 @staticmethod
1237 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001238 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001239 e.g. 'origin', 'refs/heads/master'
1240 """
1241 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001242 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1243
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001244 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001245 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001247 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1248 error_ok=True).strip()
1249 if upstream_branch:
1250 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001252 # Else, try to guess the origin remote.
1253 remote_branches = RunGit(['branch', '-r']).split()
1254 if 'origin/master' in remote_branches:
1255 # Fall back on origin/master if it exits.
1256 remote = 'origin'
1257 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001258 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001259 DieWithError(
1260 'Unable to determine default branch to diff against.\n'
1261 'Either pass complete "git diff"-style arguments, like\n'
1262 ' git cl upload origin/master\n'
1263 'or verify this branch is set up to track another \n'
1264 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001265
1266 return remote, upstream_branch
1267
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001268 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001269 upstream_branch = self.GetUpstreamBranch()
1270 if not BranchExists(upstream_branch):
1271 DieWithError('The upstream for the current branch (%s) does not exist '
1272 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001273 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001274 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001275
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001276 def GetUpstreamBranch(self):
1277 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001278 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001280 upstream_branch = upstream_branch.replace('refs/heads/',
1281 'refs/remotes/%s/' % remote)
1282 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1283 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001284 self.upstream_branch = upstream_branch
1285 return self.upstream_branch
1286
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001287 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001288 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001289 remote, branch = None, self.GetBranch()
1290 seen_branches = set()
1291 while branch not in seen_branches:
1292 seen_branches.add(branch)
1293 remote, branch = self.FetchUpstreamTuple(branch)
1294 branch = ShortBranchName(branch)
1295 if remote != '.' or branch.startswith('refs/remotes'):
1296 break
1297 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001298 remotes = RunGit(['remote'], error_ok=True).split()
1299 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001300 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001301 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001302 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001303 logging.warn('Could not determine which remote this change is '
1304 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001305 else:
1306 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001307 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001308 branch = 'HEAD'
1309 if branch.startswith('refs/remotes'):
1310 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001311 elif branch.startswith('refs/branch-heads/'):
1312 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001313 else:
1314 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001315 return self._remote
1316
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001317 def GitSanityChecks(self, upstream_git_obj):
1318 """Checks git repo status and ensures diff is from local commits."""
1319
sbc@chromium.org79706062015-01-14 21:18:12 +00001320 if upstream_git_obj is None:
1321 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001322 print('ERROR: unable to determine current branch (detached HEAD?)',
1323 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001324 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001325 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001326 return False
1327
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001328 # Verify the commit we're diffing against is in our current branch.
1329 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1330 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1331 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001332 print('ERROR: %s is not in the current branch. You may need to rebase '
1333 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001334 return False
1335
1336 # List the commits inside the diff, and verify they are all local.
1337 commits_in_diff = RunGit(
1338 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1339 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1340 remote_branch = remote_branch.strip()
1341 if code != 0:
1342 _, remote_branch = self.GetRemoteBranch()
1343
1344 commits_in_remote = RunGit(
1345 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1346
1347 common_commits = set(commits_in_diff) & set(commits_in_remote)
1348 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001349 print('ERROR: Your diff contains %d commits already in %s.\n'
1350 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1351 'the diff. If you are using a custom git flow, you can override'
1352 ' the reference used for this check with "git config '
1353 'gitcl.remotebranch <git-ref>".' % (
1354 len(common_commits), remote_branch, upstream_git_obj),
1355 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001356 return False
1357 return True
1358
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001359 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001360 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001361
1362 Returns None if it is not set.
1363 """
tandrii5d48c322016-08-18 16:19:37 -07001364 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001365
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366 def GetRemoteUrl(self):
1367 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1368
1369 Returns None if there is no remote.
1370 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001371 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001372 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1373
1374 # If URL is pointing to a local directory, it is probably a git cache.
1375 if os.path.isdir(url):
1376 url = RunGit(['config', 'remote.%s.url' % remote],
1377 error_ok=True,
1378 cwd=url).strip()
1379 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001380
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001381 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001382 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001383 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001384 self.issue = self._GitGetBranchConfigValue(
1385 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001386 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001387 return self.issue
1388
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001389 def GetIssueURL(self):
1390 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001391 issue = self.GetIssue()
1392 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001393 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001394 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001395
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001396 def GetDescription(self, pretty=False, force=False):
1397 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001398 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001399 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001400 self.has_description = True
1401 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001402 # Set width to 72 columns + 2 space indent.
1403 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001404 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001405 lines = self.description.splitlines()
1406 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407 return self.description
1408
1409 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001410 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001411 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001412 self.patchset = self._GitGetBranchConfigValue(
1413 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001414 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415 return self.patchset
1416
1417 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001418 """Set this branch's patchset. If patchset=0, clears the patchset."""
1419 assert self.GetBranch()
1420 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001421 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001422 else:
1423 self.patchset = int(patchset)
1424 self._GitSetBranchConfigValue(
1425 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001426
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001427 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001428 """Set this branch's issue. If issue isn't given, clears the issue."""
1429 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001430 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001431 issue = int(issue)
1432 self._GitSetBranchConfigValue(
1433 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001434 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001435 codereview_server = self._codereview_impl.GetCodereviewServer()
1436 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001437 self._GitSetBranchConfigValue(
1438 self._codereview_impl.CodereviewServerConfigKey(),
1439 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440 else:
tandrii5d48c322016-08-18 16:19:37 -07001441 # Reset all of these just to be clean.
1442 reset_suffixes = [
1443 'last-upload-hash',
1444 self._codereview_impl.IssueConfigKey(),
1445 self._codereview_impl.PatchsetConfigKey(),
1446 self._codereview_impl.CodereviewServerConfigKey(),
1447 ] + self._PostUnsetIssueProperties()
1448 for prop in reset_suffixes:
1449 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001450 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001451 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001452
dnjba1b0f32016-09-02 12:37:42 -07001453 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001454 if not self.GitSanityChecks(upstream_branch):
1455 DieWithError('\nGit sanity check failure')
1456
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001457 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001458 if not root:
1459 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001460 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001461
1462 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001463 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001464 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001465 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001466 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001467 except subprocess2.CalledProcessError:
1468 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001469 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001470 'This branch probably doesn\'t exist anymore. To reset the\n'
1471 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001472 ' git branch --set-upstream-to origin/master %s\n'
1473 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001474 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001475
maruel@chromium.org52424302012-08-29 15:14:30 +00001476 issue = self.GetIssue()
1477 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001478 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001479 description = self.GetDescription()
1480 else:
1481 # If the change was never uploaded, use the log messages of all commits
1482 # up to the branch point, as git cl upload will prefill the description
1483 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001484 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1485 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001486
1487 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001488 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001489 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001490 name,
1491 description,
1492 absroot,
1493 files,
1494 issue,
1495 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001496 author,
1497 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001498
dsansomee2d6fd92016-09-08 00:10:47 -07001499 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001500 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001501 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001502 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001503
1504 def RunHook(self, committing, may_prompt, verbose, change):
1505 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1506 try:
1507 return presubmit_support.DoPresubmitChecks(change, committing,
1508 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1509 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001510 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1511 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001512 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001513 DieWithError(
1514 ('%s\nMaybe your depot_tools is out of date?\n'
1515 'If all fails, contact maruel@') % e)
1516
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001517 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1518 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001519 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1520 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001521 else:
1522 # Assume url.
1523 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1524 urlparse.urlparse(issue_arg))
1525 if not parsed_issue_arg or not parsed_issue_arg.valid:
1526 DieWithError('Failed to parse issue argument "%s". '
1527 'Must be an issue number or a valid URL.' % issue_arg)
1528 return self._codereview_impl.CMDPatchWithParsedIssue(
1529 parsed_issue_arg, reject, nocommit, directory)
1530
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001531 def CMDUpload(self, options, git_diff_args, orig_args):
1532 """Uploads a change to codereview."""
1533 if git_diff_args:
1534 # TODO(ukai): is it ok for gerrit case?
1535 base_branch = git_diff_args[0]
1536 else:
1537 if self.GetBranch() is None:
1538 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1539
1540 # Default to diffing against common ancestor of upstream branch
1541 base_branch = self.GetCommonAncestorWithUpstream()
1542 git_diff_args = [base_branch, 'HEAD']
1543
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001544 # Fast best-effort checks to abort before running potentially
1545 # expensive hooks if uploading is likely to fail anyway. Passing these
1546 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001547 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001548 self._codereview_impl.EnsureCanUploadPatchset()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001549
1550 # Apply watchlists on upload.
1551 change = self.GetChange(base_branch, None)
1552 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1553 files = [f.LocalPath() for f in change.AffectedFiles()]
1554 if not options.bypass_watchlists:
1555 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1556
1557 if not options.bypass_hooks:
1558 if options.reviewers or options.tbr_owners:
1559 # Set the reviewer list now so that presubmit checks can access it.
1560 change_description = ChangeDescription(change.FullDescriptionText())
1561 change_description.update_reviewers(options.reviewers,
1562 options.tbr_owners,
1563 change)
1564 change.SetDescriptionText(change_description.description)
1565 hook_results = self.RunHook(committing=False,
1566 may_prompt=not options.force,
1567 verbose=options.verbose,
1568 change=change)
1569 if not hook_results.should_continue():
1570 return 1
1571 if not options.reviewers and hook_results.reviewers:
1572 options.reviewers = hook_results.reviewers.split(',')
1573
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001574 # TODO(tandrii): Checking local patchset against remote patchset is only
1575 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1576 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001577 latest_patchset = self.GetMostRecentPatchset()
1578 local_patchset = self.GetPatchset()
1579 if (latest_patchset and local_patchset and
1580 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001581 print('The last upload made from this repository was patchset #%d but '
1582 'the most recent patchset on the server is #%d.'
1583 % (local_patchset, latest_patchset))
1584 print('Uploading will still work, but if you\'ve uploaded to this '
1585 'issue from another machine or branch the patch you\'re '
1586 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001587 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001588
1589 print_stats(options.similarity, options.find_copies, git_diff_args)
1590 ret = self.CMDUploadChange(options, git_diff_args, change)
1591 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001592 if options.use_commit_queue:
1593 self.SetCQState(_CQState.COMMIT)
1594 elif options.cq_dry_run:
1595 self.SetCQState(_CQState.DRY_RUN)
1596
tandrii5d48c322016-08-18 16:19:37 -07001597 _git_set_branch_config_value('last-upload-hash',
1598 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001599 # Run post upload hooks, if specified.
1600 if settings.GetRunPostUploadHook():
1601 presubmit_support.DoPostUploadExecuter(
1602 change,
1603 self,
1604 settings.GetRoot(),
1605 options.verbose,
1606 sys.stdout)
1607
1608 # Upload all dependencies if specified.
1609 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001610 print()
1611 print('--dependencies has been specified.')
1612 print('All dependent local branches will be re-uploaded.')
1613 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001614 # Remove the dependencies flag from args so that we do not end up in a
1615 # loop.
1616 orig_args.remove('--dependencies')
1617 ret = upload_branch_deps(self, orig_args)
1618 return ret
1619
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001620 def SetCQState(self, new_state):
1621 """Update the CQ state for latest patchset.
1622
1623 Issue must have been already uploaded and known.
1624 """
1625 assert new_state in _CQState.ALL_STATES
1626 assert self.GetIssue()
1627 return self._codereview_impl.SetCQState(new_state)
1628
qyearsley1fdfcb62016-10-24 13:22:03 -07001629 def TriggerDryRun(self):
1630 """Triggers a dry run and prints a warning on failure."""
1631 # TODO(qyearsley): Either re-use this method in CMDset_commit
1632 # and CMDupload, or change CMDtry to trigger dry runs with
1633 # just SetCQState, and catch keyboard interrupt and other
1634 # errors in that method.
1635 try:
1636 self.SetCQState(_CQState.DRY_RUN)
1637 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1638 return 0
1639 except KeyboardInterrupt:
1640 raise
1641 except:
1642 print('WARNING: failed to trigger CQ Dry Run.\n'
1643 'Either:\n'
1644 ' * your project has no CQ\n'
1645 ' * you don\'t have permission to trigger Dry Run\n'
1646 ' * bug in this code (see stack trace below).\n'
1647 'Consider specifying which bots to trigger manually '
1648 'or asking your project owners for permissions '
1649 'or contacting Chrome Infrastructure team at '
1650 'https://www.chromium.org/infra\n\n')
1651 # Still raise exception so that stack trace is printed.
1652 raise
1653
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001654 # Forward methods to codereview specific implementation.
1655
1656 def CloseIssue(self):
1657 return self._codereview_impl.CloseIssue()
1658
1659 def GetStatus(self):
1660 return self._codereview_impl.GetStatus()
1661
1662 def GetCodereviewServer(self):
1663 return self._codereview_impl.GetCodereviewServer()
1664
tandriide281ae2016-10-12 06:02:30 -07001665 def GetIssueOwner(self):
1666 """Get owner from codereview, which may differ from this checkout."""
1667 return self._codereview_impl.GetIssueOwner()
1668
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001669 def GetApprovingReviewers(self):
1670 return self._codereview_impl.GetApprovingReviewers()
1671
1672 def GetMostRecentPatchset(self):
1673 return self._codereview_impl.GetMostRecentPatchset()
1674
tandriide281ae2016-10-12 06:02:30 -07001675 def CannotTriggerTryJobReason(self):
1676 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1677 return self._codereview_impl.CannotTriggerTryJobReason()
1678
tandrii8c5a3532016-11-04 07:52:02 -07001679 def GetTryjobProperties(self, patchset=None):
1680 """Returns dictionary of properties to launch tryjob."""
1681 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1682
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001683 def __getattr__(self, attr):
1684 # This is because lots of untested code accesses Rietveld-specific stuff
1685 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001686 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001687 # Note that child method defines __getattr__ as well, and forwards it here,
1688 # because _RietveldChangelistImpl is not cleaned up yet, and given
1689 # deprecation of Rietveld, it should probably be just removed.
1690 # Until that time, avoid infinite recursion by bypassing __getattr__
1691 # of implementation class.
1692 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001693
1694
1695class _ChangelistCodereviewBase(object):
1696 """Abstract base class encapsulating codereview specifics of a changelist."""
1697 def __init__(self, changelist):
1698 self._changelist = changelist # instance of Changelist
1699
1700 def __getattr__(self, attr):
1701 # Forward methods to changelist.
1702 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1703 # _RietveldChangelistImpl to avoid this hack?
1704 return getattr(self._changelist, attr)
1705
1706 def GetStatus(self):
1707 """Apply a rough heuristic to give a simple summary of an issue's review
1708 or CQ status, assuming adherence to a common workflow.
1709
1710 Returns None if no issue for this branch, or specific string keywords.
1711 """
1712 raise NotImplementedError()
1713
1714 def GetCodereviewServer(self):
1715 """Returns server URL without end slash, like "https://codereview.com"."""
1716 raise NotImplementedError()
1717
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001718 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001719 """Fetches and returns description from the codereview server."""
1720 raise NotImplementedError()
1721
tandrii5d48c322016-08-18 16:19:37 -07001722 @classmethod
1723 def IssueConfigKey(cls):
1724 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001725 raise NotImplementedError()
1726
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001727 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001728 def PatchsetConfigKey(cls):
1729 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001730 raise NotImplementedError()
1731
tandrii5d48c322016-08-18 16:19:37 -07001732 @classmethod
1733 def CodereviewServerConfigKey(cls):
1734 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001735 raise NotImplementedError()
1736
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001737 def _PostUnsetIssueProperties(self):
1738 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001739 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001740
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001741 def GetRieveldObjForPresubmit(self):
1742 # This is an unfortunate Rietveld-embeddedness in presubmit.
1743 # For non-Rietveld codereviews, this probably should return a dummy object.
1744 raise NotImplementedError()
1745
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001746 def GetGerritObjForPresubmit(self):
1747 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1748 return None
1749
dsansomee2d6fd92016-09-08 00:10:47 -07001750 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001751 """Update the description on codereview site."""
1752 raise NotImplementedError()
1753
1754 def CloseIssue(self):
1755 """Closes the issue."""
1756 raise NotImplementedError()
1757
1758 def GetApprovingReviewers(self):
1759 """Returns a list of reviewers approving the change.
1760
1761 Note: not necessarily committers.
1762 """
1763 raise NotImplementedError()
1764
1765 def GetMostRecentPatchset(self):
1766 """Returns the most recent patchset number from the codereview site."""
1767 raise NotImplementedError()
1768
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001769 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1770 directory):
1771 """Fetches and applies the issue.
1772
1773 Arguments:
1774 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1775 reject: if True, reject the failed patch instead of switching to 3-way
1776 merge. Rietveld only.
1777 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1778 only.
1779 directory: switch to directory before applying the patch. Rietveld only.
1780 """
1781 raise NotImplementedError()
1782
1783 @staticmethod
1784 def ParseIssueURL(parsed_url):
1785 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1786 failed."""
1787 raise NotImplementedError()
1788
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001789 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001790 """Best effort check that user is authenticated with codereview server.
1791
1792 Arguments:
1793 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001794 refresh: whether to attempt to refresh credentials. Ignored if not
1795 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001796 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001797 raise NotImplementedError()
1798
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001799 def EnsureCanUploadPatchset(self):
1800 """Best effort check that uploading isn't supposed to fail for predictable
1801 reasons.
1802
1803 This method should raise informative exception if uploading shouldn't
1804 proceed.
1805 """
1806 pass
1807
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001808 def CMDUploadChange(self, options, args, change):
1809 """Uploads a change to codereview."""
1810 raise NotImplementedError()
1811
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001812 def SetCQState(self, new_state):
1813 """Update the CQ state for latest patchset.
1814
1815 Issue must have been already uploaded and known.
1816 """
1817 raise NotImplementedError()
1818
tandriie113dfd2016-10-11 10:20:12 -07001819 def CannotTriggerTryJobReason(self):
1820 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1821 raise NotImplementedError()
1822
tandriide281ae2016-10-12 06:02:30 -07001823 def GetIssueOwner(self):
1824 raise NotImplementedError()
1825
tandrii8c5a3532016-11-04 07:52:02 -07001826 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001827 raise NotImplementedError()
1828
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001829
1830class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001831 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001832 super(_RietveldChangelistImpl, self).__init__(changelist)
1833 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001834 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001835 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001836
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001837 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001838 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001839 self._props = None
1840 self._rpc_server = None
1841
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001842 def GetCodereviewServer(self):
1843 if not self._rietveld_server:
1844 # If we're on a branch then get the server potentially associated
1845 # with that branch.
1846 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001847 self._rietveld_server = gclient_utils.UpgradeToHttps(
1848 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001849 if not self._rietveld_server:
1850 self._rietveld_server = settings.GetDefaultServerUrl()
1851 return self._rietveld_server
1852
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001853 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001854 """Best effort check that user is authenticated with Rietveld server."""
1855 if self._auth_config.use_oauth2:
1856 authenticator = auth.get_authenticator_for_host(
1857 self.GetCodereviewServer(), self._auth_config)
1858 if not authenticator.has_cached_credentials():
1859 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001860 if refresh:
1861 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001862
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001863 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001864 issue = self.GetIssue()
1865 assert issue
1866 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001867 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001868 except urllib2.HTTPError as e:
1869 if e.code == 404:
1870 DieWithError(
1871 ('\nWhile fetching the description for issue %d, received a '
1872 '404 (not found)\n'
1873 'error. It is likely that you deleted this '
1874 'issue on the server. If this is the\n'
1875 'case, please run\n\n'
1876 ' git cl issue 0\n\n'
1877 'to clear the association with the deleted issue. Then run '
1878 'this command again.') % issue)
1879 else:
1880 DieWithError(
1881 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1882 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001883 print('Warning: Failed to retrieve CL description due to network '
1884 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001885 return ''
1886
1887 def GetMostRecentPatchset(self):
1888 return self.GetIssueProperties()['patchsets'][-1]
1889
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001890 def GetIssueProperties(self):
1891 if self._props is None:
1892 issue = self.GetIssue()
1893 if not issue:
1894 self._props = {}
1895 else:
1896 self._props = self.RpcServer().get_issue_properties(issue, True)
1897 return self._props
1898
tandriie113dfd2016-10-11 10:20:12 -07001899 def CannotTriggerTryJobReason(self):
1900 props = self.GetIssueProperties()
1901 if not props:
1902 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1903 if props.get('closed'):
1904 return 'CL %s is closed' % self.GetIssue()
1905 if props.get('private'):
1906 return 'CL %s is private' % self.GetIssue()
1907 return None
1908
tandrii8c5a3532016-11-04 07:52:02 -07001909 def GetTryjobProperties(self, patchset=None):
1910 """Returns dictionary of properties to launch tryjob."""
1911 project = (self.GetIssueProperties() or {}).get('project')
1912 return {
1913 'issue': self.GetIssue(),
1914 'patch_project': project,
1915 'patch_storage': 'rietveld',
1916 'patchset': patchset or self.GetPatchset(),
1917 'rietveld': self.GetCodereviewServer(),
1918 }
1919
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001920 def GetApprovingReviewers(self):
1921 return get_approving_reviewers(self.GetIssueProperties())
1922
tandriide281ae2016-10-12 06:02:30 -07001923 def GetIssueOwner(self):
1924 return (self.GetIssueProperties() or {}).get('owner_email')
1925
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001926 def AddComment(self, message):
1927 return self.RpcServer().add_comment(self.GetIssue(), message)
1928
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001929 def GetStatus(self):
1930 """Apply a rough heuristic to give a simple summary of an issue's review
1931 or CQ status, assuming adherence to a common workflow.
1932
1933 Returns None if no issue for this branch, or one of the following keywords:
1934 * 'error' - error from review tool (including deleted issues)
1935 * 'unsent' - not sent for review
1936 * 'waiting' - waiting for review
1937 * 'reply' - waiting for owner to reply to review
1938 * 'lgtm' - LGTM from at least one approved reviewer
1939 * 'commit' - in the commit queue
1940 * 'closed' - closed
1941 """
1942 if not self.GetIssue():
1943 return None
1944
1945 try:
1946 props = self.GetIssueProperties()
1947 except urllib2.HTTPError:
1948 return 'error'
1949
1950 if props.get('closed'):
1951 # Issue is closed.
1952 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001953 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001954 # Issue is in the commit queue.
1955 return 'commit'
1956
1957 try:
1958 reviewers = self.GetApprovingReviewers()
1959 except urllib2.HTTPError:
1960 return 'error'
1961
1962 if reviewers:
1963 # Was LGTM'ed.
1964 return 'lgtm'
1965
1966 messages = props.get('messages') or []
1967
tandrii9d2c7a32016-06-22 03:42:45 -07001968 # Skip CQ messages that don't require owner's action.
1969 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1970 if 'Dry run:' in messages[-1]['text']:
1971 messages.pop()
1972 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1973 # This message always follows prior messages from CQ,
1974 # so skip this too.
1975 messages.pop()
1976 else:
1977 # This is probably a CQ messages warranting user attention.
1978 break
1979
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001980 if not messages:
1981 # No message was sent.
1982 return 'unsent'
1983 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001984 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001985 return 'reply'
1986 return 'waiting'
1987
dsansomee2d6fd92016-09-08 00:10:47 -07001988 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001989 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001990
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001991 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00001992 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001993
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001994 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07001995 return self.SetFlags({flag: value})
1996
1997 def SetFlags(self, flags):
1998 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07001999 """
phajdan.jr68598232016-08-10 03:28:28 -07002000 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002001 try:
tandrii4b233bd2016-07-06 03:50:29 -07002002 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002003 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002004 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002005 if e.code == 404:
2006 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2007 if e.code == 403:
2008 DieWithError(
2009 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002010 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002011 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002012
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002013 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002014 """Returns an upload.RpcServer() to access this review's rietveld instance.
2015 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002016 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002017 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002018 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002019 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002020 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002021
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002022 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002023 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002024 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002025
tandrii5d48c322016-08-18 16:19:37 -07002026 @classmethod
2027 def PatchsetConfigKey(cls):
2028 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002029
tandrii5d48c322016-08-18 16:19:37 -07002030 @classmethod
2031 def CodereviewServerConfigKey(cls):
2032 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002033
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002034 def GetRieveldObjForPresubmit(self):
2035 return self.RpcServer()
2036
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002037 def SetCQState(self, new_state):
2038 props = self.GetIssueProperties()
2039 if props.get('private'):
2040 DieWithError('Cannot set-commit on private issue')
2041
2042 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002043 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002044 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002045 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002046 else:
tandrii4b233bd2016-07-06 03:50:29 -07002047 assert new_state == _CQState.DRY_RUN
2048 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002049
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002050 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2051 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002052 # PatchIssue should never be called with a dirty tree. It is up to the
2053 # caller to check this, but just in case we assert here since the
2054 # consequences of the caller not checking this could be dire.
2055 assert(not git_common.is_dirty_git_tree('apply'))
2056 assert(parsed_issue_arg.valid)
2057 self._changelist.issue = parsed_issue_arg.issue
2058 if parsed_issue_arg.hostname:
2059 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2060
skobes6468b902016-10-24 08:45:10 -07002061 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2062 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2063 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002064 try:
skobes6468b902016-10-24 08:45:10 -07002065 scm_obj.apply_patch(patchset_object)
2066 except Exception as e:
2067 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002068 return 1
2069
2070 # If we had an issue, commit the current state and register the issue.
2071 if not nocommit:
2072 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2073 'patch from issue %(i)s at patchset '
2074 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2075 % {'i': self.GetIssue(), 'p': patchset})])
2076 self.SetIssue(self.GetIssue())
2077 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002078 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002079 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002080 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002081 return 0
2082
2083 @staticmethod
2084 def ParseIssueURL(parsed_url):
2085 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2086 return None
wychen3c1c1722016-08-04 11:46:36 -07002087 # Rietveld patch: https://domain/<number>/#ps<patchset>
2088 match = re.match(r'/(\d+)/$', parsed_url.path)
2089 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2090 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002091 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002092 issue=int(match.group(1)),
2093 patchset=int(match2.group(1)),
2094 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002095 # Typical url: https://domain/<issue_number>[/[other]]
2096 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2097 if match:
skobes6468b902016-10-24 08:45:10 -07002098 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002099 issue=int(match.group(1)),
2100 hostname=parsed_url.netloc)
2101 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2102 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2103 if match:
skobes6468b902016-10-24 08:45:10 -07002104 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002105 issue=int(match.group(1)),
2106 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002107 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002108 return None
2109
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002110 def CMDUploadChange(self, options, args, change):
2111 """Upload the patch to Rietveld."""
2112 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2113 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002114 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2115 if options.emulate_svn_auto_props:
2116 upload_args.append('--emulate_svn_auto_props')
2117
2118 change_desc = None
2119
2120 if options.email is not None:
2121 upload_args.extend(['--email', options.email])
2122
2123 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002124 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002125 upload_args.extend(['--title', options.title])
2126 if options.message:
2127 upload_args.extend(['--message', options.message])
2128 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002129 print('This branch is associated with issue %s. '
2130 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002131 else:
nodirca166002016-06-27 10:59:51 -07002132 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002133 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002134 if options.message:
2135 message = options.message
2136 else:
2137 message = CreateDescriptionFromLog(args)
2138 if options.title:
2139 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002140 change_desc = ChangeDescription(message)
2141 if options.reviewers or options.tbr_owners:
2142 change_desc.update_reviewers(options.reviewers,
2143 options.tbr_owners,
2144 change)
2145 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002146 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002147
2148 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002149 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002150 return 1
2151
2152 upload_args.extend(['--message', change_desc.description])
2153 if change_desc.get_reviewers():
2154 upload_args.append('--reviewers=%s' % ','.join(
2155 change_desc.get_reviewers()))
2156 if options.send_mail:
2157 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002158 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002159 upload_args.append('--send_mail')
2160
2161 # We check this before applying rietveld.private assuming that in
2162 # rietveld.cc only addresses which we can send private CLs to are listed
2163 # if rietveld.private is set, and so we should ignore rietveld.cc only
2164 # when --private is specified explicitly on the command line.
2165 if options.private:
2166 logging.warn('rietveld.cc is ignored since private flag is specified. '
2167 'You need to review and add them manually if necessary.')
2168 cc = self.GetCCListWithoutDefault()
2169 else:
2170 cc = self.GetCCList()
2171 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002172 if change_desc.get_cced():
2173 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002174 if cc:
2175 upload_args.extend(['--cc', cc])
2176
2177 if options.private or settings.GetDefaultPrivateFlag() == "True":
2178 upload_args.append('--private')
2179
2180 upload_args.extend(['--git_similarity', str(options.similarity)])
2181 if not options.find_copies:
2182 upload_args.extend(['--git_no_find_copies'])
2183
2184 # Include the upstream repo's URL in the change -- this is useful for
2185 # projects that have their source spread across multiple repos.
2186 remote_url = self.GetGitBaseUrlFromConfig()
2187 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002188 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2189 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2190 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002191 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002192 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002193 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002194 if target_ref:
2195 upload_args.extend(['--target_ref', target_ref])
2196
2197 # Look for dependent patchsets. See crbug.com/480453 for more details.
2198 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2199 upstream_branch = ShortBranchName(upstream_branch)
2200 if remote is '.':
2201 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002202 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002203 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002204 print()
2205 print('Skipping dependency patchset upload because git config '
2206 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2207 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002208 else:
2209 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002210 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002211 auth_config=auth_config)
2212 branch_cl_issue_url = branch_cl.GetIssueURL()
2213 branch_cl_issue = branch_cl.GetIssue()
2214 branch_cl_patchset = branch_cl.GetPatchset()
2215 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2216 upload_args.extend(
2217 ['--depends_on_patchset', '%s:%s' % (
2218 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002219 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002220 '\n'
2221 'The current branch (%s) is tracking a local branch (%s) with '
2222 'an associated CL.\n'
2223 'Adding %s/#ps%s as a dependency patchset.\n'
2224 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2225 branch_cl_patchset))
2226
2227 project = settings.GetProject()
2228 if project:
2229 upload_args.extend(['--project', project])
2230
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002231 try:
2232 upload_args = ['upload'] + upload_args + args
2233 logging.info('upload.RealMain(%s)', upload_args)
2234 issue, patchset = upload.RealMain(upload_args)
2235 issue = int(issue)
2236 patchset = int(patchset)
2237 except KeyboardInterrupt:
2238 sys.exit(1)
2239 except:
2240 # If we got an exception after the user typed a description for their
2241 # change, back up the description before re-raising.
2242 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002243 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002244 raise
2245
2246 if not self.GetIssue():
2247 self.SetIssue(issue)
2248 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002249 return 0
2250
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002251
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002252class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002253 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002254 # auth_config is Rietveld thing, kept here to preserve interface only.
2255 super(_GerritChangelistImpl, self).__init__(changelist)
2256 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002257 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002258 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002259 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002260 # Map from change number (issue) to its detail cache.
2261 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002262
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002263 if codereview_host is not None:
2264 assert not codereview_host.startswith('https://'), codereview_host
2265 self._gerrit_host = codereview_host
2266 self._gerrit_server = 'https://%s' % codereview_host
2267
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002268 def _GetGerritHost(self):
2269 # Lazy load of configs.
2270 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002271 if self._gerrit_host and '.' not in self._gerrit_host:
2272 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2273 # This happens for internal stuff http://crbug.com/614312.
2274 parsed = urlparse.urlparse(self.GetRemoteUrl())
2275 if parsed.scheme == 'sso':
2276 print('WARNING: using non https URLs for remote is likely broken\n'
2277 ' Your current remote is: %s' % self.GetRemoteUrl())
2278 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2279 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002280 return self._gerrit_host
2281
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002282 def _GetGitHost(self):
2283 """Returns git host to be used when uploading change to Gerrit."""
2284 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2285
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002286 def GetCodereviewServer(self):
2287 if not self._gerrit_server:
2288 # If we're on a branch then get the server potentially associated
2289 # with that branch.
2290 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002291 self._gerrit_server = self._GitGetBranchConfigValue(
2292 self.CodereviewServerConfigKey())
2293 if self._gerrit_server:
2294 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002295 if not self._gerrit_server:
2296 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2297 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002298 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002299 parts[0] = parts[0] + '-review'
2300 self._gerrit_host = '.'.join(parts)
2301 self._gerrit_server = 'https://%s' % self._gerrit_host
2302 return self._gerrit_server
2303
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002304 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002305 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002306 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002307
tandrii5d48c322016-08-18 16:19:37 -07002308 @classmethod
2309 def PatchsetConfigKey(cls):
2310 return 'gerritpatchset'
2311
2312 @classmethod
2313 def CodereviewServerConfigKey(cls):
2314 return 'gerritserver'
2315
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002316 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002317 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002318 if settings.GetGerritSkipEnsureAuthenticated():
2319 # For projects with unusual authentication schemes.
2320 # See http://crbug.com/603378.
2321 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002322 # Lazy-loader to identify Gerrit and Git hosts.
2323 if gerrit_util.GceAuthenticator.is_gce():
2324 return
2325 self.GetCodereviewServer()
2326 git_host = self._GetGitHost()
2327 assert self._gerrit_server and self._gerrit_host
2328 cookie_auth = gerrit_util.CookiesAuthenticator()
2329
2330 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2331 git_auth = cookie_auth.get_auth_header(git_host)
2332 if gerrit_auth and git_auth:
2333 if gerrit_auth == git_auth:
2334 return
2335 print((
2336 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2337 ' Check your %s or %s file for credentials of hosts:\n'
2338 ' %s\n'
2339 ' %s\n'
2340 ' %s') %
2341 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2342 git_host, self._gerrit_host,
2343 cookie_auth.get_new_password_message(git_host)))
2344 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002345 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002346 return
2347 else:
2348 missing = (
2349 [] if gerrit_auth else [self._gerrit_host] +
2350 [] if git_auth else [git_host])
2351 DieWithError('Credentials for the following hosts are required:\n'
2352 ' %s\n'
2353 'These are read from %s (or legacy %s)\n'
2354 '%s' % (
2355 '\n '.join(missing),
2356 cookie_auth.get_gitcookies_path(),
2357 cookie_auth.get_netrc_path(),
2358 cookie_auth.get_new_password_message(git_host)))
2359
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002360 def EnsureCanUploadPatchset(self):
2361 """Best effort check that uploading isn't supposed to fail for predictable
2362 reasons.
2363
2364 This method should raise informative exception if uploading shouldn't
2365 proceed.
2366 """
2367 if not self.GetIssue():
2368 return
2369
2370 # Warm change details cache now to avoid RPCs later, reducing latency for
2371 # developers.
2372 self.FetchDescription()
2373
2374 status = self._GetChangeDetail()['status']
2375 if status in ('MERGED', 'ABANDONED'):
2376 DieWithError('Change %s has been %s, new uploads are not allowed' %
2377 (self.GetIssueURL(),
2378 'submitted' if status == 'MERGED' else 'abandoned'))
2379
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002380 def _PostUnsetIssueProperties(self):
2381 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002382 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002383
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002384 def GetRieveldObjForPresubmit(self):
2385 class ThisIsNotRietveldIssue(object):
2386 def __nonzero__(self):
2387 # This is a hack to make presubmit_support think that rietveld is not
2388 # defined, yet still ensure that calls directly result in a decent
2389 # exception message below.
2390 return False
2391
2392 def __getattr__(self, attr):
2393 print(
2394 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2395 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2396 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2397 'or use Rietveld for codereview.\n'
2398 'See also http://crbug.com/579160.' % attr)
2399 raise NotImplementedError()
2400 return ThisIsNotRietveldIssue()
2401
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002402 def GetGerritObjForPresubmit(self):
2403 return presubmit_support.GerritAccessor(self._GetGerritHost())
2404
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002405 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002406 """Apply a rough heuristic to give a simple summary of an issue's review
2407 or CQ status, assuming adherence to a common workflow.
2408
2409 Returns None if no issue for this branch, or one of the following keywords:
2410 * 'error' - error from review tool (including deleted issues)
2411 * 'unsent' - no reviewers added
2412 * 'waiting' - waiting for review
2413 * 'reply' - waiting for owner to reply to review
Quinten Yearsley442fb642016-12-15 15:38:27 -08002414 * 'not lgtm' - Code-Review disapproval from at least one valid reviewer
tandriic2405f52016-10-10 08:13:15 -07002415 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002416 * 'commit' - in the commit queue
2417 * 'closed' - abandoned
2418 """
2419 if not self.GetIssue():
2420 return None
2421
2422 try:
2423 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002424 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002425 return 'error'
2426
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002427 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002428 return 'closed'
2429
2430 cq_label = data['labels'].get('Commit-Queue', {})
2431 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002432 votes = cq_label.get('all', [])
2433 highest_vote = 0
2434 for v in votes:
2435 highest_vote = max(highest_vote, v.get('value', 0))
2436 vote_value = str(highest_vote)
2437 if vote_value != '0':
2438 # Add a '+' if the value is not 0 to match the values in the label.
2439 # The cq_label does not have negatives.
2440 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002441 vote_text = cq_label.get('values', {}).get(vote_value, '')
2442 if vote_text.lower() == 'commit':
2443 return 'commit'
2444
2445 lgtm_label = data['labels'].get('Code-Review', {})
2446 if lgtm_label:
2447 if 'rejected' in lgtm_label:
2448 return 'not lgtm'
2449 if 'approved' in lgtm_label:
2450 return 'lgtm'
2451
2452 if not data.get('reviewers', {}).get('REVIEWER', []):
2453 return 'unsent'
2454
2455 messages = data.get('messages', [])
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002456 owner = data['owner'].get('_account_id')
2457 while messages:
2458 last_message_author = messages.pop().get('author', {})
2459 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2460 # Ignore replies from CQ.
2461 continue
Andrii Shyshkalov7afd1642017-01-29 16:07:15 +01002462 if last_message_author.get('_account_id') != owner:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002463 # Some reply from non-owner.
2464 return 'reply'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002465 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002466
2467 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002468 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002469 return data['revisions'][data['current_revision']]['_number']
2470
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002471 def FetchDescription(self, force=False):
2472 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2473 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002474 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002475 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002476
dsansomee2d6fd92016-09-08 00:10:47 -07002477 def UpdateDescriptionRemote(self, description, force=False):
2478 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2479 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002480 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002481 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002482 'unpublished edit. Either publish the edit in the Gerrit web UI '
2483 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002484
2485 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2486 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002487 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002488 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002489
2490 def CloseIssue(self):
2491 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2492
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002493 def GetApprovingReviewers(self):
2494 """Returns a list of reviewers approving the change.
2495
2496 Note: not necessarily committers.
2497 """
2498 raise NotImplementedError()
2499
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002500 def SubmitIssue(self, wait_for_merge=True):
2501 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2502 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002503
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002504 def _GetChangeDetail(self, options=None, issue=None,
2505 no_cache=False):
2506 """Returns details of the issue by querying Gerrit and caching results.
2507
2508 If fresh data is needed, set no_cache=True which will clear cache and
2509 thus new data will be fetched from Gerrit.
2510 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002511 options = options or []
2512 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002513 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002514
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002515 # Optimization to avoid multiple RPCs:
2516 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2517 'CURRENT_COMMIT' not in options):
2518 options.append('CURRENT_COMMIT')
2519
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002520 # Normalize issue and options for consistent keys in cache.
2521 issue = str(issue)
2522 options = [o.upper() for o in options]
2523
2524 # Check in cache first unless no_cache is True.
2525 if no_cache:
2526 self._detail_cache.pop(issue, None)
2527 else:
2528 options_set = frozenset(options)
2529 for cached_options_set, data in self._detail_cache.get(issue, []):
2530 # Assumption: data fetched before with extra options is suitable
2531 # for return for a smaller set of options.
2532 # For example, if we cached data for
2533 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2534 # and request is for options=[CURRENT_REVISION],
2535 # THEN we can return prior cached data.
2536 if options_set.issubset(cached_options_set):
2537 return data
2538
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002539 try:
2540 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2541 options, ignore_404=False)
2542 except gerrit_util.GerritError as e:
2543 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002544 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002545 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002546
2547 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002548 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002549
agable32978d92016-11-01 12:55:02 -07002550 def _GetChangeCommit(self, issue=None):
2551 issue = issue or self.GetIssue()
2552 assert issue, 'issue is required to query Gerrit'
2553 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2554 if not data:
Aaron Gablea45ee112016-11-22 15:14:38 -08002555 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
agable32978d92016-11-01 12:55:02 -07002556 return data
2557
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002558 def CMDLand(self, force, bypass_hooks, verbose):
2559 if git_common.is_dirty_git_tree('land'):
2560 return 1
tandriid60367b2016-06-22 05:25:12 -07002561 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2562 if u'Commit-Queue' in detail.get('labels', {}):
2563 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002564 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2565 'which can test and land changes for you. '
2566 'Are you sure you wish to bypass it?\n',
2567 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002568
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002569 differs = True
tandriic4344b52016-08-29 06:04:54 -07002570 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002571 # Note: git diff outputs nothing if there is no diff.
2572 if not last_upload or RunGit(['diff', last_upload]).strip():
2573 print('WARNING: some changes from local branch haven\'t been uploaded')
2574 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002575 if detail['current_revision'] == last_upload:
2576 differs = False
2577 else:
2578 print('WARNING: local branch contents differ from latest uploaded '
2579 'patchset')
2580 if differs:
2581 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002582 confirm_or_exit(
2583 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2584 action='submit')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002585 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2586 elif not bypass_hooks:
2587 hook_results = self.RunHook(
2588 committing=True,
2589 may_prompt=not force,
2590 verbose=verbose,
2591 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2592 if not hook_results.should_continue():
2593 return 1
2594
2595 self.SubmitIssue(wait_for_merge=True)
2596 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002597 links = self._GetChangeCommit().get('web_links', [])
2598 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002599 if link.get('name') == 'gitiles' and link.get('url'):
agable32978d92016-11-01 12:55:02 -07002600 print('Landed as %s' % link.get('url'))
2601 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002602 return 0
2603
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002604 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2605 directory):
2606 assert not reject
2607 assert not nocommit
2608 assert not directory
2609 assert parsed_issue_arg.valid
2610
2611 self._changelist.issue = parsed_issue_arg.issue
2612
2613 if parsed_issue_arg.hostname:
2614 self._gerrit_host = parsed_issue_arg.hostname
2615 self._gerrit_server = 'https://%s' % self._gerrit_host
2616
tandriic2405f52016-10-10 08:13:15 -07002617 try:
2618 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002619 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002620 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002621
2622 if not parsed_issue_arg.patchset:
2623 # Use current revision by default.
2624 revision_info = detail['revisions'][detail['current_revision']]
2625 patchset = int(revision_info['_number'])
2626 else:
2627 patchset = parsed_issue_arg.patchset
2628 for revision_info in detail['revisions'].itervalues():
2629 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2630 break
2631 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002632 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002633 (parsed_issue_arg.patchset, self.GetIssue()))
2634
2635 fetch_info = revision_info['fetch']['http']
2636 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2637 RunGit(['cherry-pick', 'FETCH_HEAD'])
2638 self.SetIssue(self.GetIssue())
2639 self.SetPatchset(patchset)
Aaron Gablea45ee112016-11-22 15:14:38 -08002640 print('Committed patch for change %i patchset %i locally' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002641 (self.GetIssue(), self.GetPatchset()))
2642 return 0
2643
2644 @staticmethod
2645 def ParseIssueURL(parsed_url):
2646 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2647 return None
2648 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2649 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2650 # Short urls like https://domain/<issue_number> can be used, but don't allow
2651 # specifying the patchset (you'd 404), but we allow that here.
2652 if parsed_url.path == '/':
2653 part = parsed_url.fragment
2654 else:
2655 part = parsed_url.path
2656 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2657 if match:
2658 return _ParsedIssueNumberArgument(
2659 issue=int(match.group(2)),
2660 patchset=int(match.group(4)) if match.group(4) else None,
2661 hostname=parsed_url.netloc)
2662 return None
2663
tandrii16e0b4e2016-06-07 10:34:28 -07002664 def _GerritCommitMsgHookCheck(self, offer_removal):
2665 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2666 if not os.path.exists(hook):
2667 return
2668 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2669 # custom developer made one.
2670 data = gclient_utils.FileRead(hook)
2671 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2672 return
2673 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002674 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002675 'and may interfere with it in subtle ways.\n'
2676 'We recommend you remove the commit-msg hook.')
2677 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002678 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002679 gclient_utils.rm_file_or_tree(hook)
2680 print('Gerrit commit-msg hook removed.')
2681 else:
2682 print('OK, will keep Gerrit commit-msg hook in place.')
2683
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002684 def CMDUploadChange(self, options, args, change):
2685 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002686 if options.squash and options.no_squash:
2687 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002688
2689 if not options.squash and not options.no_squash:
2690 # Load default for user, repo, squash=true, in this order.
2691 options.squash = settings.GetSquashGerritUploads()
2692 elif options.no_squash:
2693 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002694
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002695 # We assume the remote called "origin" is the one we want.
2696 # It is probably not worthwhile to support different workflows.
2697 gerrit_remote = 'origin'
2698
2699 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002700 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002701
Aaron Gableb56ad332017-01-06 15:24:31 -08002702 # This may be None; default fallback value is determined in logic below.
2703 title = options.title
Aaron Gable856585d2017-01-23 15:32:00 -08002704 automatic_title = False
Aaron Gableb56ad332017-01-06 15:24:31 -08002705
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002706 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002707 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002708 if self.GetIssue():
2709 # Try to get the message from a previous upload.
2710 message = self.GetDescription()
2711 if not message:
2712 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002713 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002714 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002715 if not title:
2716 default_title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
2717 title = ask_for_data(
2718 'Title for patchset [%s]: ' % default_title) or default_title
Aaron Gable856585d2017-01-23 15:32:00 -08002719 if title == default_title:
2720 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002721 change_id = self._GetChangeDetail()['change_id']
2722 while True:
2723 footer_change_ids = git_footers.get_footer_change_id(message)
2724 if footer_change_ids == [change_id]:
2725 break
2726 if not footer_change_ids:
2727 message = git_footers.add_footer_change_id(message, change_id)
Aaron Gablea45ee112016-11-22 15:14:38 -08002728 print('WARNING: appended missing Change-Id to change description')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002729 continue
2730 # There is already a valid footer but with different or several ids.
2731 # Doing this automatically is non-trivial as we don't want to lose
2732 # existing other footers, yet we want to append just 1 desired
2733 # Change-Id. Thus, just create a new footer, but let user verify the
2734 # new description.
2735 message = '%s\n\nChange-Id: %s' % (message, change_id)
2736 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002737 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002738 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002739 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002740 'Please, check the proposed correction to the description, '
2741 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2742 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2743 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002744 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002745 if not options.force:
2746 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002747 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002748 message = change_desc.description
2749 if not message:
2750 DieWithError("Description is empty. Aborting...")
2751 # Continue the while loop.
2752 # Sanity check of this code - we should end up with proper message
2753 # footer.
2754 assert [change_id] == git_footers.get_footer_change_id(message)
2755 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002756 else: # if not self.GetIssue()
2757 if options.message:
2758 message = options.message
2759 else:
2760 message = CreateDescriptionFromLog(args)
2761 if options.title:
2762 message = options.title + '\n\n' + message
2763 change_desc = ChangeDescription(message)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002764 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002765 change_desc.prompt(bug=options.bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002766 # On first upload, patchset title is always this string, while
2767 # --title flag gets converted to first line of message.
2768 title = 'Initial upload'
Aaron Gable856585d2017-01-23 15:32:00 -08002769 automatic_title = True
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002770 if not change_desc.description:
2771 DieWithError("Description is empty. Aborting...")
2772 message = change_desc.description
2773 change_ids = git_footers.get_footer_change_id(message)
2774 if len(change_ids) > 1:
2775 DieWithError('too many Change-Id footers, at most 1 allowed.')
2776 if not change_ids:
2777 # Generate the Change-Id automatically.
2778 message = git_footers.add_footer_change_id(
2779 message, GenerateGerritChangeId(message))
2780 change_desc.set_description(message)
2781 change_ids = git_footers.get_footer_change_id(message)
2782 assert len(change_ids) == 1
2783 change_id = change_ids[0]
2784
2785 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2786 if remote is '.':
2787 # If our upstream branch is local, we base our squashed commit on its
2788 # squashed version.
2789 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2790 # Check the squashed hash of the parent.
2791 parent = RunGit(['config',
2792 'branch.%s.gerritsquashhash' % upstream_branch_name],
2793 error_ok=True).strip()
2794 # Verify that the upstream branch has been uploaded too, otherwise
2795 # Gerrit will create additional CLs when uploading.
2796 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2797 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002798 DieWithError(
Aaron Gable48746652017-01-06 11:49:21 -08002799 '\nUpload upstream branch %s first.\n'
2800 'It is likely that this branch has been rebased since its last '
2801 'upload, so you just need to upload it again.\n'
2802 '(If you uploaded it with --no-squash, then branch dependencies '
2803 'are not supported, and you should reupload with --squash.)'
Christopher Lamf732cd52017-01-24 12:40:11 +11002804 % upstream_branch_name, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002805 else:
2806 parent = self.GetCommonAncestorWithUpstream()
2807
2808 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2809 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2810 '-m', message]).strip()
2811 else:
2812 change_desc = ChangeDescription(
2813 options.message or CreateDescriptionFromLog(args))
2814 if not change_desc.description:
2815 DieWithError("Description is empty. Aborting...")
2816
2817 if not git_footers.get_footer_change_id(change_desc.description):
2818 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002819 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2820 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002821 ref_to_push = 'HEAD'
2822 parent = '%s/%s' % (gerrit_remote, branch)
2823 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2824
2825 assert change_desc
2826 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2827 ref_to_push)]).splitlines()
2828 if len(commits) > 1:
2829 print('WARNING: This will upload %d commits. Run the following command '
2830 'to see which commits will be uploaded: ' % len(commits))
2831 print('git log %s..%s' % (parent, ref_to_push))
2832 print('You can also use `git squash-branch` to squash these into a '
2833 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002834 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002835
2836 if options.reviewers or options.tbr_owners:
2837 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2838 change)
2839
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002840 # Extra options that can be specified at push time. Doc:
2841 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2842 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002843 if change_desc.get_reviewers(tbr_only=True):
2844 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2845 refspec_opts.append('l=Code-Review+1')
2846
Aaron Gable9b713dd2016-12-14 16:04:21 -08002847 if title:
2848 if not re.match(r'^[\w ]+$', title):
2849 title = re.sub(r'[^\w ]', '', title)
Aaron Gable856585d2017-01-23 15:32:00 -08002850 if not automatic_title:
2851 print('WARNING: Patchset title may only contain alphanumeric chars '
Aaron Gableeb3af712017-02-02 12:42:02 -08002852 'and spaces. You can edit it in the UI. '
2853 'See https://crbug.com/663787.\n'
2854 'Cleaned up title: %s' % title)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002855 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2856 # reverse on its side.
Aaron Gable9b713dd2016-12-14 16:04:21 -08002857 refspec_opts.append('m=' + title.replace(' ', '_'))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002858
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002859 if options.send_mail:
2860 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002861 DieWithError('Must specify reviewers to send email.', change_desc)
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002862 refspec_opts.append('notify=ALL')
2863 else:
2864 refspec_opts.append('notify=NONE')
2865
tandrii99a72f22016-08-17 14:33:24 -07002866 reviewers = change_desc.get_reviewers()
2867 if reviewers:
Andrii Shyshkalovaf3a9992017-02-09 14:18:01 +01002868 # TODO(tandrii): remove this horrible hack once (Poly)Gerrit fixes their
2869 # side for real (b/34702620).
2870 def clean_invisible_chars(email):
2871 return email.decode('unicode_escape').encode('ascii', 'ignore')
2872 refspec_opts.extend('r=' + clean_invisible_chars(email).strip()
2873 for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002874
agablec6787972016-09-09 16:13:34 -07002875 if options.private:
2876 refspec_opts.append('draft')
2877
rmistry9eadede2016-09-19 11:22:43 -07002878 if options.topic:
2879 # Documentation on Gerrit topics is here:
2880 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2881 refspec_opts.append('topic=%s' % options.topic)
2882
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002883 refspec_suffix = ''
2884 if refspec_opts:
2885 refspec_suffix = '%' + ','.join(refspec_opts)
2886 assert ' ' not in refspec_suffix, (
2887 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002888 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002889
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002890 try:
2891 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalove9c78ff2017-02-06 15:53:13 +01002892 ['git', 'push', self.GetRemoteUrl(), refspec],
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002893 print_stdout=True,
2894 # Flush after every line: useful for seeing progress when running as
2895 # recipe.
2896 filter_fn=lambda _: sys.stdout.flush())
2897 except subprocess2.CalledProcessError:
2898 DieWithError('Failed to create a change. Please examine output above '
Christopher Lamf732cd52017-01-24 12:40:11 +11002899 'for the reason of the failure. ', change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002900
2901 if options.squash:
2902 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2903 change_numbers = [m.group(1)
2904 for m in map(regex.match, push_stdout.splitlines())
2905 if m]
2906 if len(change_numbers) != 1:
2907 DieWithError(
2908 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002909 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002910 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002911 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002912
2913 # Add cc's from the CC_LIST and --cc flag (if any).
2914 cc = self.GetCCList().split(',')
2915 if options.cc:
2916 cc.extend(options.cc)
2917 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002918 if change_desc.get_cced():
2919 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002920 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002921 gerrit_util.AddReviewers(
Aaron Gable59f48512017-01-12 10:54:46 -08002922 self._GetGerritHost(), self.GetIssue(), cc,
2923 is_reviewer=False, notify=bool(options.send_mail))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002924 return 0
2925
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002926 def _AddChangeIdToCommitMessage(self, options, args):
2927 """Re-commits using the current message, assumes the commit hook is in
2928 place.
2929 """
2930 log_desc = options.message or CreateDescriptionFromLog(args)
2931 git_command = ['commit', '--amend', '-m', log_desc]
2932 RunGit(git_command)
2933 new_log_desc = CreateDescriptionFromLog(args)
2934 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002935 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002936 return new_log_desc
2937 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002938 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002939
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002940 def SetCQState(self, new_state):
2941 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002942 vote_map = {
2943 _CQState.NONE: 0,
2944 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002945 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002946 }
Andrii Shyshkalov828701b2016-12-09 10:46:47 +01002947 kwargs = {'labels': {'Commit-Queue': vote_map[new_state]}}
2948 if new_state == _CQState.DRY_RUN:
2949 # Don't spam everybody reviewer/owner.
2950 kwargs['notify'] = 'NONE'
2951 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(), **kwargs)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002952
tandriie113dfd2016-10-11 10:20:12 -07002953 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002954 try:
2955 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002956 except GerritChangeNotExists:
2957 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002958
2959 if data['status'] in ('ABANDONED', 'MERGED'):
2960 return 'CL %s is closed' % self.GetIssue()
2961
2962 def GetTryjobProperties(self, patchset=None):
2963 """Returns dictionary of properties to launch tryjob."""
2964 data = self._GetChangeDetail(['ALL_REVISIONS'])
2965 patchset = int(patchset or self.GetPatchset())
2966 assert patchset
2967 revision_data = None # Pylint wants it to be defined.
2968 for revision_data in data['revisions'].itervalues():
2969 if int(revision_data['_number']) == patchset:
2970 break
2971 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002972 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002973 (patchset, self.GetIssue()))
2974 return {
2975 'patch_issue': self.GetIssue(),
2976 'patch_set': patchset or self.GetPatchset(),
2977 'patch_project': data['project'],
2978 'patch_storage': 'gerrit',
2979 'patch_ref': revision_data['fetch']['http']['ref'],
2980 'patch_repository_url': revision_data['fetch']['http']['url'],
2981 'patch_gerrit_url': self.GetCodereviewServer(),
2982 }
tandriie113dfd2016-10-11 10:20:12 -07002983
tandriide281ae2016-10-12 06:02:30 -07002984 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002985 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002986
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002987
2988_CODEREVIEW_IMPLEMENTATIONS = {
2989 'rietveld': _RietveldChangelistImpl,
2990 'gerrit': _GerritChangelistImpl,
2991}
2992
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002993
iannuccie53c9352016-08-17 14:40:40 -07002994def _add_codereview_issue_select_options(parser, extra=""):
2995 _add_codereview_select_options(parser)
2996
2997 text = ('Operate on this issue number instead of the current branch\'s '
2998 'implicit issue.')
2999 if extra:
3000 text += ' '+extra
3001 parser.add_option('-i', '--issue', type=int, help=text)
3002
3003
3004def _process_codereview_issue_select_options(parser, options):
3005 _process_codereview_select_options(parser, options)
3006 if options.issue is not None and not options.forced_codereview:
3007 parser.error('--issue must be specified with either --rietveld or --gerrit')
3008
3009
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003010def _add_codereview_select_options(parser):
3011 """Appends --gerrit and --rietveld options to force specific codereview."""
3012 parser.codereview_group = optparse.OptionGroup(
3013 parser, 'EXPERIMENTAL! Codereview override options')
3014 parser.add_option_group(parser.codereview_group)
3015 parser.codereview_group.add_option(
3016 '--gerrit', action='store_true',
3017 help='Force the use of Gerrit for codereview')
3018 parser.codereview_group.add_option(
3019 '--rietveld', action='store_true',
3020 help='Force the use of Rietveld for codereview')
3021
3022
3023def _process_codereview_select_options(parser, options):
3024 if options.gerrit and options.rietveld:
3025 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3026 options.forced_codereview = None
3027 if options.gerrit:
3028 options.forced_codereview = 'gerrit'
3029 elif options.rietveld:
3030 options.forced_codereview = 'rietveld'
3031
3032
tandriif9aefb72016-07-01 09:06:51 -07003033def _get_bug_line_values(default_project, bugs):
3034 """Given default_project and comma separated list of bugs, yields bug line
3035 values.
3036
3037 Each bug can be either:
3038 * a number, which is combined with default_project
3039 * string, which is left as is.
3040
3041 This function may produce more than one line, because bugdroid expects one
3042 project per line.
3043
3044 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3045 ['v8:123', 'chromium:789']
3046 """
3047 default_bugs = []
3048 others = []
3049 for bug in bugs.split(','):
3050 bug = bug.strip()
3051 if bug:
3052 try:
3053 default_bugs.append(int(bug))
3054 except ValueError:
3055 others.append(bug)
3056
3057 if default_bugs:
3058 default_bugs = ','.join(map(str, default_bugs))
3059 if default_project:
3060 yield '%s:%s' % (default_project, default_bugs)
3061 else:
3062 yield default_bugs
3063 for other in sorted(others):
3064 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3065 yield other
3066
3067
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003068class ChangeDescription(object):
3069 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003070 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003071 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Mark Mentovai600d3092017-03-08 12:58:18 -05003072 BUG_LINE = r'^[ \t]*(BUGS?|Bugs?)[ \t]*[:=][ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003073 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003074
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003075 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003076 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003077
agable@chromium.org42c20792013-09-12 17:34:49 +00003078 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003079 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003080 return '\n'.join(self._description_lines)
3081
3082 def set_description(self, desc):
3083 if isinstance(desc, basestring):
3084 lines = desc.splitlines()
3085 else:
3086 lines = [line.rstrip() for line in desc]
3087 while lines and not lines[0]:
3088 lines.pop(0)
3089 while lines and not lines[-1]:
3090 lines.pop(-1)
3091 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003092
piman@chromium.org336f9122014-09-04 02:16:55 +00003093 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003094 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003095 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003096 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003097 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003098 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003099
agable@chromium.org42c20792013-09-12 17:34:49 +00003100 # Get the set of R= and TBR= lines and remove them from the desciption.
3101 regexp = re.compile(self.R_LINE)
3102 matches = [regexp.match(line) for line in self._description_lines]
3103 new_desc = [l for i, l in enumerate(self._description_lines)
3104 if not matches[i]]
3105 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003106
agable@chromium.org42c20792013-09-12 17:34:49 +00003107 # Construct new unified R= and TBR= lines.
3108 r_names = []
3109 tbr_names = []
3110 for match in matches:
3111 if not match:
3112 continue
3113 people = cleanup_list([match.group(2).strip()])
3114 if match.group(1) == 'TBR':
3115 tbr_names.extend(people)
3116 else:
3117 r_names.extend(people)
3118 for name in r_names:
3119 if name not in reviewers:
3120 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003121 if add_owners_tbr:
3122 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003123 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003124 all_reviewers = set(tbr_names + reviewers)
3125 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3126 all_reviewers)
3127 tbr_names.extend(owners_db.reviewers_for(missing_files,
3128 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003129 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3130 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3131
3132 # Put the new lines in the description where the old first R= line was.
3133 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3134 if 0 <= line_loc < len(self._description_lines):
3135 if new_tbr_line:
3136 self._description_lines.insert(line_loc, new_tbr_line)
3137 if new_r_line:
3138 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003139 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003140 if new_r_line:
3141 self.append_footer(new_r_line)
3142 if new_tbr_line:
3143 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003144
tandriif9aefb72016-07-01 09:06:51 -07003145 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003146 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003147 self.set_description([
3148 '# Enter a description of the change.',
3149 '# This will be displayed on the codereview site.',
3150 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003151 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003152 '--------------------',
3153 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003154
agable@chromium.org42c20792013-09-12 17:34:49 +00003155 regexp = re.compile(self.BUG_LINE)
3156 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003157 prefix = settings.GetBugPrefix()
3158 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Mark Mentovai57c47212017-03-09 11:14:09 -05003159 bug_line_format = settings.GetBugLineFormat()
tandriif9aefb72016-07-01 09:06:51 -07003160 for value in values:
Mark Mentovai57c47212017-03-09 11:14:09 -05003161 self.append_footer(bug_line_format % value)
tandriif9aefb72016-07-01 09:06:51 -07003162
agable@chromium.org42c20792013-09-12 17:34:49 +00003163 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003164 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003165 if not content:
3166 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003167 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003168
3169 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003170 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3171 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003172 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003173 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003174
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003175 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003176 """Adds a footer line to the description.
3177
3178 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3179 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3180 that Gerrit footers are always at the end.
3181 """
3182 parsed_footer_line = git_footers.parse_footer(line)
3183 if parsed_footer_line:
3184 # Line is a gerrit footer in the form: Footer-Key: any value.
3185 # Thus, must be appended observing Gerrit footer rules.
3186 self.set_description(
3187 git_footers.add_footer(self.description,
3188 key=parsed_footer_line[0],
3189 value=parsed_footer_line[1]))
3190 return
3191
3192 if not self._description_lines:
3193 self._description_lines.append(line)
3194 return
3195
3196 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3197 if gerrit_footers:
3198 # git_footers.split_footers ensures that there is an empty line before
3199 # actual (gerrit) footers, if any. We have to keep it that way.
3200 assert top_lines and top_lines[-1] == ''
3201 top_lines, separator = top_lines[:-1], top_lines[-1:]
3202 else:
3203 separator = [] # No need for separator if there are no gerrit_footers.
3204
3205 prev_line = top_lines[-1] if top_lines else ''
3206 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3207 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3208 top_lines.append('')
3209 top_lines.append(line)
3210 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003211
tandrii99a72f22016-08-17 14:33:24 -07003212 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003213 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003214 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003215 reviewers = [match.group(2).strip()
3216 for match in matches
3217 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003218 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003219
bradnelsond975b302016-10-23 12:20:23 -07003220 def get_cced(self):
3221 """Retrieves the list of reviewers."""
3222 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3223 cced = [match.group(2).strip() for match in matches if match]
3224 return cleanup_list(cced)
3225
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003226 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3227 """Updates this commit description given the parent.
3228
3229 This is essentially what Gnumbd used to do.
3230 Consult https://goo.gl/WMmpDe for more details.
3231 """
3232 assert parent_msg # No, orphan branch creation isn't supported.
3233 assert parent_hash
3234 assert dest_ref
3235 parent_footer_map = git_footers.parse_footers(parent_msg)
3236 # This will also happily parse svn-position, which GnumbD is no longer
3237 # supporting. While we'd generate correct footers, the verifier plugin
3238 # installed in Gerrit will block such commit (ie git push below will fail).
3239 parent_position = git_footers.get_position(parent_footer_map)
3240
3241 # Cherry-picks may have last line obscuring their prior footers,
3242 # from git_footers perspective. This is also what Gnumbd did.
3243 cp_line = None
3244 if (self._description_lines and
3245 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3246 cp_line = self._description_lines.pop()
3247
3248 top_lines, _, parsed_footers = git_footers.split_footers(self.description)
3249
3250 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3251 # user interference with actual footers we'd insert below.
3252 for i, (k, v) in enumerate(parsed_footers):
3253 if k.startswith('Cr-'):
3254 parsed_footers[i] = (k.replace('Cr-', 'Cr-Original-'), v)
3255
3256 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003257 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003258 if parent_position[0] == dest_ref:
3259 # Same branch as parent.
3260 number = int(parent_position[1]) + 1
3261 else:
3262 number = 1 # New branch, and extra lineage.
3263 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3264 int(parent_position[1])))
3265
3266 parsed_footers.append(('Cr-Commit-Position',
3267 '%s@{#%d}' % (dest_ref, number)))
3268 parsed_footers.extend(('Cr-Branched-From', v) for v in lineage)
3269
3270 self._description_lines = top_lines
3271 if cp_line:
3272 self._description_lines.append(cp_line)
3273 if self._description_lines[-1] != '':
3274 self._description_lines.append('') # Ensure footer separator.
3275 self._description_lines.extend('%s: %s' % kv for kv in parsed_footers)
3276
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003277
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003278def get_approving_reviewers(props):
3279 """Retrieves the reviewers that approved a CL from the issue properties with
3280 messages.
3281
3282 Note that the list may contain reviewers that are not committer, thus are not
3283 considered by the CQ.
3284 """
3285 return sorted(
3286 set(
3287 message['sender']
3288 for message in props['messages']
3289 if message['approval'] and message['sender'] in props['reviewers']
3290 )
3291 )
3292
3293
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003294def FindCodereviewSettingsFile(filename='codereview.settings'):
3295 """Finds the given file starting in the cwd and going up.
3296
3297 Only looks up to the top of the repository unless an
3298 'inherit-review-settings-ok' file exists in the root of the repository.
3299 """
3300 inherit_ok_file = 'inherit-review-settings-ok'
3301 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003302 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003303 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3304 root = '/'
3305 while True:
3306 if filename in os.listdir(cwd):
3307 if os.path.isfile(os.path.join(cwd, filename)):
3308 return open(os.path.join(cwd, filename))
3309 if cwd == root:
3310 break
3311 cwd = os.path.dirname(cwd)
3312
3313
3314def LoadCodereviewSettingsFromFile(fileobj):
3315 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003316 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003317
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003318 def SetProperty(name, setting, unset_error_ok=False):
3319 fullname = 'rietveld.' + name
3320 if setting in keyvals:
3321 RunGit(['config', fullname, keyvals[setting]])
3322 else:
3323 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3324
tandrii48df5812016-10-17 03:55:37 -07003325 if not keyvals.get('GERRIT_HOST', False):
3326 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003327 # Only server setting is required. Other settings can be absent.
3328 # In that case, we ignore errors raised during option deletion attempt.
3329 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003330 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003331 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3332 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
Mark Mentovai57c47212017-03-09 11:14:09 -05003333 SetProperty('bug-line-format', 'BUG_LINE_FORMAT', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003334 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003335 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3336 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003337 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003338 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3339 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003340
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003341 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003342 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003343
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003344 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003345 RunGit(['config', 'gerrit.squash-uploads',
3346 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003347
tandrii@chromium.org28253532016-04-14 13:46:56 +00003348 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003349 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003350 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3351
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003352 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003353 # should be of the form
3354 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3355 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003356 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3357 keyvals['ORIGIN_URL_CONFIG']])
3358
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003359
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003360def urlretrieve(source, destination):
3361 """urllib is broken for SSL connections via a proxy therefore we
3362 can't use urllib.urlretrieve()."""
3363 with open(destination, 'w') as f:
3364 f.write(urllib2.urlopen(source).read())
3365
3366
ukai@chromium.org712d6102013-11-27 00:52:58 +00003367def hasSheBang(fname):
3368 """Checks fname is a #! script."""
3369 with open(fname) as f:
3370 return f.read(2).startswith('#!')
3371
3372
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003373# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3374def DownloadHooks(*args, **kwargs):
3375 pass
3376
3377
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003378def DownloadGerritHook(force):
3379 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003380
3381 Args:
3382 force: True to update hooks. False to install hooks if not present.
3383 """
3384 if not settings.GetIsGerrit():
3385 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003386 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003387 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3388 if not os.access(dst, os.X_OK):
3389 if os.path.exists(dst):
3390 if not force:
3391 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003392 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003393 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003394 if not hasSheBang(dst):
3395 DieWithError('Not a script: %s\n'
3396 'You need to download from\n%s\n'
3397 'into .git/hooks/commit-msg and '
3398 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003399 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3400 except Exception:
3401 if os.path.exists(dst):
3402 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003403 DieWithError('\nFailed to download hooks.\n'
3404 'You need to download from\n%s\n'
3405 'into .git/hooks/commit-msg and '
3406 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003407
3408
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003409def GetRietveldCodereviewSettingsInteractively():
3410 """Prompt the user for settings."""
3411 server = settings.GetDefaultServerUrl(error_ok=True)
3412 prompt = 'Rietveld server (host[:port])'
3413 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3414 newserver = ask_for_data(prompt + ':')
3415 if not server and not newserver:
3416 newserver = DEFAULT_SERVER
3417 if newserver:
3418 newserver = gclient_utils.UpgradeToHttps(newserver)
3419 if newserver != server:
3420 RunGit(['config', 'rietveld.server', newserver])
3421
3422 def SetProperty(initial, caption, name, is_url):
3423 prompt = caption
3424 if initial:
3425 prompt += ' ("x" to clear) [%s]' % initial
3426 new_val = ask_for_data(prompt + ':')
3427 if new_val == 'x':
3428 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3429 elif new_val:
3430 if is_url:
3431 new_val = gclient_utils.UpgradeToHttps(new_val)
3432 if new_val != initial:
3433 RunGit(['config', 'rietveld.' + name, new_val])
3434
3435 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3436 SetProperty(settings.GetDefaultPrivateFlag(),
3437 'Private flag (rietveld only)', 'private', False)
3438 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3439 'tree-status-url', False)
3440 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3441 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3442 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3443 'run-post-upload-hook', False)
3444
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003445
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003446class _GitCookiesChecker(object):
3447 """Provides facilties for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003448
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003449 def ensure_configured_gitcookies(self):
3450 """Runs checks and suggests fixes to make git use .gitcookies from default
3451 path."""
3452 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3453 configured_path = RunGitSilent(
3454 ['config', '--global', 'http.cookiefile']).strip()
3455 if configured_path:
3456 self._ensure_default_gitcookies_path(configured_path, default)
3457 else:
3458 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003459
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003460 @staticmethod
3461 def _ensure_default_gitcookies_path(configured_path, default_path):
3462 assert configured_path
3463 if configured_path == default_path:
3464 print('git is already configured to use your .gitcookies from %s' %
3465 configured_path)
3466 return
3467
3468 print('WARNING: you have configured custom path to .gitcookies: %s\n'
3469 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3470 (configured_path, default_path))
3471
3472 if not os.path.exists(configured_path):
3473 print('However, your configured .gitcookies file is missing.')
3474 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3475 action='reconfigure')
3476 RunGit(['config', '--global', 'http.cookiefile', default_path])
3477 return
3478
3479 if os.path.exists(default_path):
3480 print('WARNING: default .gitcookies file already exists %s' %
3481 default_path)
3482 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3483 default_path)
3484
3485 confirm_or_exit('Move existing .gitcookies to default location?',
3486 action='move')
3487 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003488 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003489 print('Moved and reconfigured git to use .gitcookies from %s' %
3490 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003491
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003492 @staticmethod
3493 def _configure_gitcookies_path(default_path):
3494 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3495 if os.path.exists(netrc_path):
3496 print('You seem to be using outdated .netrc for git credentials: %s' %
3497 netrc_path)
3498 print('This tool will guide you through setting up recommended '
3499 '.gitcookies store for git credentials.\n'
3500 '\n'
3501 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3502 ' git config --global --unset http.cookiefile\n'
3503 ' mv %s %s.backup\n\n' % (default_path, default_path))
3504 confirm_or_exit(action='setup .gitcookies')
3505 RunGit(['config', '--global', 'http.cookiefile', default_path])
3506 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003507
3508
3509def CMDcreds_check(parser, args):
3510 """Checks credentials and suggests changes."""
3511 _, _ = parser.parse_args(args)
3512
3513 if gerrit_util.GceAuthenticator.is_gce():
3514 DieWithError('this command is not designed for GCE, are you on a bot?')
3515
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003516 checker = _GitCookiesChecker()
3517 checker.ensure_configured_gitcookies()
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003518 # TODO(tandrii): finish this.
3519 return 0
3520
3521
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003522@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003523def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003524 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003525
tandrii5d0a0422016-09-14 06:24:35 -07003526 print('WARNING: git cl config works for Rietveld only')
3527 # TODO(tandrii): remove this once we switch to Gerrit.
3528 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003529 parser.add_option('--activate-update', action='store_true',
3530 help='activate auto-updating [rietveld] section in '
3531 '.git/config')
3532 parser.add_option('--deactivate-update', action='store_true',
3533 help='deactivate auto-updating [rietveld] section in '
3534 '.git/config')
3535 options, args = parser.parse_args(args)
3536
3537 if options.deactivate_update:
3538 RunGit(['config', 'rietveld.autoupdate', 'false'])
3539 return
3540
3541 if options.activate_update:
3542 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3543 return
3544
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003545 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003546 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003547 return 0
3548
3549 url = args[0]
3550 if not url.endswith('codereview.settings'):
3551 url = os.path.join(url, 'codereview.settings')
3552
3553 # Load code review settings and download hooks (if available).
3554 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3555 return 0
3556
3557
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003558def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003559 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003560 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3561 branch = ShortBranchName(branchref)
3562 _, args = parser.parse_args(args)
3563 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003564 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003565 return RunGit(['config', 'branch.%s.base-url' % branch],
3566 error_ok=False).strip()
3567 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003568 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003569 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3570 error_ok=False).strip()
3571
3572
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003573def color_for_status(status):
3574 """Maps a Changelist status to color, for CMDstatus and other tools."""
3575 return {
3576 'unsent': Fore.RED,
3577 'waiting': Fore.BLUE,
3578 'reply': Fore.YELLOW,
3579 'lgtm': Fore.GREEN,
3580 'commit': Fore.MAGENTA,
3581 'closed': Fore.CYAN,
3582 'error': Fore.WHITE,
3583 }.get(status, Fore.WHITE)
3584
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003585
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003586def get_cl_statuses(changes, fine_grained, max_processes=None):
3587 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003588
3589 If fine_grained is true, this will fetch CL statuses from the server.
3590 Otherwise, simply indicate if there's a matching url for the given branches.
3591
3592 If max_processes is specified, it is used as the maximum number of processes
3593 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3594 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003595
3596 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003597 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003598 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003599 upload.verbosity = 0
3600
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003601 if not changes:
3602 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003603
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003604 if not fine_grained:
3605 # Fast path which doesn't involve querying codereview servers.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003606 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003607 for cl in changes:
3608 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003609 return
3610
3611 # First, sort out authentication issues.
3612 logging.debug('ensuring credentials exist')
3613 for cl in changes:
3614 cl.EnsureAuthenticated(force=False, refresh=True)
3615
3616 def fetch(cl):
3617 try:
3618 return (cl, cl.GetStatus())
3619 except:
3620 # See http://crbug.com/629863.
3621 logging.exception('failed to fetch status for %s:', cl)
3622 raise
3623
3624 threads_count = len(changes)
3625 if max_processes:
3626 threads_count = max(1, min(threads_count, max_processes))
3627 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3628
3629 pool = ThreadPool(threads_count)
3630 fetched_cls = set()
3631 try:
3632 it = pool.imap_unordered(fetch, changes).__iter__()
3633 while True:
3634 try:
3635 cl, status = it.next(timeout=5)
3636 except multiprocessing.TimeoutError:
3637 break
3638 fetched_cls.add(cl)
3639 yield cl, status
3640 finally:
3641 pool.close()
3642
3643 # Add any branches that failed to fetch.
3644 for cl in set(changes) - fetched_cls:
3645 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003646
rmistry@google.com2dd99862015-06-22 12:22:18 +00003647
3648def upload_branch_deps(cl, args):
3649 """Uploads CLs of local branches that are dependents of the current branch.
3650
3651 If the local branch dependency tree looks like:
3652 test1 -> test2.1 -> test3.1
3653 -> test3.2
3654 -> test2.2 -> test3.3
3655
3656 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3657 run on the dependent branches in this order:
3658 test2.1, test3.1, test3.2, test2.2, test3.3
3659
3660 Note: This function does not rebase your local dependent branches. Use it when
3661 you make a change to the parent branch that will not conflict with its
3662 dependent branches, and you would like their dependencies updated in
3663 Rietveld.
3664 """
3665 if git_common.is_dirty_git_tree('upload-branch-deps'):
3666 return 1
3667
3668 root_branch = cl.GetBranch()
3669 if root_branch is None:
3670 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3671 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01003672 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003673 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3674 'patchset dependencies without an uploaded CL.')
3675
3676 branches = RunGit(['for-each-ref',
3677 '--format=%(refname:short) %(upstream:short)',
3678 'refs/heads'])
3679 if not branches:
3680 print('No local branches found.')
3681 return 0
3682
3683 # Create a dictionary of all local branches to the branches that are dependent
3684 # on it.
3685 tracked_to_dependents = collections.defaultdict(list)
3686 for b in branches.splitlines():
3687 tokens = b.split()
3688 if len(tokens) == 2:
3689 branch_name, tracked = tokens
3690 tracked_to_dependents[tracked].append(branch_name)
3691
vapiera7fbd5a2016-06-16 09:17:49 -07003692 print()
3693 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003694 dependents = []
3695 def traverse_dependents_preorder(branch, padding=''):
3696 dependents_to_process = tracked_to_dependents.get(branch, [])
3697 padding += ' '
3698 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003699 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003700 dependents.append(dependent)
3701 traverse_dependents_preorder(dependent, padding)
3702 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003703 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003704
3705 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003706 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003707 return 0
3708
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003709 confirm_or_exit('This command will checkout all dependent branches and run '
3710 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003711
andybons@chromium.org962f9462016-02-03 20:00:42 +00003712 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003713 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003714 args.extend(['-t', 'Updated patchset dependency'])
3715
rmistry@google.com2dd99862015-06-22 12:22:18 +00003716 # Record all dependents that failed to upload.
3717 failures = {}
3718 # Go through all dependents, checkout the branch and upload.
3719 try:
3720 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003721 print()
3722 print('--------------------------------------')
3723 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003724 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003725 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003726 try:
3727 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003728 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003729 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003730 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003731 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003732 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003733 finally:
3734 # Swap back to the original root branch.
3735 RunGit(['checkout', '-q', root_branch])
3736
vapiera7fbd5a2016-06-16 09:17:49 -07003737 print()
3738 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003739 for dependent_branch in dependents:
3740 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003741 print(' %s : %s' % (dependent_branch, upload_status))
3742 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003743
3744 return 0
3745
3746
kmarshall3bff56b2016-06-06 18:31:47 -07003747def CMDarchive(parser, args):
3748 """Archives and deletes branches associated with closed changelists."""
3749 parser.add_option(
3750 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003751 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003752 parser.add_option(
3753 '-f', '--force', action='store_true',
3754 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003755 parser.add_option(
3756 '-d', '--dry-run', action='store_true',
3757 help='Skip the branch tagging and removal steps.')
3758 parser.add_option(
3759 '-t', '--notags', action='store_true',
3760 help='Do not tag archived branches. '
3761 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003762
3763 auth.add_auth_options(parser)
3764 options, args = parser.parse_args(args)
3765 if args:
3766 parser.error('Unsupported args: %s' % ' '.join(args))
3767 auth_config = auth.extract_auth_config_from_options(options)
3768
3769 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3770 if not branches:
3771 return 0
3772
vapiera7fbd5a2016-06-16 09:17:49 -07003773 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003774 changes = [Changelist(branchref=b, auth_config=auth_config)
3775 for b in branches.splitlines()]
3776 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3777 statuses = get_cl_statuses(changes,
3778 fine_grained=True,
3779 max_processes=options.maxjobs)
3780 proposal = [(cl.GetBranch(),
3781 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3782 for cl, status in statuses
3783 if status == 'closed']
3784 proposal.sort()
3785
3786 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003787 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003788 return 0
3789
3790 current_branch = GetCurrentBranch()
3791
vapiera7fbd5a2016-06-16 09:17:49 -07003792 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003793 if options.notags:
3794 for next_item in proposal:
3795 print(' ' + next_item[0])
3796 else:
3797 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3798 for next_item in proposal:
3799 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003800
kmarshall9249e012016-08-23 12:02:16 -07003801 # Quit now on precondition failure or if instructed by the user, either
3802 # via an interactive prompt or by command line flags.
3803 if options.dry_run:
3804 print('\nNo changes were made (dry run).\n')
3805 return 0
3806 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003807 print('You are currently on a branch \'%s\' which is associated with a '
3808 'closed codereview issue, so archive cannot proceed. Please '
3809 'checkout another branch and run this command again.' %
3810 current_branch)
3811 return 1
kmarshall9249e012016-08-23 12:02:16 -07003812 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003813 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3814 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003815 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003816 return 1
3817
3818 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003819 if not options.notags:
3820 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003821 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003822
vapiera7fbd5a2016-06-16 09:17:49 -07003823 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003824
3825 return 0
3826
3827
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003828def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003829 """Show status of changelists.
3830
3831 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003832 - Red not sent for review or broken
3833 - Blue waiting for review
3834 - Yellow waiting for you to reply to review
3835 - Green LGTM'ed
3836 - Magenta in the commit queue
3837 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003838
3839 Also see 'git cl comments'.
3840 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003841 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003842 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003843 parser.add_option('-f', '--fast', action='store_true',
3844 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003845 parser.add_option(
3846 '-j', '--maxjobs', action='store', type=int,
3847 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003848
3849 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003850 _add_codereview_issue_select_options(
3851 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003852 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003853 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003854 if args:
3855 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003856 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003857
iannuccie53c9352016-08-17 14:40:40 -07003858 if options.issue is not None and not options.field:
3859 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003860
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003861 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003862 cl = Changelist(auth_config=auth_config, issue=options.issue,
3863 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003864 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003865 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003866 elif options.field == 'id':
3867 issueid = cl.GetIssue()
3868 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003869 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003870 elif options.field == 'patch':
3871 patchset = cl.GetPatchset()
3872 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003873 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003874 elif options.field == 'status':
3875 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003876 elif options.field == 'url':
3877 url = cl.GetIssueURL()
3878 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003879 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003880 return 0
3881
3882 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3883 if not branches:
3884 print('No local branch found.')
3885 return 0
3886
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003887 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003888 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003889 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003890 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003891 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003892 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003893 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003894
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003895 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003896 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3897 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3898 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003899 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003900 c, status = output.next()
3901 branch_statuses[c.GetBranch()] = status
3902 status = branch_statuses.pop(branch)
3903 url = cl.GetIssueURL()
3904 if url and (not status or status == 'error'):
3905 # The issue probably doesn't exist anymore.
3906 url += ' (broken)'
3907
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003908 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003909 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003910 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003911 color = ''
3912 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003913 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003914 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003915 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003916 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003917
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003918
3919 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07003920 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003921 print('Current branch: %s' % branch)
3922 for cl in changes:
3923 if cl.GetBranch() == branch:
3924 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003925 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003926 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003927 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003928 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003929 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003930 print('Issue description:')
3931 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003932 return 0
3933
3934
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003935def colorize_CMDstatus_doc():
3936 """To be called once in main() to add colors to git cl status help."""
3937 colors = [i for i in dir(Fore) if i[0].isupper()]
3938
3939 def colorize_line(line):
3940 for color in colors:
3941 if color in line.upper():
3942 # Extract whitespaces first and the leading '-'.
3943 indent = len(line) - len(line.lstrip(' ')) + 1
3944 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3945 return line
3946
3947 lines = CMDstatus.__doc__.splitlines()
3948 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3949
3950
phajdan.jre328cf92016-08-22 04:12:17 -07003951def write_json(path, contents):
3952 with open(path, 'w') as f:
3953 json.dump(contents, f)
3954
3955
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003956@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003957def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003958 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003959
3960 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003961 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003962 parser.add_option('-r', '--reverse', action='store_true',
3963 help='Lookup the branch(es) for the specified issues. If '
3964 'no issues are specified, all branches with mapped '
3965 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003966 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003967 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003968 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003969 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003970
dnj@chromium.org406c4402015-03-03 17:22:28 +00003971 if options.reverse:
3972 branches = RunGit(['for-each-ref', 'refs/heads',
3973 '--format=%(refname:short)']).splitlines()
3974
3975 # Reverse issue lookup.
3976 issue_branch_map = {}
3977 for branch in branches:
3978 cl = Changelist(branchref=branch)
3979 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3980 if not args:
3981 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003982 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003983 for issue in args:
3984 if not issue:
3985 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003986 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003987 print('Branch for issue number %s: %s' % (
3988 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003989 if options.json:
3990 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003991 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003992 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003993 if len(args) > 0:
3994 try:
3995 issue = int(args[0])
3996 except ValueError:
3997 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003998 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003999 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07004000 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07004001 if options.json:
4002 write_json(options.json, {
4003 'issue': cl.GetIssue(),
4004 'issue_url': cl.GetIssueURL(),
4005 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004006 return 0
4007
4008
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004009def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004010 """Shows or posts review comments for any changelist."""
4011 parser.add_option('-a', '--add-comment', dest='comment',
4012 help='comment to add to an issue')
4013 parser.add_option('-i', dest='issue',
4014 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00004015 parser.add_option('-j', '--json-file',
4016 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004017 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004018 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004019 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004020
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004021 issue = None
4022 if options.issue:
4023 try:
4024 issue = int(options.issue)
4025 except ValueError:
4026 DieWithError('A review issue id is expected to be a number')
4027
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00004028 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004029
4030 if options.comment:
4031 cl.AddComment(options.comment)
4032 return 0
4033
4034 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00004035 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00004036 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00004037 summary.append({
4038 'date': message['date'],
4039 'lgtm': False,
4040 'message': message['text'],
4041 'not_lgtm': False,
4042 'sender': message['sender'],
4043 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004044 if message['disapproval']:
4045 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00004046 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004047 elif message['approval']:
4048 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00004049 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004050 elif message['sender'] == data['owner_email']:
4051 color = Fore.MAGENTA
4052 else:
4053 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07004054 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004055 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07004056 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004057 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004058 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00004059 if options.json_file:
4060 with open(options.json_file, 'wb') as f:
4061 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004062 return 0
4063
4064
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004065@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004066def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004067 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004068 parser.add_option('-d', '--display', action='store_true',
4069 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004070 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004071 help='New description to set for this issue (- for stdin, '
4072 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004073 parser.add_option('-f', '--force', action='store_true',
4074 help='Delete any unpublished Gerrit edits for this issue '
4075 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004076
4077 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004078 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004079 options, args = parser.parse_args(args)
4080 _process_codereview_select_options(parser, options)
4081
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004082 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004083 if len(args) > 0:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004084 target_issue_arg = ParseIssueNumberArgument(args[0])
4085 if not target_issue_arg.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004086 parser.print_help()
4087 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004088
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004089 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004090
martiniss6eda05f2016-06-30 10:18:35 -07004091 kwargs = {
4092 'auth_config': auth_config,
4093 'codereview': options.forced_codereview,
4094 }
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004095 if target_issue_arg:
4096 kwargs['issue'] = target_issue_arg.issue
4097 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07004098
4099 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004100
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004101 if not cl.GetIssue():
4102 DieWithError('This branch has no associated changelist.')
4103 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004104
smut@google.com34fb6b12015-07-13 20:03:26 +00004105 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004106 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004107 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004108
4109 if options.new_description:
4110 text = options.new_description
4111 if text == '-':
4112 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004113 elif text == '+':
4114 base_branch = cl.GetCommonAncestorWithUpstream()
4115 change = cl.GetChange(base_branch, None, local_description=True)
4116 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004117
4118 description.set_description(text)
4119 else:
4120 description.prompt()
4121
wychen@chromium.org063e4e52015-04-03 06:51:44 +00004122 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004123 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004124 return 0
4125
4126
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004127def CreateDescriptionFromLog(args):
4128 """Pulls out the commit log to use as a base for the CL description."""
4129 log_args = []
4130 if len(args) == 1 and not args[0].endswith('.'):
4131 log_args = [args[0] + '..']
4132 elif len(args) == 1 and args[0].endswith('...'):
4133 log_args = [args[0][:-1]]
4134 elif len(args) == 2:
4135 log_args = [args[0] + '..' + args[1]]
4136 else:
4137 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004138 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004139
4140
thestig@chromium.org44202a22014-03-11 19:22:18 +00004141def CMDlint(parser, args):
4142 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004143 parser.add_option('--filter', action='append', metavar='-x,+y',
4144 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004145 auth.add_auth_options(parser)
4146 options, args = parser.parse_args(args)
4147 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004148
4149 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004150 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004151 try:
4152 import cpplint
4153 import cpplint_chromium
4154 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004155 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004156 return 1
4157
4158 # Change the current working directory before calling lint so that it
4159 # shows the correct base.
4160 previous_cwd = os.getcwd()
4161 os.chdir(settings.GetRoot())
4162 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004163 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004164 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4165 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004166 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004167 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004168 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004169
4170 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004171 command = args + files
4172 if options.filter:
4173 command = ['--filter=' + ','.join(options.filter)] + command
4174 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004175
4176 white_regex = re.compile(settings.GetLintRegex())
4177 black_regex = re.compile(settings.GetLintIgnoreRegex())
4178 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4179 for filename in filenames:
4180 if white_regex.match(filename):
4181 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004182 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004183 else:
4184 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4185 extra_check_functions)
4186 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004187 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004188 finally:
4189 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004190 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004191 if cpplint._cpplint_state.error_count != 0:
4192 return 1
4193 return 0
4194
4195
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004196def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004197 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004198 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004199 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004200 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004201 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004202 auth.add_auth_options(parser)
4203 options, args = parser.parse_args(args)
4204 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004205
sbc@chromium.org71437c02015-04-09 19:29:40 +00004206 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004207 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004208 return 1
4209
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004210 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004211 if args:
4212 base_branch = args[0]
4213 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004214 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004215 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004216
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004217 cl.RunHook(
4218 committing=not options.upload,
4219 may_prompt=False,
4220 verbose=options.verbose,
4221 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004222 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004223
4224
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004225def GenerateGerritChangeId(message):
4226 """Returns Ixxxxxx...xxx change id.
4227
4228 Works the same way as
4229 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4230 but can be called on demand on all platforms.
4231
4232 The basic idea is to generate git hash of a state of the tree, original commit
4233 message, author/committer info and timestamps.
4234 """
4235 lines = []
4236 tree_hash = RunGitSilent(['write-tree'])
4237 lines.append('tree %s' % tree_hash.strip())
4238 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4239 if code == 0:
4240 lines.append('parent %s' % parent.strip())
4241 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4242 lines.append('author %s' % author.strip())
4243 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4244 lines.append('committer %s' % committer.strip())
4245 lines.append('')
4246 # Note: Gerrit's commit-hook actually cleans message of some lines and
4247 # whitespace. This code is not doing this, but it clearly won't decrease
4248 # entropy.
4249 lines.append(message)
4250 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4251 stdin='\n'.join(lines))
4252 return 'I%s' % change_hash.strip()
4253
4254
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004255def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004256 """Computes the remote branch ref to use for the CL.
4257
4258 Args:
4259 remote (str): The git remote for the CL.
4260 remote_branch (str): The git remote branch for the CL.
4261 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004262 """
4263 if not (remote and remote_branch):
4264 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004265
wittman@chromium.org455dc922015-01-26 20:15:50 +00004266 if target_branch:
4267 # Cannonicalize branch references to the equivalent local full symbolic
4268 # refs, which are then translated into the remote full symbolic refs
4269 # below.
4270 if '/' not in target_branch:
4271 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4272 else:
4273 prefix_replacements = (
4274 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4275 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4276 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4277 )
4278 match = None
4279 for regex, replacement in prefix_replacements:
4280 match = re.search(regex, target_branch)
4281 if match:
4282 remote_branch = target_branch.replace(match.group(0), replacement)
4283 break
4284 if not match:
4285 # This is a branch path but not one we recognize; use as-is.
4286 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004287 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4288 # Handle the refs that need to land in different refs.
4289 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004290
wittman@chromium.org455dc922015-01-26 20:15:50 +00004291 # Create the true path to the remote branch.
4292 # Does the following translation:
4293 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4294 # * refs/remotes/origin/master -> refs/heads/master
4295 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4296 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4297 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4298 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4299 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4300 'refs/heads/')
4301 elif remote_branch.startswith('refs/remotes/branch-heads'):
4302 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004303
wittman@chromium.org455dc922015-01-26 20:15:50 +00004304 return remote_branch
4305
4306
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004307def cleanup_list(l):
4308 """Fixes a list so that comma separated items are put as individual items.
4309
4310 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4311 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4312 """
4313 items = sum((i.split(',') for i in l), [])
4314 stripped_items = (i.strip() for i in items)
4315 return sorted(filter(None, stripped_items))
4316
4317
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004318@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004319def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004320 """Uploads the current changelist to codereview.
4321
4322 Can skip dependency patchset uploads for a branch by running:
4323 git config branch.branch_name.skip-deps-uploads True
4324 To unset run:
4325 git config --unset branch.branch_name.skip-deps-uploads
4326 Can also set the above globally by using the --global flag.
4327 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004328 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4329 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004330 parser.add_option('--bypass-watchlists', action='store_true',
4331 dest='bypass_watchlists',
4332 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004333 parser.add_option('-f', action='store_true', dest='force',
4334 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004335 parser.add_option('--message', '-m', dest='message',
4336 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004337 parser.add_option('-b', '--bug',
4338 help='pre-populate the bug number(s) for this issue. '
4339 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004340 parser.add_option('--message-file', dest='message_file',
4341 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004342 parser.add_option('--title', '-t', dest='title',
4343 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004344 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004345 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004346 help='reviewer email addresses')
4347 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004348 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004349 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004350 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004351 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004352 parser.add_option('--emulate_svn_auto_props',
4353 '--emulate-svn-auto-props',
4354 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004355 dest="emulate_svn_auto_props",
4356 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004357 parser.add_option('-c', '--use-commit-queue', action='store_true',
4358 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004359 parser.add_option('--private', action='store_true',
4360 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004361 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004362 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004363 metavar='TARGET',
4364 help='Apply CL to remote ref TARGET. ' +
4365 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004366 parser.add_option('--squash', action='store_true',
4367 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004368 parser.add_option('--no-squash', action='store_true',
4369 help='Don\'t squash multiple commits into one ' +
4370 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004371 parser.add_option('--topic', default=None,
4372 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004373 parser.add_option('--email', default=None,
4374 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004375 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4376 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004377 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4378 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004379 help='Send the patchset to do a CQ dry run right after '
4380 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004381 parser.add_option('--dependencies', action='store_true',
4382 help='Uploads CLs of all the local branches that depend on '
4383 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004384
rmistry@google.com2dd99862015-06-22 12:22:18 +00004385 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004386 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004387 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004388 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004389 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004390 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004391 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004392
sbc@chromium.org71437c02015-04-09 19:29:40 +00004393 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004394 return 1
4395
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004396 options.reviewers = cleanup_list(options.reviewers)
4397 options.cc = cleanup_list(options.cc)
4398
tandriib80458a2016-06-23 12:20:07 -07004399 if options.message_file:
4400 if options.message:
4401 parser.error('only one of --message and --message-file allowed.')
4402 options.message = gclient_utils.FileRead(options.message_file)
4403 options.message_file = None
4404
tandrii4d0545a2016-07-06 03:56:49 -07004405 if options.cq_dry_run and options.use_commit_queue:
4406 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4407
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004408 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4409 settings.GetIsGerrit()
4410
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004411 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004412 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004413
4414
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004415@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004416def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004417 """DEPRECATED: Used to commit the current changelist via git-svn."""
4418 message = ('git-cl no longer supports committing to SVN repositories via '
4419 'git-svn. You probably want to use `git cl land` instead.')
4420 print(message)
4421 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004422
4423
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004424@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004425def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004426 """Commits the current changelist via git.
4427
4428 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4429 upstream and closes the issue automatically and atomically.
4430
4431 Otherwise (in case of Rietveld):
4432 Squashes branch into a single commit.
4433 Updates commit message with metadata (e.g. pointer to review).
4434 Pushes the code upstream.
4435 Updates review and closes.
4436 """
4437 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4438 help='bypass upload presubmit hook')
4439 parser.add_option('-m', dest='message',
4440 help="override review description")
4441 parser.add_option('-f', action='store_true', dest='force',
4442 help="force yes to questions (don't prompt)")
4443 parser.add_option('-c', dest='contributor',
4444 help="external contributor for patch (appended to " +
4445 "description and used as author for git). Should be " +
4446 "formatted as 'First Last <email@example.com>'")
4447 add_git_similarity(parser)
4448 auth.add_auth_options(parser)
4449 (options, args) = parser.parse_args(args)
4450 auth_config = auth.extract_auth_config_from_options(options)
4451
4452 cl = Changelist(auth_config=auth_config)
4453
4454 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4455 if cl.IsGerrit():
4456 if options.message:
4457 # This could be implemented, but it requires sending a new patch to
4458 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4459 # Besides, Gerrit has the ability to change the commit message on submit
4460 # automatically, thus there is no need to support this option (so far?).
4461 parser.error('-m MESSAGE option is not supported for Gerrit.')
4462 if options.contributor:
4463 parser.error(
4464 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4465 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4466 'the contributor\'s "name <email>". If you can\'t upload such a '
4467 'commit for review, contact your repository admin and request'
4468 '"Forge-Author" permission.')
4469 if not cl.GetIssue():
4470 DieWithError('You must upload the change first to Gerrit.\n'
4471 ' If you would rather have `git cl land` upload '
4472 'automatically for you, see http://crbug.com/642759')
4473 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4474 options.verbose)
4475
4476 current = cl.GetBranch()
4477 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4478 if remote == '.':
4479 print()
4480 print('Attempting to push branch %r into another local branch!' % current)
4481 print()
4482 print('Either reparent this branch on top of origin/master:')
4483 print(' git reparent-branch --root')
4484 print()
4485 print('OR run `git rebase-update` if you think the parent branch is ')
4486 print('already committed.')
4487 print()
4488 print(' Current parent: %r' % upstream_branch)
4489 return 1
4490
4491 if not args:
4492 # Default to merging against our best guess of the upstream branch.
4493 args = [cl.GetUpstreamBranch()]
4494
4495 if options.contributor:
4496 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
4497 print("Please provide contibutor as 'First Last <email@example.com>'")
4498 return 1
4499
4500 base_branch = args[0]
4501
4502 if git_common.is_dirty_git_tree('land'):
4503 return 1
4504
4505 # This rev-list syntax means "show all commits not in my branch that
4506 # are in base_branch".
4507 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4508 base_branch]).splitlines()
4509 if upstream_commits:
4510 print('Base branch "%s" has %d commits '
4511 'not in this branch.' % (base_branch, len(upstream_commits)))
4512 print('Run "git merge %s" before attempting to land.' % base_branch)
4513 return 1
4514
4515 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
4516 if not options.bypass_hooks:
4517 author = None
4518 if options.contributor:
4519 author = re.search(r'\<(.*)\>', options.contributor).group(1)
4520 hook_results = cl.RunHook(
4521 committing=True,
4522 may_prompt=not options.force,
4523 verbose=options.verbose,
4524 change=cl.GetChange(merge_base, author))
4525 if not hook_results.should_continue():
4526 return 1
4527
4528 # Check the tree status if the tree status URL is set.
4529 status = GetTreeStatus()
4530 if 'closed' == status:
4531 print('The tree is closed. Please wait for it to reopen. Use '
4532 '"git cl land --bypass-hooks" to commit on a closed tree.')
4533 return 1
4534 elif 'unknown' == status:
4535 print('Unable to determine tree status. Please verify manually and '
4536 'use "git cl land --bypass-hooks" to commit on a closed tree.')
4537 return 1
4538
4539 change_desc = ChangeDescription(options.message)
4540 if not change_desc.description and cl.GetIssue():
4541 change_desc = ChangeDescription(cl.GetDescription())
4542
4543 if not change_desc.description:
4544 if not cl.GetIssue() and options.bypass_hooks:
4545 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
4546 else:
4547 print('No description set.')
4548 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
4549 return 1
4550
4551 # Keep a separate copy for the commit message, because the commit message
4552 # contains the link to the Rietveld issue, while the Rietveld message contains
4553 # the commit viewvc url.
4554 if cl.GetIssue():
4555 change_desc.update_reviewers(cl.GetApprovingReviewers())
4556
4557 commit_desc = ChangeDescription(change_desc.description)
4558 if cl.GetIssue():
4559 # Xcode won't linkify this URL unless there is a non-whitespace character
4560 # after it. Add a period on a new line to circumvent this. Also add a space
4561 # before the period to make sure that Gitiles continues to correctly resolve
4562 # the URL.
4563 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
4564 if options.contributor:
4565 commit_desc.append_footer('Patch from %s.' % options.contributor)
4566
4567 print('Description:')
4568 print(commit_desc.description)
4569
4570 branches = [merge_base, cl.GetBranchRef()]
4571 if not options.force:
4572 print_stats(options.similarity, options.find_copies, branches)
4573
4574 # We want to squash all this branch's commits into one commit with the proper
4575 # description. We do this by doing a "reset --soft" to the base branch (which
4576 # keeps the working copy the same), then landing that.
4577 MERGE_BRANCH = 'git-cl-commit'
4578 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4579 # Delete the branches if they exist.
4580 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4581 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4582 result = RunGitWithCode(showref_cmd)
4583 if result[0] == 0:
4584 RunGit(['branch', '-D', branch])
4585
4586 # We might be in a directory that's present in this branch but not in the
4587 # trunk. Move up to the top of the tree so that git commands that expect a
4588 # valid CWD won't fail after we check out the merge branch.
4589 rel_base_path = settings.GetRelativeRoot()
4590 if rel_base_path:
4591 os.chdir(rel_base_path)
4592
4593 # Stuff our change into the merge branch.
4594 # We wrap in a try...finally block so if anything goes wrong,
4595 # we clean up the branches.
4596 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004597 revision = None
4598 try:
4599 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
4600 RunGit(['reset', '--soft', merge_base])
4601 if options.contributor:
4602 RunGit(
4603 [
4604 'commit', '--author', options.contributor,
4605 '-m', commit_desc.description,
4606 ])
4607 else:
4608 RunGit(['commit', '-m', commit_desc.description])
4609
4610 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
4611 mirror = settings.GetGitMirror(remote)
4612 if mirror:
4613 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004614 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004615 else:
4616 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004617 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004618 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
4619
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004620 if git_numberer_enabled:
4621 # TODO(tandrii): maybe do autorebase + retry on failure
4622 # http://crbug.com/682934, but better just use Gerrit :)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004623 logging.debug('Adding git number footers')
4624 parent_msg = RunGit(['show', '-s', '--format=%B', merge_base]).strip()
4625 commit_desc.update_with_git_number_footers(merge_base, parent_msg,
4626 branch)
4627 # Ensure timestamps are monotonically increasing.
4628 timestamp = max(1 + _get_committer_timestamp(merge_base),
4629 _get_committer_timestamp('HEAD'))
4630 _git_amend_head(commit_desc.description, timestamp)
4631 change_desc = ChangeDescription(commit_desc.description)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004632
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004633 retcode, output = RunGitWithCode(
4634 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004635 if retcode == 0:
4636 revision = RunGit(['rev-parse', 'HEAD']).strip()
4637 logging.debug(output)
4638 except: # pylint: disable=bare-except
4639 if _IS_BEING_TESTED:
4640 logging.exception('this is likely your ACTUAL cause of test failure.\n'
4641 + '-' * 30 + '8<' + '-' * 30)
4642 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
4643 raise
4644 finally:
4645 # And then swap back to the original branch and clean up.
4646 RunGit(['checkout', '-q', cl.GetBranch()])
4647 RunGit(['branch', '-D', MERGE_BRANCH])
4648
4649 if not revision:
4650 print('Failed to push. If this persists, please file a bug.')
4651 return 1
4652
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004653 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004654 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004655 if viewvc_url and revision:
4656 change_desc.append_footer(
4657 'Committed: %s%s' % (viewvc_url, revision))
4658 elif revision:
4659 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004660 print('Closing issue '
4661 '(you may be prompted for your codereview password)...')
4662 cl.UpdateDescription(change_desc.description)
4663 cl.CloseIssue()
4664 props = cl.GetIssueProperties()
4665 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004666 comment = "Committed patchset #%d (id:%d) manually as %s" % (
4667 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004668 if options.bypass_hooks:
4669 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4670 else:
4671 comment += ' (presubmit successful).'
4672 cl.RpcServer().add_comment(cl.GetIssue(), comment)
4673
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004674 if os.path.isfile(POSTUPSTREAM_HOOK):
4675 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
4676
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004677 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004678
4679
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004680@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004681def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004682 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004683 parser.add_option('-b', dest='newbranch',
4684 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004685 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004686 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004687 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4688 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004689 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004690 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004691 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004692 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004693 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004694 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004695
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004696
4697 group = optparse.OptionGroup(
4698 parser,
4699 'Options for continuing work on the current issue uploaded from a '
4700 'different clone (e.g. different machine). Must be used independently '
4701 'from the other options. No issue number should be specified, and the '
4702 'branch must have an issue number associated with it')
4703 group.add_option('--reapply', action='store_true', dest='reapply',
4704 help='Reset the branch and reapply the issue.\n'
4705 'CAUTION: This will undo any local changes in this '
4706 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004707
4708 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004709 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004710 parser.add_option_group(group)
4711
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004712 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004713 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004714 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004715 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004716 auth_config = auth.extract_auth_config_from_options(options)
4717
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004718
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004719 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004720 if options.newbranch:
4721 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004722 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004723 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004724
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004725 cl = Changelist(auth_config=auth_config,
4726 codereview=options.forced_codereview)
4727 if not cl.GetIssue():
4728 parser.error('current branch must have an associated issue')
4729
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004730 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004731 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004732 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004733
4734 RunGit(['reset', '--hard', upstream])
4735 if options.pull:
4736 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004737
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004738 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4739 options.directory)
4740
4741 if len(args) != 1 or not args[0]:
4742 parser.error('Must specify issue number or url')
4743
4744 # We don't want uncommitted changes mixed up with the patch.
4745 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004746 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004747
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004748 if options.newbranch:
4749 if options.force:
4750 RunGit(['branch', '-D', options.newbranch],
4751 stderr=subprocess2.PIPE, error_ok=True)
4752 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004753 elif not GetCurrentBranch():
4754 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004755
4756 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4757
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004758 if cl.IsGerrit():
4759 if options.reject:
4760 parser.error('--reject is not supported with Gerrit codereview.')
4761 if options.nocommit:
4762 parser.error('--nocommit is not supported with Gerrit codereview.')
4763 if options.directory:
4764 parser.error('--directory is not supported with Gerrit codereview.')
4765
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004766 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004767 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004768
4769
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004770def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004771 """Fetches the tree status and returns either 'open', 'closed',
4772 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004773 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004774 if url:
4775 status = urllib2.urlopen(url).read().lower()
4776 if status.find('closed') != -1 or status == '0':
4777 return 'closed'
4778 elif status.find('open') != -1 or status == '1':
4779 return 'open'
4780 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004781 return 'unset'
4782
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004783
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004784def GetTreeStatusReason():
4785 """Fetches the tree status from a json url and returns the message
4786 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004787 url = settings.GetTreeStatusUrl()
4788 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004789 connection = urllib2.urlopen(json_url)
4790 status = json.loads(connection.read())
4791 connection.close()
4792 return status['message']
4793
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004794
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004795def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004796 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004797 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004798 status = GetTreeStatus()
4799 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004800 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004801 return 2
4802
vapiera7fbd5a2016-06-16 09:17:49 -07004803 print('The tree is %s' % status)
4804 print()
4805 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004806 if status != 'open':
4807 return 1
4808 return 0
4809
4810
maruel@chromium.org15192402012-09-06 12:38:29 +00004811def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004812 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004813 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004814 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004815 '-b', '--bot', action='append',
4816 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4817 'times to specify multiple builders. ex: '
4818 '"-b win_rel -b win_layout". See '
4819 'the try server waterfall for the builders name and the tests '
4820 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004821 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004822 '-B', '--bucket', default='',
4823 help=('Buildbucket bucket to send the try requests.'))
4824 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004825 '-m', '--master', default='',
4826 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004827 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004828 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004829 help='Revision to use for the try job; default: the revision will '
4830 'be determined by the try recipe that builder runs, which usually '
4831 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004832 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004833 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004834 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004835 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004836 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004837 '--project',
4838 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004839 'in recipe to determine to which repository or directory to '
4840 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004841 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004842 '-p', '--property', dest='properties', action='append', default=[],
4843 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004844 'key2=value2 etc. The value will be treated as '
4845 'json if decodable, or as string otherwise. '
4846 'NOTE: using this may make your try job not usable for CQ, '
4847 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004848 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004849 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4850 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004851 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004852 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004853 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004854 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004855
machenbach@chromium.org45453142015-09-15 08:45:22 +00004856 # Make sure that all properties are prop=value pairs.
4857 bad_params = [x for x in options.properties if '=' not in x]
4858 if bad_params:
4859 parser.error('Got properties with missing "=": %s' % bad_params)
4860
maruel@chromium.org15192402012-09-06 12:38:29 +00004861 if args:
4862 parser.error('Unknown arguments: %s' % args)
4863
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004864 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004865 if not cl.GetIssue():
4866 parser.error('Need to upload first')
4867
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004868 if cl.IsGerrit():
4869 # HACK: warm up Gerrit change detail cache to save on RPCs.
4870 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
4871
tandriie113dfd2016-10-11 10:20:12 -07004872 error_message = cl.CannotTriggerTryJobReason()
4873 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004874 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004875
borenet6c0efe62016-10-19 08:13:29 -07004876 if options.bucket and options.master:
4877 parser.error('Only one of --bucket and --master may be used.')
4878
qyearsley1fdfcb62016-10-24 13:22:03 -07004879 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004880
qyearsleydd49f942016-10-28 11:57:22 -07004881 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4882 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004883 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004884 if options.verbose:
4885 print('git cl try with no bots now defaults to CQ Dry Run.')
4886 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004887
borenet6c0efe62016-10-19 08:13:29 -07004888 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004889 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004890 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004891 'of bot requires an initial job from a parent (usually a builder). '
4892 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004893 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004894 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004895
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004896 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004897 # TODO(tandrii): Checking local patchset against remote patchset is only
4898 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4899 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004900 print('Warning: Codereview server has newer patchsets (%s) than most '
4901 'recent upload from local checkout (%s). Did a previous upload '
4902 'fail?\n'
4903 'By default, git cl try uses the latest patchset from '
4904 'codereview, continuing to use patchset %s.\n' %
4905 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004906
tandrii568043b2016-10-11 07:49:18 -07004907 try:
borenet6c0efe62016-10-19 08:13:29 -07004908 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4909 patchset)
tandrii568043b2016-10-11 07:49:18 -07004910 except BuildbucketResponseException as ex:
4911 print('ERROR: %s' % ex)
4912 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004913 return 0
4914
4915
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004916def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004917 """Prints info about try jobs associated with current CL."""
4918 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004919 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004920 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004921 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004922 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004923 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004924 '--color', action='store_true', default=setup_color.IS_TTY,
4925 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004926 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004927 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4928 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004929 group.add_option(
4930 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004931 parser.add_option_group(group)
4932 auth.add_auth_options(parser)
4933 options, args = parser.parse_args(args)
4934 if args:
4935 parser.error('Unrecognized args: %s' % ' '.join(args))
4936
4937 auth_config = auth.extract_auth_config_from_options(options)
4938 cl = Changelist(auth_config=auth_config)
4939 if not cl.GetIssue():
4940 parser.error('Need to upload first')
4941
tandrii221ab252016-10-06 08:12:04 -07004942 patchset = options.patchset
4943 if not patchset:
4944 patchset = cl.GetMostRecentPatchset()
4945 if not patchset:
4946 parser.error('Codereview doesn\'t know about issue %s. '
4947 'No access to issue or wrong issue number?\n'
4948 'Either upload first, or pass --patchset explicitely' %
4949 cl.GetIssue())
4950
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004951 # TODO(tandrii): Checking local patchset against remote patchset is only
4952 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4953 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004954 print('Warning: Codereview server has newer patchsets (%s) than most '
4955 'recent upload from local checkout (%s). Did a previous upload '
4956 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004957 'By default, git cl try-results uses the latest patchset from '
4958 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004959 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004960 try:
tandrii221ab252016-10-06 08:12:04 -07004961 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004962 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004963 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004964 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004965 if options.json:
4966 write_try_results_json(options.json, jobs)
4967 else:
4968 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004969 return 0
4970
4971
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004972@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004973def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004974 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004975 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004976 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004977 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004978
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004979 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004980 if args:
4981 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004982 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004983 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004984 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004985 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004986
4987 # Clear configured merge-base, if there is one.
4988 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004989 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004990 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004991 return 0
4992
4993
thestig@chromium.org00858c82013-12-02 23:08:03 +00004994def CMDweb(parser, args):
4995 """Opens the current CL in the web browser."""
4996 _, args = parser.parse_args(args)
4997 if args:
4998 parser.error('Unrecognized args: %s' % ' '.join(args))
4999
5000 issue_url = Changelist().GetIssueURL()
5001 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005002 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005003 return 1
5004
5005 webbrowser.open(issue_url)
5006 return 0
5007
5008
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005009def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005010 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005011 parser.add_option('-d', '--dry-run', action='store_true',
5012 help='trigger in dry run mode')
5013 parser.add_option('-c', '--clear', action='store_true',
5014 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005015 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005016 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005017 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005018 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005019 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005020 if args:
5021 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005022 if options.dry_run and options.clear:
5023 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5024
iannuccie53c9352016-08-17 14:40:40 -07005025 cl = Changelist(auth_config=auth_config, issue=options.issue,
5026 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005027 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005028 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005029 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005030 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005031 state = _CQState.DRY_RUN
5032 else:
5033 state = _CQState.COMMIT
5034 if not cl.GetIssue():
5035 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005036 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005037 return 0
5038
5039
groby@chromium.org411034a2013-02-26 15:12:01 +00005040def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005041 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005042 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005043 auth.add_auth_options(parser)
5044 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005045 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005046 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005047 if args:
5048 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005049 cl = Changelist(auth_config=auth_config, issue=options.issue,
5050 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005051 # Ensure there actually is an issue to close.
5052 cl.GetDescription()
5053 cl.CloseIssue()
5054 return 0
5055
5056
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005057def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005058 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005059 parser.add_option(
5060 '--stat',
5061 action='store_true',
5062 dest='stat',
5063 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005064 auth.add_auth_options(parser)
5065 options, args = parser.parse_args(args)
5066 auth_config = auth.extract_auth_config_from_options(options)
5067 if args:
5068 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005069
5070 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005071 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005072 # Staged changes would be committed along with the patch from last
5073 # upload, hence counted toward the "last upload" side in the final
5074 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005075 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005076 return 1
5077
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005078 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005079 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005080 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005081 if not issue:
5082 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005083 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005084 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005085
5086 # Create a new branch based on the merge-base
5087 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005088 # Clear cached branch in cl object, to avoid overwriting original CL branch
5089 # properties.
5090 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005091 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005092 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005093 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005094 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005095 return rtn
5096
wychen@chromium.org06928532015-02-03 02:11:29 +00005097 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005098 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005099 cmd = ['git', 'diff']
5100 if options.stat:
5101 cmd.append('--stat')
5102 cmd.extend([TMP_BRANCH, branch, '--'])
5103 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005104 finally:
5105 RunGit(['checkout', '-q', branch])
5106 RunGit(['branch', '-D', TMP_BRANCH])
5107
5108 return 0
5109
5110
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005111def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005112 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005113 parser.add_option(
5114 '--no-color',
5115 action='store_true',
5116 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005117 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005118 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005119 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005120
5121 author = RunGit(['config', 'user.email']).strip() or None
5122
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005123 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005124
5125 if args:
5126 if len(args) > 1:
5127 parser.error('Unknown args')
5128 base_branch = args[0]
5129 else:
5130 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005131 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005132
5133 change = cl.GetChange(base_branch, None)
5134 return owners_finder.OwnersFinder(
5135 [f.LocalPath() for f in
5136 cl.GetChange(base_branch, None).AffectedFiles()],
5137 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005138 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005139 disable_color=options.no_color).run()
5140
5141
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005142def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005143 """Generates a diff command."""
5144 # Generate diff for the current branch's changes.
5145 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005146 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005147
5148 if args:
5149 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005150 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005151 diff_cmd.append(arg)
5152 else:
5153 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005154
5155 return diff_cmd
5156
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005157
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005158def MatchingFileType(file_name, extensions):
5159 """Returns true if the file name ends with one of the given extensions."""
5160 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005161
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005162
enne@chromium.org555cfe42014-01-29 18:21:39 +00005163@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005164def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005165 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005166 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005167 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005168 parser.add_option('--full', action='store_true',
5169 help='Reformat the full content of all touched files')
5170 parser.add_option('--dry-run', action='store_true',
5171 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005172 parser.add_option('--python', action='store_true',
5173 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005174 parser.add_option('--js', action='store_true',
5175 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005176 parser.add_option('--diff', action='store_true',
5177 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005178 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005179
Daniel Chengc55eecf2016-12-30 03:11:02 -08005180 # Normalize any remaining args against the current path, so paths relative to
5181 # the current directory are still resolved as expected.
5182 args = [os.path.join(os.getcwd(), arg) for arg in args]
5183
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005184 # git diff generates paths against the root of the repository. Change
5185 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005186 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005187 if rel_base_path:
5188 os.chdir(rel_base_path)
5189
digit@chromium.org29e47272013-05-17 17:01:46 +00005190 # Grab the merge-base commit, i.e. the upstream commit of the current
5191 # branch when it was created or the last time it was rebased. This is
5192 # to cover the case where the user may have called "git fetch origin",
5193 # moving the origin branch to a newer commit, but hasn't rebased yet.
5194 upstream_commit = None
5195 cl = Changelist()
5196 upstream_branch = cl.GetUpstreamBranch()
5197 if upstream_branch:
5198 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5199 upstream_commit = upstream_commit.strip()
5200
5201 if not upstream_commit:
5202 DieWithError('Could not find base commit for this branch. '
5203 'Are you in detached state?')
5204
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005205 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5206 diff_output = RunGit(changed_files_cmd)
5207 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005208 # Filter out files deleted by this CL
5209 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005210
Christopher Lamc5ba6922017-01-24 11:19:14 +11005211 if opts.js:
5212 CLANG_EXTS.append('.js')
5213
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005214 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5215 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5216 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005217 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005218
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005219 top_dir = os.path.normpath(
5220 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5221
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005222 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5223 # formatted. This is used to block during the presubmit.
5224 return_value = 0
5225
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005226 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005227 # Locate the clang-format binary in the checkout
5228 try:
5229 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005230 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005231 DieWithError(e)
5232
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005233 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005234 cmd = [clang_format_tool]
5235 if not opts.dry_run and not opts.diff:
5236 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005237 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005238 if opts.diff:
5239 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005240 else:
5241 env = os.environ.copy()
5242 env['PATH'] = str(os.path.dirname(clang_format_tool))
5243 try:
5244 script = clang_format.FindClangFormatScriptInChromiumTree(
5245 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005246 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005247 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005248
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005249 cmd = [sys.executable, script, '-p0']
5250 if not opts.dry_run and not opts.diff:
5251 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005252
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005253 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5254 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005255
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005256 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5257 if opts.diff:
5258 sys.stdout.write(stdout)
5259 if opts.dry_run and len(stdout) > 0:
5260 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005261
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005262 # Similar code to above, but using yapf on .py files rather than clang-format
5263 # on C/C++ files
5264 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005265 yapf_tool = gclient_utils.FindExecutable('yapf')
5266 if yapf_tool is None:
5267 DieWithError('yapf not found in PATH')
5268
5269 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005270 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005271 cmd = [yapf_tool]
5272 if not opts.dry_run and not opts.diff:
5273 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005274 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005275 if opts.diff:
5276 sys.stdout.write(stdout)
5277 else:
5278 # TODO(sbc): yapf --lines mode still has some issues.
5279 # https://github.com/google/yapf/issues/154
5280 DieWithError('--python currently only works with --full')
5281
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005282 # Dart's formatter does not have the nice property of only operating on
5283 # modified chunks, so hard code full.
5284 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005285 try:
5286 command = [dart_format.FindDartFmtToolInChromiumTree()]
5287 if not opts.dry_run and not opts.diff:
5288 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005289 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005290
ppi@chromium.org6593d932016-03-03 15:41:15 +00005291 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005292 if opts.dry_run and stdout:
5293 return_value = 2
5294 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005295 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5296 'found in this checkout. Files in other languages are still '
5297 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005298
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005299 # Format GN build files. Always run on full build files for canonical form.
5300 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005301 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005302 if opts.dry_run or opts.diff:
5303 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005304 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005305 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5306 shell=sys.platform == 'win32',
5307 cwd=top_dir)
5308 if opts.dry_run and gn_ret == 2:
5309 return_value = 2 # Not formatted.
5310 elif opts.diff and gn_ret == 2:
5311 # TODO this should compute and print the actual diff.
5312 print("This change has GN build file diff for " + gn_diff_file)
5313 elif gn_ret != 0:
5314 # For non-dry run cases (and non-2 return values for dry-run), a
5315 # nonzero error code indicates a failure, probably because the file
5316 # doesn't parse.
5317 DieWithError("gn format failed on " + gn_diff_file +
5318 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005319
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005320 metrics_xml_files = [
5321 'tools/metrics/actions/actions.xml',
5322 'tools/metrics/histograms/histograms.xml',
5323 'tools/metrics/rappor/rappor.xml']
5324 for xml_file in metrics_xml_files:
5325 if xml_file in diff_files:
5326 tool_dir = top_dir + '/' + os.path.dirname(xml_file)
5327 cmd = [tool_dir + '/pretty_print.py', '--non-interactive']
5328 if opts.dry_run or opts.diff:
5329 cmd.append('--diff')
5330 stdout = RunCommand(cmd, cwd=top_dir)
5331 if opts.diff:
5332 sys.stdout.write(stdout)
5333 if opts.dry_run and stdout:
5334 return_value = 2 # Not formatted.
5335
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005336 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005337
5338
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005339@subcommand.usage('<codereview url or issue id>')
5340def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005341 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005342 _, args = parser.parse_args(args)
5343
5344 if len(args) != 1:
5345 parser.print_help()
5346 return 1
5347
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005348 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005349 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005350 parser.print_help()
5351 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005352 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005353
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005354 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005355 output = RunGit(['config', '--local', '--get-regexp',
5356 r'branch\..*\.%s' % issueprefix],
5357 error_ok=True)
5358 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005359 if issue == target_issue:
5360 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005361
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005362 branches = []
5363 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005364 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005365 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005366 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005367 return 1
5368 if len(branches) == 1:
5369 RunGit(['checkout', branches[0]])
5370 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005371 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005372 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005373 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005374 which = raw_input('Choose by index: ')
5375 try:
5376 RunGit(['checkout', branches[int(which)]])
5377 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005378 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005379 return 1
5380
5381 return 0
5382
5383
maruel@chromium.org29404b52014-09-08 22:58:00 +00005384def CMDlol(parser, args):
5385 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005386 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005387 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5388 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5389 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005390 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005391 return 0
5392
5393
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005394class OptionParser(optparse.OptionParser):
5395 """Creates the option parse and add --verbose support."""
5396 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005397 optparse.OptionParser.__init__(
5398 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005399 self.add_option(
5400 '-v', '--verbose', action='count', default=0,
5401 help='Use 2 times for more debugging info')
5402
5403 def parse_args(self, args=None, values=None):
5404 options, args = optparse.OptionParser.parse_args(self, args, values)
5405 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005406 logging.basicConfig(
5407 level=levels[min(options.verbose, len(levels) - 1)],
5408 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5409 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005410 return options, args
5411
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005412
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005413def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005414 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005415 print('\nYour python version %s is unsupported, please upgrade.\n' %
5416 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005417 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005418
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005419 # Reload settings.
5420 global settings
5421 settings = Settings()
5422
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005423 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005424 dispatcher = subcommand.CommandDispatcher(__name__)
5425 try:
5426 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005427 except auth.AuthenticationError as e:
5428 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005429 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005430 if e.code != 500:
5431 raise
5432 DieWithError(
5433 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5434 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005435 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005436
5437
5438if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005439 # These affect sys.stdout so do it outside of main() to simplify mocks in
5440 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005441 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005442 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005443 try:
5444 sys.exit(main(sys.argv[1:]))
5445 except KeyboardInterrupt:
5446 sys.stderr.write('interrupted\n')
5447 sys.exit(1)