blob: e90b93f5c3563d532c2856acbe97072aa2b1beb5 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002# Copyright (c) 2013 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 Shyshkalovd8aa49f2017-03-17 16:05:49 +010017import datetime
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010018import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000019import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010020import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000021import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000023import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import optparse
25import os
26import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010027import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000028import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070030import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031import textwrap
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000034import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000035import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000036import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000037import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038
39try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080040 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000041except ImportError:
42 pass
43
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000044from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000045from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000046from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000047import auth
skobes6468b902016-10-24 08:45:10 -070048import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000049import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000050import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000051import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000052import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000053import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000054import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000055import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000057import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000058import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000059import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000061import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040063import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000064import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000065import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000066import watchlists
67
tandrii7400cf02016-06-21 08:48:07 -070068__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000069
tandrii9d2c7a32016-06-22 03:42:45 -070070COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070071DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080072POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000074REFS_THAT_ALIAS_TO_OTHER_REFS = {
75 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
76 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
77}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000078
thestig@chromium.org44202a22014-03-11 19:22:18 +000079# Valid extensions for files we want to lint.
80DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
81DEFAULT_LINT_IGNORE_REGEX = r"$^"
82
borenet6c0efe62016-10-19 08:13:29 -070083# Buildbucket master name prefix.
84MASTER_PREFIX = 'master.'
85
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000086# Shortcut since it quickly becomes redundant.
87Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000088
maruel@chromium.orgddd59412011-11-30 14:20:38 +000089# Initialized in main()
90settings = None
91
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010092# Used by tests/git_cl_test.py to add extra logging.
93# Inside the weirdly failing test, add this:
94# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -070095# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010096_IS_BEING_TESTED = False
97
maruel@chromium.orgddd59412011-11-30 14:20:38 +000098
Christopher Lamf732cd52017-01-24 12:40:11 +110099def DieWithError(message, change_desc=None):
100 if change_desc:
101 SaveDescriptionBackup(change_desc)
102
vapiera7fbd5a2016-06-16 09:17:49 -0700103 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 sys.exit(1)
105
106
Christopher Lamf732cd52017-01-24 12:40:11 +1100107def SaveDescriptionBackup(change_desc):
108 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
109 print('\nError after CL description prompt -- saving description to %s\n' %
110 backup_path)
111 backup_file = open(backup_path, 'w')
112 backup_file.write(change_desc.description)
113 backup_file.close()
114
115
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000116def GetNoGitPagerEnv():
117 env = os.environ.copy()
118 # 'cat' is a magical git string that disables pagers on all platforms.
119 env['GIT_PAGER'] = 'cat'
120 return env
121
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000122
bsep@chromium.org627d9002016-04-29 00:00:52 +0000123def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000124 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000125 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000126 except subprocess2.CalledProcessError as e:
127 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000128 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000129 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000130 'Command "%s" failed.\n%s' % (
131 ' '.join(args), error_message or e.stdout or ''))
132 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133
134
135def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000136 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000137 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000138
139
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000140def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000141 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700142 if suppress_stderr:
143 stderr = subprocess2.VOID
144 else:
145 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000146 try:
tandrii5d48c322016-08-18 16:19:37 -0700147 (out, _), code = subprocess2.communicate(['git'] + args,
148 env=GetNoGitPagerEnv(),
149 stdout=subprocess2.PIPE,
150 stderr=stderr)
151 return code, out
152 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900153 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700154 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000155
156
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000157def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000158 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000159 return RunGitWithCode(args, suppress_stderr=True)[1]
160
161
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000162def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000163 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000164 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000165 return (version.startswith(prefix) and
166 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000167
168
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000169def BranchExists(branch):
170 """Return True if specified branch exists."""
171 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
172 suppress_stderr=True)
173 return not code
174
175
tandrii2a16b952016-10-19 07:09:44 -0700176def time_sleep(seconds):
177 # Use this so that it can be mocked in tests without interfering with python
178 # system machinery.
179 import time # Local import to discourage others from importing time globally.
180 return time.sleep(seconds)
181
182
maruel@chromium.org90541732011-04-01 17:54:18 +0000183def ask_for_data(prompt):
184 try:
185 return raw_input(prompt)
186 except KeyboardInterrupt:
187 # Hide the exception.
188 sys.exit(1)
189
190
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100191def confirm_or_exit(prefix='', action='confirm'):
192 """Asks user to press enter to continue or press Ctrl+C to abort."""
193 if not prefix or prefix.endswith('\n'):
194 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100195 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100196 mid = ' Press'
197 elif prefix.endswith(' '):
198 mid = 'press'
199 else:
200 mid = ' press'
201 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
202
203
204def ask_for_explicit_yes(prompt):
205 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
206 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
207 while True:
208 if 'yes'.startswith(result):
209 return True
210 if 'no'.startswith(result):
211 return False
212 result = ask_for_data('Please, type yes or no: ').lower()
213
214
tandrii5d48c322016-08-18 16:19:37 -0700215def _git_branch_config_key(branch, key):
216 """Helper method to return Git config key for a branch."""
217 assert branch, 'branch name is required to set git config for it'
218 return 'branch.%s.%s' % (branch, key)
219
220
221def _git_get_branch_config_value(key, default=None, value_type=str,
222 branch=False):
223 """Returns git config value of given or current branch if any.
224
225 Returns default in all other cases.
226 """
227 assert value_type in (int, str, bool)
228 if branch is False: # Distinguishing default arg value from None.
229 branch = GetCurrentBranch()
230
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000231 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700232 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000233
tandrii5d48c322016-08-18 16:19:37 -0700234 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700235 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700236 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700237 # git config also has --int, but apparently git config suffers from integer
238 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700239 args.append(_git_branch_config_key(branch, key))
240 code, out = RunGitWithCode(args)
241 if code == 0:
242 value = out.strip()
243 if value_type == int:
244 return int(value)
245 if value_type == bool:
246 return bool(value.lower() == 'true')
247 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000248 return default
249
250
tandrii5d48c322016-08-18 16:19:37 -0700251def _git_set_branch_config_value(key, value, branch=None, **kwargs):
252 """Sets the value or unsets if it's None of a git branch config.
253
254 Valid, though not necessarily existing, branch must be provided,
255 otherwise currently checked out branch is used.
256 """
257 if not branch:
258 branch = GetCurrentBranch()
259 assert branch, 'a branch name OR currently checked out branch is required'
260 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700261 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700262 if value is None:
263 args.append('--unset')
264 elif isinstance(value, bool):
265 args.append('--bool')
266 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700267 else:
tandrii33a46ff2016-08-23 05:53:40 -0700268 # git config also has --int, but apparently git config suffers from integer
269 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700270 value = str(value)
271 args.append(_git_branch_config_key(branch, key))
272 if value is not None:
273 args.append(value)
274 RunGit(args, **kwargs)
275
276
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100277def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700278 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100279
280 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
281 """
282 # Git also stores timezone offset, but it only affects visual display,
283 # actual point in time is defined by this timestamp only.
284 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
285
286
287def _git_amend_head(message, committer_timestamp):
288 """Amends commit with new message and desired committer_timestamp.
289
290 Sets committer timezone to UTC.
291 """
292 env = os.environ.copy()
293 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
294 return RunGit(['commit', '--amend', '-m', message], env=env)
295
296
machenbach@chromium.org45453142015-09-15 08:45:22 +0000297def _get_properties_from_options(options):
298 properties = dict(x.split('=', 1) for x in options.properties)
299 for key, val in properties.iteritems():
300 try:
301 properties[key] = json.loads(val)
302 except ValueError:
303 pass # If a value couldn't be evaluated, treat it as a string.
304 return properties
305
306
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000307def _prefix_master(master):
308 """Convert user-specified master name to full master name.
309
310 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
311 name, while the developers always use shortened master name
312 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
313 function does the conversion for buildbucket migration.
314 """
borenet6c0efe62016-10-19 08:13:29 -0700315 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000316 return master
borenet6c0efe62016-10-19 08:13:29 -0700317 return '%s%s' % (MASTER_PREFIX, master)
318
319
320def _unprefix_master(bucket):
321 """Convert bucket name to shortened master name.
322
323 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
324 name, while the developers always use shortened master name
325 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
326 function does the conversion for buildbucket migration.
327 """
328 if bucket.startswith(MASTER_PREFIX):
329 return bucket[len(MASTER_PREFIX):]
330 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000331
332
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000333def _buildbucket_retry(operation_name, http, *args, **kwargs):
334 """Retries requests to buildbucket service and returns parsed json content."""
335 try_count = 0
336 while True:
337 response, content = http.request(*args, **kwargs)
338 try:
339 content_json = json.loads(content)
340 except ValueError:
341 content_json = None
342
343 # Buildbucket could return an error even if status==200.
344 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000345 error = content_json.get('error')
346 if error.get('code') == 403:
347 raise BuildbucketResponseException(
348 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000349 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000350 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000351 raise BuildbucketResponseException(msg)
352
353 if response.status == 200:
354 if not content_json:
355 raise BuildbucketResponseException(
356 'Buildbucket returns invalid json content: %s.\n'
357 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
358 content)
359 return content_json
360 if response.status < 500 or try_count >= 2:
361 raise httplib2.HttpLib2Error(content)
362
363 # status >= 500 means transient failures.
364 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700365 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000366 try_count += 1
367 assert False, 'unreachable'
368
369
qyearsley1fdfcb62016-10-24 13:22:03 -0700370def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700371 """Returns a dict mapping bucket names to builders and tests,
372 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700373 """
qyearsleydd49f942016-10-28 11:57:22 -0700374 # If no bots are listed, we try to get a set of builders and tests based
375 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700376 if not options.bot:
377 change = changelist.GetChange(
378 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700379 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700380 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700381 change=change,
382 changed_files=change.LocalPaths(),
383 repository_root=settings.GetRoot(),
384 default_presubmit=None,
385 project=None,
386 verbose=options.verbose,
387 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700388 if masters is None:
389 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100390 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700391
qyearsley1fdfcb62016-10-24 13:22:03 -0700392 if options.bucket:
393 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700394 if options.master:
395 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700396
qyearsleydd49f942016-10-28 11:57:22 -0700397 # If bots are listed but no master or bucket, then we need to find out
398 # the corresponding master for each bot.
399 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
400 if error_message:
401 option_parser.error(
402 'Tryserver master cannot be found because: %s\n'
403 'Please manually specify the tryserver master, e.g. '
404 '"-m tryserver.chromium.linux".' % error_message)
405 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700406
407
qyearsley123a4682016-10-26 09:12:17 -0700408def _get_bucket_map_for_builders(builders):
409 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700410 map_url = 'https://builders-map.appspot.com/'
411 try:
qyearsley123a4682016-10-26 09:12:17 -0700412 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700413 except urllib2.URLError as e:
414 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
415 (map_url, e))
416 except ValueError as e:
417 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700418 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700419 return None, 'Failed to build master map.'
420
qyearsley123a4682016-10-26 09:12:17 -0700421 bucket_map = {}
422 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800423 bucket = builders_map.get(builder, {}).get('bucket')
424 if bucket:
425 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700426 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700427
428
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800429def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700430 """Sends a request to Buildbucket to trigger try jobs for a changelist.
431
432 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700433 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700434 changelist: Changelist that the try jobs are associated with.
435 buckets: A nested dict mapping bucket names to builders to tests.
436 options: Command-line options.
437 """
tandriide281ae2016-10-12 06:02:30 -0700438 assert changelist.GetIssue(), 'CL must be uploaded first'
439 codereview_url = changelist.GetCodereviewServer()
440 assert codereview_url, 'CL must be uploaded first'
441 patchset = patchset or changelist.GetMostRecentPatchset()
442 assert patchset, 'CL must be uploaded first'
443
444 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700445 # Cache the buildbucket credentials under the codereview host key, so that
446 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700447 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000448 http = authenticator.authorize(httplib2.Http())
449 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700450
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000451 buildbucket_put_url = (
452 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000453 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700454 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
455 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
456 hostname=codereview_host,
457 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000458 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700459
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700460 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800461 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700462 if options.clobber:
463 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700464 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700465 if extra_properties:
466 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000467
468 batch_req_body = {'builds': []}
469 print_text = []
470 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700471 for bucket, builders_and_tests in sorted(buckets.iteritems()):
472 print_text.append('Bucket: %s' % bucket)
473 master = None
474 if bucket.startswith(MASTER_PREFIX):
475 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000476 for builder, tests in sorted(builders_and_tests.iteritems()):
477 print_text.append(' %s: %s' % (builder, tests))
478 parameters = {
479 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000480 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100481 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000482 'revision': options.revision,
483 }],
tandrii8c5a3532016-11-04 07:52:02 -0700484 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000485 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000486 if 'presubmit' in builder.lower():
487 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000488 if tests:
489 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700490
491 tags = [
492 'builder:%s' % builder,
493 'buildset:%s' % buildset,
494 'user_agent:git_cl_try',
495 ]
496 if master:
497 parameters['properties']['master'] = master
498 tags.append('master:%s' % master)
499
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000500 batch_req_body['builds'].append(
501 {
502 'bucket': bucket,
503 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000504 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700505 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000506 }
507 )
508
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000509 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700510 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000511 http,
512 buildbucket_put_url,
513 'PUT',
514 body=json.dumps(batch_req_body),
515 headers={'Content-Type': 'application/json'}
516 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000517 print_text.append('To see results here, run: git cl try-results')
518 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700519 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000520
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000521
tandrii221ab252016-10-06 08:12:04 -0700522def fetch_try_jobs(auth_config, changelist, buildbucket_host,
523 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700524 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000525
qyearsley53f48a12016-09-01 10:45:13 -0700526 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000527 """
tandrii221ab252016-10-06 08:12:04 -0700528 assert buildbucket_host
529 assert changelist.GetIssue(), 'CL must be uploaded first'
530 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
531 patchset = patchset or changelist.GetMostRecentPatchset()
532 assert patchset, 'CL must be uploaded first'
533
534 codereview_url = changelist.GetCodereviewServer()
535 codereview_host = urlparse.urlparse(codereview_url).hostname
536 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000537 if authenticator.has_cached_credentials():
538 http = authenticator.authorize(httplib2.Http())
539 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700540 print('Warning: Some results might be missing because %s' %
541 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700542 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000543 http = httplib2.Http()
544
545 http.force_exception_to_status_code = True
546
tandrii221ab252016-10-06 08:12:04 -0700547 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
548 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
549 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000550 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700551 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000552 params = {'tag': 'buildset:%s' % buildset}
553
554 builds = {}
555 while True:
556 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700557 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000558 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700559 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000560 for build in content.get('builds', []):
561 builds[build['id']] = build
562 if 'next_cursor' in content:
563 params['start_cursor'] = content['next_cursor']
564 else:
565 break
566 return builds
567
568
qyearsleyeab3c042016-08-24 09:18:28 -0700569def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000570 """Prints nicely result of fetch_try_jobs."""
571 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700572 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000573 return
574
575 # Make a copy, because we'll be modifying builds dictionary.
576 builds = builds.copy()
577 builder_names_cache = {}
578
579 def get_builder(b):
580 try:
581 return builder_names_cache[b['id']]
582 except KeyError:
583 try:
584 parameters = json.loads(b['parameters_json'])
585 name = parameters['builder_name']
586 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700587 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700588 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000589 name = None
590 builder_names_cache[b['id']] = name
591 return name
592
593 def get_bucket(b):
594 bucket = b['bucket']
595 if bucket.startswith('master.'):
596 return bucket[len('master.'):]
597 return bucket
598
599 if options.print_master:
600 name_fmt = '%%-%ds %%-%ds' % (
601 max(len(str(get_bucket(b))) for b in builds.itervalues()),
602 max(len(str(get_builder(b))) for b in builds.itervalues()))
603 def get_name(b):
604 return name_fmt % (get_bucket(b), get_builder(b))
605 else:
606 name_fmt = '%%-%ds' % (
607 max(len(str(get_builder(b))) for b in builds.itervalues()))
608 def get_name(b):
609 return name_fmt % get_builder(b)
610
611 def sort_key(b):
612 return b['status'], b.get('result'), get_name(b), b.get('url')
613
614 def pop(title, f, color=None, **kwargs):
615 """Pop matching builds from `builds` dict and print them."""
616
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000617 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000618 colorize = str
619 else:
620 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
621
622 result = []
623 for b in builds.values():
624 if all(b.get(k) == v for k, v in kwargs.iteritems()):
625 builds.pop(b['id'])
626 result.append(b)
627 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700628 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000629 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700630 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000631
632 total = len(builds)
633 pop(status='COMPLETED', result='SUCCESS',
634 title='Successes:', color=Fore.GREEN,
635 f=lambda b: (get_name(b), b.get('url')))
636 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
637 title='Infra Failures:', color=Fore.MAGENTA,
638 f=lambda b: (get_name(b), b.get('url')))
639 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
640 title='Failures:', color=Fore.RED,
641 f=lambda b: (get_name(b), b.get('url')))
642 pop(status='COMPLETED', result='CANCELED',
643 title='Canceled:', color=Fore.MAGENTA,
644 f=lambda b: (get_name(b),))
645 pop(status='COMPLETED', result='FAILURE',
646 failure_reason='INVALID_BUILD_DEFINITION',
647 title='Wrong master/builder name:', color=Fore.MAGENTA,
648 f=lambda b: (get_name(b),))
649 pop(status='COMPLETED', result='FAILURE',
650 title='Other failures:',
651 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
652 pop(status='COMPLETED',
653 title='Other finished:',
654 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
655 pop(status='STARTED',
656 title='Started:', color=Fore.YELLOW,
657 f=lambda b: (get_name(b), b.get('url')))
658 pop(status='SCHEDULED',
659 title='Scheduled:',
660 f=lambda b: (get_name(b), 'id=%s' % b['id']))
661 # The last section is just in case buildbucket API changes OR there is a bug.
662 pop(title='Other:',
663 f=lambda b: (get_name(b), 'id=%s' % b['id']))
664 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700665 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000666
667
qyearsley53f48a12016-09-01 10:45:13 -0700668def write_try_results_json(output_file, builds):
669 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
670
671 The input |builds| dict is assumed to be generated by Buildbucket.
672 Buildbucket documentation: http://goo.gl/G0s101
673 """
674
675 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800676 """Extracts some of the information from one build dict."""
677 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700678 return {
679 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700680 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800681 'builder_name': parameters.get('builder_name'),
682 'created_ts': build.get('created_ts'),
683 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700684 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800685 'result': build.get('result'),
686 'status': build.get('status'),
687 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700688 'url': build.get('url'),
689 }
690
691 converted = []
692 for _, build in sorted(builds.items()):
693 converted.append(convert_build_dict(build))
694 write_json(output_file, converted)
695
696
Aaron Gable13101a62018-02-09 13:20:41 -0800697def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000698 """Prints statistics about the change to the user."""
699 # --no-ext-diff is broken in some versions of Git, so try to work around
700 # this by overriding the environment (but there is still a problem if the
701 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000702 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000703 if 'GIT_EXTERNAL_DIFF' in env:
704 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000705
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000706 try:
707 stdout = sys.stdout.fileno()
708 except AttributeError:
709 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000710 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800711 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000712 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000713
714
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000715class BuildbucketResponseException(Exception):
716 pass
717
718
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719class Settings(object):
720 def __init__(self):
721 self.default_server = None
722 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000723 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000724 self.tree_status_url = None
725 self.viewvc_url = None
726 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000727 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000728 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000729 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000730 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000731 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000732 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000733
734 def LazyUpdateIfNeeded(self):
735 """Updates the settings from a codereview.settings file, if available."""
736 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000737 # The only value that actually changes the behavior is
738 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000739 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000740 error_ok=True
741 ).strip().lower()
742
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000744 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000745 LoadCodereviewSettingsFromFile(cr_settings_file)
746 self.updated = True
747
748 def GetDefaultServerUrl(self, error_ok=False):
749 if not self.default_server:
750 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000751 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000752 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000753 if error_ok:
754 return self.default_server
755 if not self.default_server:
756 error_message = ('Could not find settings file. You must configure '
757 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000758 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000759 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000760 return self.default_server
761
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000762 @staticmethod
763 def GetRelativeRoot():
764 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000765
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000766 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000767 if self.root is None:
768 self.root = os.path.abspath(self.GetRelativeRoot())
769 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000770
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000771 def GetGitMirror(self, remote='origin'):
772 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000773 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000774 if not os.path.isdir(local_url):
775 return None
776 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
777 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100778 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100779 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000780 if mirror.exists():
781 return mirror
782 return None
783
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000784 def GetTreeStatusUrl(self, error_ok=False):
785 if not self.tree_status_url:
786 error_message = ('You must configure your tree status URL by running '
787 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000788 self.tree_status_url = self._GetRietveldConfig(
789 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000790 return self.tree_status_url
791
792 def GetViewVCUrl(self):
793 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000794 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795 return self.viewvc_url
796
rmistry@google.com90752582014-01-14 21:04:50 +0000797 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000798 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000799
rmistry@google.com78948ed2015-07-08 23:09:57 +0000800 def GetIsSkipDependencyUpload(self, branch_name):
801 """Returns true if specified branch should skip dep uploads."""
802 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
803 error_ok=True)
804
rmistry@google.com5626a922015-02-26 14:03:30 +0000805 def GetRunPostUploadHook(self):
806 run_post_upload_hook = self._GetRietveldConfig(
807 'run-post-upload-hook', error_ok=True)
808 return run_post_upload_hook == "True"
809
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000810 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000811 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000812
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000813 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000814 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000815
ukai@chromium.orge8077812012-02-03 03:41:46 +0000816 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700817 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000818 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700819 self.is_gerrit = (
820 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000821 return self.is_gerrit
822
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000823 def GetSquashGerritUploads(self):
824 """Return true if uploads to Gerrit should be squashed by default."""
825 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700826 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
827 if self.squash_gerrit_uploads is None:
828 # Default is squash now (http://crbug.com/611892#c23).
829 self.squash_gerrit_uploads = not (
830 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
831 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000832 return self.squash_gerrit_uploads
833
tandriia60502f2016-06-20 02:01:53 -0700834 def GetSquashGerritUploadsOverride(self):
835 """Return True or False if codereview.settings should be overridden.
836
837 Returns None if no override has been defined.
838 """
839 # See also http://crbug.com/611892#c23
840 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
841 error_ok=True).strip()
842 if result == 'true':
843 return True
844 if result == 'false':
845 return False
846 return None
847
tandrii@chromium.org28253532016-04-14 13:46:56 +0000848 def GetGerritSkipEnsureAuthenticated(self):
849 """Return True if EnsureAuthenticated should not be done for Gerrit
850 uploads."""
851 if self.gerrit_skip_ensure_authenticated is None:
852 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000853 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000854 error_ok=True).strip() == 'true')
855 return self.gerrit_skip_ensure_authenticated
856
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000857 def GetGitEditor(self):
858 """Return the editor specified in the git config, or None if none is."""
859 if self.git_editor is None:
860 self.git_editor = self._GetConfig('core.editor', error_ok=True)
861 return self.git_editor or None
862
thestig@chromium.org44202a22014-03-11 19:22:18 +0000863 def GetLintRegex(self):
864 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
865 DEFAULT_LINT_REGEX)
866
867 def GetLintIgnoreRegex(self):
868 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
869 DEFAULT_LINT_IGNORE_REGEX)
870
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000871 def GetProject(self):
872 if not self.project:
873 self.project = self._GetRietveldConfig('project', error_ok=True)
874 return self.project
875
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000876 def _GetRietveldConfig(self, param, **kwargs):
877 return self._GetConfig('rietveld.' + param, **kwargs)
878
rmistry@google.com78948ed2015-07-08 23:09:57 +0000879 def _GetBranchConfig(self, branch_name, param, **kwargs):
880 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
881
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000882 def _GetConfig(self, param, **kwargs):
883 self.LazyUpdateIfNeeded()
884 return RunGit(['config', param], **kwargs).strip()
885
886
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100887@contextlib.contextmanager
888def _get_gerrit_project_config_file(remote_url):
889 """Context manager to fetch and store Gerrit's project.config from
890 refs/meta/config branch and store it in temp file.
891
892 Provides a temporary filename or None if there was error.
893 """
894 error, _ = RunGitWithCode([
895 'fetch', remote_url,
896 '+refs/meta/config:refs/git_cl/meta/config'])
897 if error:
898 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700899 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100900 (remote_url, error))
901 yield None
902 return
903
904 error, project_config_data = RunGitWithCode(
905 ['show', 'refs/git_cl/meta/config:project.config'])
906 if error:
907 print('WARNING: project.config file not found')
908 yield None
909 return
910
911 with gclient_utils.temporary_directory() as tempdir:
912 project_config_file = os.path.join(tempdir, 'project.config')
913 gclient_utils.FileWrite(project_config_file, project_config_data)
914 yield project_config_file
915
916
917def _is_git_numberer_enabled(remote_url, remote_ref):
918 """Returns True if Git Numberer is enabled on this ref."""
919 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100920 KNOWN_PROJECTS_WHITELIST = [
921 'chromium/src',
922 'external/webrtc',
923 'v8/v8',
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +0100924 'infra/experimental',
Edward Lemur32357d32017-09-11 20:22:45 +0200925 # For webrtc.googlesource.com/src.
926 'src',
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100927 ]
928
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100929 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
930 url_parts = urlparse.urlparse(remote_url)
931 project_name = url_parts.path.lstrip('/').rstrip('git./')
932 for known in KNOWN_PROJECTS_WHITELIST:
933 if project_name.endswith(known):
934 break
935 else:
936 # Early exit to avoid extra fetches for repos that aren't using Git
937 # Numberer.
938 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100939
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100940 with _get_gerrit_project_config_file(remote_url) as project_config_file:
941 if project_config_file is None:
942 # Failed to fetch project.config, which shouldn't happen on open source
943 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100944 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100945 def get_opts(x):
946 code, out = RunGitWithCode(
947 ['config', '-f', project_config_file, '--get-all',
948 'plugin.git-numberer.validate-%s-refglob' % x])
949 if code == 0:
950 return out.strip().splitlines()
951 return []
952 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100953
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100954 logging.info('validator config enabled %s disabled %s refglobs for '
955 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000956
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100957 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100958 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100959 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100960 return True
961 return False
962
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100963 if match_refglobs(disabled):
964 return False
965 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100966
967
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000968def ShortBranchName(branch):
969 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000970 return branch.replace('refs/heads/', '', 1)
971
972
973def GetCurrentBranchRef():
974 """Returns branch ref (e.g., refs/heads/master) or None."""
975 return RunGit(['symbolic-ref', 'HEAD'],
976 stderr=subprocess2.VOID, error_ok=True).strip() or None
977
978
979def GetCurrentBranch():
980 """Returns current branch or None.
981
982 For refs/heads/* branches, returns just last part. For others, full ref.
983 """
984 branchref = GetCurrentBranchRef()
985 if branchref:
986 return ShortBranchName(branchref)
987 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000988
989
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000990class _CQState(object):
991 """Enum for states of CL with respect to Commit Queue."""
992 NONE = 'none'
993 DRY_RUN = 'dry_run'
994 COMMIT = 'commit'
995
996 ALL_STATES = [NONE, DRY_RUN, COMMIT]
997
998
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000999class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001000 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001001 self.issue = issue
1002 self.patchset = patchset
1003 self.hostname = hostname
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001004 assert codereview in (None, 'rietveld', 'gerrit')
1005 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001006
1007 @property
1008 def valid(self):
1009 return self.issue is not None
1010
1011
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001012def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001013 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1014 fail_result = _ParsedIssueNumberArgument()
1015
1016 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001017 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001018 if not arg.startswith('http'):
1019 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001020
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001021 url = gclient_utils.UpgradeToHttps(arg)
1022 try:
1023 parsed_url = urlparse.urlparse(url)
1024 except ValueError:
1025 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001026
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001027 if codereview is not None:
1028 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1029 return parsed or fail_result
1030
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001031 results = {}
1032 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1033 parsed = cls.ParseIssueURL(parsed_url)
1034 if parsed is not None:
1035 results[name] = parsed
1036
1037 if not results:
1038 return fail_result
1039 if len(results) == 1:
1040 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001041
1042 if parsed_url.netloc and parsed_url.netloc.split('.')[0].endswith('-review'):
1043 # This is likely Gerrit.
1044 return results['gerrit']
1045 # Choose Rietveld as before if URL can parsed by either.
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001046 return results['rietveld']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001047
1048
Aaron Gablea45ee112016-11-22 15:14:38 -08001049class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001050 def __init__(self, issue, url):
1051 self.issue = issue
1052 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001053 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001054
1055 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001056 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001057 self.issue, self.url)
1058
1059
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001060_CommentSummary = collections.namedtuple(
1061 '_CommentSummary', ['date', 'message', 'sender',
1062 # TODO(tandrii): these two aren't known in Gerrit.
1063 'approval', 'disapproval'])
1064
1065
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001066class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001067 """Changelist works with one changelist in local branch.
1068
1069 Supports two codereview backends: Rietveld or Gerrit, selected at object
1070 creation.
1071
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001072 Notes:
1073 * Not safe for concurrent multi-{thread,process} use.
1074 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001075 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001076 """
1077
1078 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1079 """Create a new ChangeList instance.
1080
1081 If issue is given, the codereview must be given too.
1082
1083 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1084 Otherwise, it's decided based on current configuration of the local branch,
1085 with default being 'rietveld' for backwards compatibility.
1086 See _load_codereview_impl for more details.
1087
1088 **kwargs will be passed directly to codereview implementation.
1089 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001091 global settings
1092 if not settings:
1093 # Happens when git_cl.py is used as a utility library.
1094 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001095
1096 if issue:
1097 assert codereview, 'codereview must be known, if issue is known'
1098
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099 self.branchref = branchref
1100 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001101 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001102 self.branch = ShortBranchName(self.branchref)
1103 else:
1104 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001105 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001106 self.lookedup_issue = False
1107 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001108 self.has_description = False
1109 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001110 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001111 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001112 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001113 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001114 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001115
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001116 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001117 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001118 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001119 assert self._codereview_impl
1120 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001121
1122 def _load_codereview_impl(self, codereview=None, **kwargs):
1123 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001124 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1125 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1126 self._codereview = codereview
1127 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001128 return
1129
1130 # Automatic selection based on issue number set for a current branch.
1131 # Rietveld takes precedence over Gerrit.
1132 assert not self.issue
1133 # Whether we find issue or not, we are doing the lookup.
1134 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001135 if self.GetBranch():
1136 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1137 issue = _git_get_branch_config_value(
1138 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1139 if issue:
1140 self._codereview = codereview
1141 self._codereview_impl = cls(self, **kwargs)
1142 self.issue = int(issue)
1143 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001144
1145 # No issue is set for this branch, so decide based on repo-wide settings.
1146 return self._load_codereview_impl(
1147 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1148 **kwargs)
1149
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001150 def IsGerrit(self):
1151 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001152
1153 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001154 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001155
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001156 The return value is a string suitable for passing to git cl with the --cc
1157 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001158 """
1159 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001160 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001161 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001162 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1163 return self.cc
1164
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001165 def GetCCListWithoutDefault(self):
1166 """Return the users cc'd on this CL excluding default ones."""
1167 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001168 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001169 return self.cc
1170
Daniel Cheng7227d212017-11-17 08:12:37 -08001171 def ExtendCC(self, more_cc):
1172 """Extends the list of users to cc on this CL based on the changed files."""
1173 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001174
1175 def GetBranch(self):
1176 """Returns the short branch name, e.g. 'master'."""
1177 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001178 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001179 if not branchref:
1180 return None
1181 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001182 self.branch = ShortBranchName(self.branchref)
1183 return self.branch
1184
1185 def GetBranchRef(self):
1186 """Returns the full branch name, e.g. 'refs/heads/master'."""
1187 self.GetBranch() # Poke the lazy loader.
1188 return self.branchref
1189
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001190 def ClearBranch(self):
1191 """Clears cached branch data of this object."""
1192 self.branch = self.branchref = None
1193
tandrii5d48c322016-08-18 16:19:37 -07001194 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1195 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1196 kwargs['branch'] = self.GetBranch()
1197 return _git_get_branch_config_value(key, default, **kwargs)
1198
1199 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1200 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1201 assert self.GetBranch(), (
1202 'this CL must have an associated branch to %sset %s%s' %
1203 ('un' if value is None else '',
1204 key,
1205 '' if value is None else ' to %r' % value))
1206 kwargs['branch'] = self.GetBranch()
1207 return _git_set_branch_config_value(key, value, **kwargs)
1208
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001209 @staticmethod
1210 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001211 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212 e.g. 'origin', 'refs/heads/master'
1213 """
1214 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001215 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1216
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001217 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001218 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001219 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001220 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1221 error_ok=True).strip()
1222 if upstream_branch:
1223 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001224 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001225 # Else, try to guess the origin remote.
1226 remote_branches = RunGit(['branch', '-r']).split()
1227 if 'origin/master' in remote_branches:
1228 # Fall back on origin/master if it exits.
1229 remote = 'origin'
1230 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001231 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001232 DieWithError(
1233 'Unable to determine default branch to diff against.\n'
1234 'Either pass complete "git diff"-style arguments, like\n'
1235 ' git cl upload origin/master\n'
1236 'or verify this branch is set up to track another \n'
1237 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001238
1239 return remote, upstream_branch
1240
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001241 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001242 upstream_branch = self.GetUpstreamBranch()
1243 if not BranchExists(upstream_branch):
1244 DieWithError('The upstream for the current branch (%s) does not exist '
1245 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001246 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001247 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001248
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249 def GetUpstreamBranch(self):
1250 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001251 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001253 upstream_branch = upstream_branch.replace('refs/heads/',
1254 'refs/remotes/%s/' % remote)
1255 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1256 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 self.upstream_branch = upstream_branch
1258 return self.upstream_branch
1259
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001260 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001261 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001262 remote, branch = None, self.GetBranch()
1263 seen_branches = set()
1264 while branch not in seen_branches:
1265 seen_branches.add(branch)
1266 remote, branch = self.FetchUpstreamTuple(branch)
1267 branch = ShortBranchName(branch)
1268 if remote != '.' or branch.startswith('refs/remotes'):
1269 break
1270 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001271 remotes = RunGit(['remote'], error_ok=True).split()
1272 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001273 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001274 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001275 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001276 logging.warn('Could not determine which remote this change is '
1277 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001278 else:
1279 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001280 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001281 branch = 'HEAD'
1282 if branch.startswith('refs/remotes'):
1283 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001284 elif branch.startswith('refs/branch-heads/'):
1285 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001286 else:
1287 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001288 return self._remote
1289
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001290 def GitSanityChecks(self, upstream_git_obj):
1291 """Checks git repo status and ensures diff is from local commits."""
1292
sbc@chromium.org79706062015-01-14 21:18:12 +00001293 if upstream_git_obj is None:
1294 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001295 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001296 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001297 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001298 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001299 return False
1300
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001301 # Verify the commit we're diffing against is in our current branch.
1302 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1303 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1304 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001305 print('ERROR: %s is not in the current branch. You may need to rebase '
1306 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001307 return False
1308
1309 # List the commits inside the diff, and verify they are all local.
1310 commits_in_diff = RunGit(
1311 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1312 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1313 remote_branch = remote_branch.strip()
1314 if code != 0:
1315 _, remote_branch = self.GetRemoteBranch()
1316
1317 commits_in_remote = RunGit(
1318 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1319
1320 common_commits = set(commits_in_diff) & set(commits_in_remote)
1321 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001322 print('ERROR: Your diff contains %d commits already in %s.\n'
1323 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1324 'the diff. If you are using a custom git flow, you can override'
1325 ' the reference used for this check with "git config '
1326 'gitcl.remotebranch <git-ref>".' % (
1327 len(common_commits), remote_branch, upstream_git_obj),
1328 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001329 return False
1330 return True
1331
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001332 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001333 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001334
1335 Returns None if it is not set.
1336 """
tandrii5d48c322016-08-18 16:19:37 -07001337 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001338
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339 def GetRemoteUrl(self):
1340 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1341
1342 Returns None if there is no remote.
1343 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001344 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001345 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1346
1347 # If URL is pointing to a local directory, it is probably a git cache.
1348 if os.path.isdir(url):
1349 url = RunGit(['config', 'remote.%s.url' % remote],
1350 error_ok=True,
1351 cwd=url).strip()
1352 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001353
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001354 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001355 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001356 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001357 self.issue = self._GitGetBranchConfigValue(
1358 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001359 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360 return self.issue
1361
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362 def GetIssueURL(self):
1363 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001364 issue = self.GetIssue()
1365 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001366 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001367 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001368
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001369 def GetDescription(self, pretty=False, force=False):
1370 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001372 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001373 self.has_description = True
1374 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001375 # Set width to 72 columns + 2 space indent.
1376 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001378 lines = self.description.splitlines()
1379 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001380 return self.description
1381
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001382 def GetDescriptionFooters(self):
1383 """Returns (non_footer_lines, footers) for the commit message.
1384
1385 Returns:
1386 non_footer_lines (list(str)) - Simple list of description lines without
1387 any footer. The lines do not contain newlines, nor does the list contain
1388 the empty line between the message and the footers.
1389 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1390 [("Change-Id", "Ideadbeef...."), ...]
1391 """
1392 raw_description = self.GetDescription()
1393 msg_lines, _, footers = git_footers.split_footers(raw_description)
1394 if footers:
1395 msg_lines = msg_lines[:len(msg_lines)-1]
1396 return msg_lines, footers
1397
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001398 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001399 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001400 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001401 self.patchset = self._GitGetBranchConfigValue(
1402 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001403 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001404 return self.patchset
1405
1406 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001407 """Set this branch's patchset. If patchset=0, clears the patchset."""
1408 assert self.GetBranch()
1409 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001410 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001411 else:
1412 self.patchset = int(patchset)
1413 self._GitSetBranchConfigValue(
1414 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001416 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001417 """Set this branch's issue. If issue isn't given, clears the issue."""
1418 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001420 issue = int(issue)
1421 self._GitSetBranchConfigValue(
1422 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001423 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001424 codereview_server = self._codereview_impl.GetCodereviewServer()
1425 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001426 self._GitSetBranchConfigValue(
1427 self._codereview_impl.CodereviewServerConfigKey(),
1428 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001429 else:
tandrii5d48c322016-08-18 16:19:37 -07001430 # Reset all of these just to be clean.
1431 reset_suffixes = [
1432 'last-upload-hash',
1433 self._codereview_impl.IssueConfigKey(),
1434 self._codereview_impl.PatchsetConfigKey(),
1435 self._codereview_impl.CodereviewServerConfigKey(),
1436 ] + self._PostUnsetIssueProperties()
1437 for prop in reset_suffixes:
1438 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001439 msg = RunGit(['log', '-1', '--format=%B']).strip()
1440 if msg and git_footers.get_footer_change_id(msg):
1441 print('WARNING: The change patched into this branch has a Change-Id. '
1442 'Removing it.')
1443 RunGit(['commit', '--amend', '-m',
1444 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001445 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001446 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001447
dnjba1b0f32016-09-02 12:37:42 -07001448 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001449 if not self.GitSanityChecks(upstream_branch):
1450 DieWithError('\nGit sanity check failure')
1451
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001452 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001453 if not root:
1454 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001455 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001456
1457 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001458 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001459 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001460 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001461 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001462 except subprocess2.CalledProcessError:
1463 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001464 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001465 'This branch probably doesn\'t exist anymore. To reset the\n'
1466 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001467 ' git branch --set-upstream-to origin/master %s\n'
1468 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001469 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001470
maruel@chromium.org52424302012-08-29 15:14:30 +00001471 issue = self.GetIssue()
1472 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001473 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001474 description = self.GetDescription()
1475 else:
1476 # If the change was never uploaded, use the log messages of all commits
1477 # up to the branch point, as git cl upload will prefill the description
1478 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001479 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1480 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001481
1482 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001483 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001484 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001485 name,
1486 description,
1487 absroot,
1488 files,
1489 issue,
1490 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001491 author,
1492 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001493
dsansomee2d6fd92016-09-08 00:10:47 -07001494 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001495 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001496 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001497 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001498
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001499 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1500 """Sets the description for this CL remotely.
1501
1502 You can get description_lines and footers with GetDescriptionFooters.
1503
1504 Args:
1505 description_lines (list(str)) - List of CL description lines without
1506 newline characters.
1507 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1508 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1509 `List-Of-Tokens`). It will be case-normalized so that each token is
1510 title-cased.
1511 """
1512 new_description = '\n'.join(description_lines)
1513 if footers:
1514 new_description += '\n'
1515 for k, v in footers:
1516 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1517 if not git_footers.FOOTER_PATTERN.match(foot):
1518 raise ValueError('Invalid footer %r' % foot)
1519 new_description += foot + '\n'
1520 self.UpdateDescription(new_description, force)
1521
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001522 def RunHook(self, committing, may_prompt, verbose, change):
1523 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1524 try:
1525 return presubmit_support.DoPresubmitChecks(change, committing,
1526 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1527 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001528 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001529 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001530 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001531
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001532 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1533 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001534 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1535 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001536 else:
1537 # Assume url.
1538 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1539 urlparse.urlparse(issue_arg))
1540 if not parsed_issue_arg or not parsed_issue_arg.valid:
1541 DieWithError('Failed to parse issue argument "%s". '
1542 'Must be an issue number or a valid URL.' % issue_arg)
1543 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001544 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001545
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001546 def CMDUpload(self, options, git_diff_args, orig_args):
1547 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001548 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001549 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001550 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001551 else:
1552 if self.GetBranch() is None:
1553 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1554
1555 # Default to diffing against common ancestor of upstream branch
1556 base_branch = self.GetCommonAncestorWithUpstream()
1557 git_diff_args = [base_branch, 'HEAD']
1558
Aaron Gablec4c40d12017-05-22 11:49:53 -07001559 # Warn about Rietveld deprecation for initial uploads to Rietveld.
1560 if not self.IsGerrit() and not self.GetIssue():
1561 print('=====================================')
1562 print('NOTICE: Rietveld is being deprecated. '
1563 'You can upload changes to Gerrit with')
1564 print(' git cl upload --gerrit')
1565 print('or set Gerrit to be your default code review tool with')
1566 print(' git config gerrit.host true')
1567 print('=====================================')
1568
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001569 # Fast best-effort checks to abort before running potentially
1570 # expensive hooks if uploading is likely to fail anyway. Passing these
1571 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001572 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001573 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001574
1575 # Apply watchlists on upload.
1576 change = self.GetChange(base_branch, None)
1577 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1578 files = [f.LocalPath() for f in change.AffectedFiles()]
1579 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001580 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001581
1582 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001583 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001584 # Set the reviewer list now so that presubmit checks can access it.
1585 change_description = ChangeDescription(change.FullDescriptionText())
1586 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001587 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001588 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001589 change)
1590 change.SetDescriptionText(change_description.description)
1591 hook_results = self.RunHook(committing=False,
1592 may_prompt=not options.force,
1593 verbose=options.verbose,
1594 change=change)
1595 if not hook_results.should_continue():
1596 return 1
1597 if not options.reviewers and hook_results.reviewers:
1598 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001599 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001600
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001601 # TODO(tandrii): Checking local patchset against remote patchset is only
1602 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1603 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001604 latest_patchset = self.GetMostRecentPatchset()
1605 local_patchset = self.GetPatchset()
1606 if (latest_patchset and local_patchset and
1607 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001608 print('The last upload made from this repository was patchset #%d but '
1609 'the most recent patchset on the server is #%d.'
1610 % (local_patchset, latest_patchset))
1611 print('Uploading will still work, but if you\'ve uploaded to this '
1612 'issue from another machine or branch the patch you\'re '
1613 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001614 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001615
Aaron Gable13101a62018-02-09 13:20:41 -08001616 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001617 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001618 if not ret:
Ravi Mistry31e7d562018-04-02 12:53:57 -04001619 if self.IsGerrit():
1620 self.SetLabels(options.enable_auto_submit, options.use_commit_queue,
1621 options.cq_dry_run);
1622 else:
1623 if options.use_commit_queue:
1624 self.SetCQState(_CQState.COMMIT)
1625 elif options.cq_dry_run:
1626 self.SetCQState(_CQState.DRY_RUN)
tandrii4d0545a2016-07-06 03:56:49 -07001627
tandrii5d48c322016-08-18 16:19:37 -07001628 _git_set_branch_config_value('last-upload-hash',
1629 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001630 # Run post upload hooks, if specified.
1631 if settings.GetRunPostUploadHook():
1632 presubmit_support.DoPostUploadExecuter(
1633 change,
1634 self,
1635 settings.GetRoot(),
1636 options.verbose,
1637 sys.stdout)
1638
1639 # Upload all dependencies if specified.
1640 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001641 print()
1642 print('--dependencies has been specified.')
1643 print('All dependent local branches will be re-uploaded.')
1644 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001645 # Remove the dependencies flag from args so that we do not end up in a
1646 # loop.
1647 orig_args.remove('--dependencies')
1648 ret = upload_branch_deps(self, orig_args)
1649 return ret
1650
Ravi Mistry31e7d562018-04-02 12:53:57 -04001651 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1652 """Sets labels on the change based on the provided flags.
1653
1654 Sets labels if issue is already uploaded and known, else returns without
1655 doing anything.
1656
1657 Args:
1658 enable_auto_submit: Sets Auto-Submit+1 on the change.
1659 use_commit_queue: Sets Commit-Queue+2 on the change.
1660 cq_dry_run: Sets Commit-Queue+1 on the change. Overrides Commit-Queue+2 if
1661 both use_commit_queue and cq_dry_run are true.
1662 """
1663 if not self.GetIssue():
1664 return
1665 try:
1666 self._codereview_impl.SetLabels(enable_auto_submit, use_commit_queue,
1667 cq_dry_run)
1668 return 0
1669 except KeyboardInterrupt:
1670 raise
1671 except:
1672 labels = []
1673 if enable_auto_submit:
1674 labels.append('Auto-Submit')
1675 if use_commit_queue or cq_dry_run:
1676 labels.append('Commit-Queue')
1677 print('WARNING: Failed to set label(s) on your change: %s\n'
1678 'Either:\n'
1679 ' * Your project does not have the above label(s),\n'
1680 ' * You don\'t have permission to set the above label(s),\n'
1681 ' * There\'s a bug in this code (see stack trace below).\n' %
1682 (', '.join(labels)))
1683 # Still raise exception so that stack trace is printed.
1684 raise
1685
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001686 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001687 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001688
1689 Issue must have been already uploaded and known.
1690 """
1691 assert new_state in _CQState.ALL_STATES
1692 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001693 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001694 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001695 return 0
1696 except KeyboardInterrupt:
1697 raise
1698 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001699 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001700 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001701 ' * Your project has no CQ,\n'
1702 ' * You don\'t have permission to change the CQ state,\n'
1703 ' * There\'s a bug in this code (see stack trace below).\n'
1704 'Consider specifying which bots to trigger manually or asking your '
1705 'project owners for permissions or contacting Chrome Infra at:\n'
1706 'https://www.chromium.org/infra\n\n' %
1707 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001708 # Still raise exception so that stack trace is printed.
1709 raise
1710
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001711 # Forward methods to codereview specific implementation.
1712
Aaron Gable636b13f2017-07-14 10:42:48 -07001713 def AddComment(self, message, publish=None):
1714 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001715
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001716 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001717 """Returns list of _CommentSummary for each comment.
1718
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001719 args:
1720 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001721 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001722 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001723
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001724 def CloseIssue(self):
1725 return self._codereview_impl.CloseIssue()
1726
1727 def GetStatus(self):
1728 return self._codereview_impl.GetStatus()
1729
1730 def GetCodereviewServer(self):
1731 return self._codereview_impl.GetCodereviewServer()
1732
tandriide281ae2016-10-12 06:02:30 -07001733 def GetIssueOwner(self):
1734 """Get owner from codereview, which may differ from this checkout."""
1735 return self._codereview_impl.GetIssueOwner()
1736
Edward Lemur707d70b2018-02-07 00:50:14 +01001737 def GetReviewers(self):
1738 return self._codereview_impl.GetReviewers()
1739
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001740 def GetMostRecentPatchset(self):
1741 return self._codereview_impl.GetMostRecentPatchset()
1742
tandriide281ae2016-10-12 06:02:30 -07001743 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001744 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001745 return self._codereview_impl.CannotTriggerTryJobReason()
1746
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001747 def GetTryJobProperties(self, patchset=None):
1748 """Returns dictionary of properties to launch try job."""
1749 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001750
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001751 def __getattr__(self, attr):
1752 # This is because lots of untested code accesses Rietveld-specific stuff
1753 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001754 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001755 # Note that child method defines __getattr__ as well, and forwards it here,
1756 # because _RietveldChangelistImpl is not cleaned up yet, and given
1757 # deprecation of Rietveld, it should probably be just removed.
1758 # Until that time, avoid infinite recursion by bypassing __getattr__
1759 # of implementation class.
1760 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001761
1762
1763class _ChangelistCodereviewBase(object):
1764 """Abstract base class encapsulating codereview specifics of a changelist."""
1765 def __init__(self, changelist):
1766 self._changelist = changelist # instance of Changelist
1767
1768 def __getattr__(self, attr):
1769 # Forward methods to changelist.
1770 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1771 # _RietveldChangelistImpl to avoid this hack?
1772 return getattr(self._changelist, attr)
1773
1774 def GetStatus(self):
1775 """Apply a rough heuristic to give a simple summary of an issue's review
1776 or CQ status, assuming adherence to a common workflow.
1777
1778 Returns None if no issue for this branch, or specific string keywords.
1779 """
1780 raise NotImplementedError()
1781
1782 def GetCodereviewServer(self):
1783 """Returns server URL without end slash, like "https://codereview.com"."""
1784 raise NotImplementedError()
1785
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001786 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001787 """Fetches and returns description from the codereview server."""
1788 raise NotImplementedError()
1789
tandrii5d48c322016-08-18 16:19:37 -07001790 @classmethod
1791 def IssueConfigKey(cls):
1792 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001793 raise NotImplementedError()
1794
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001795 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001796 def PatchsetConfigKey(cls):
1797 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001798 raise NotImplementedError()
1799
tandrii5d48c322016-08-18 16:19:37 -07001800 @classmethod
1801 def CodereviewServerConfigKey(cls):
1802 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001803 raise NotImplementedError()
1804
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001805 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001806 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001807 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001808
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001809 def GetGerritObjForPresubmit(self):
1810 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1811 return None
1812
dsansomee2d6fd92016-09-08 00:10:47 -07001813 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001814 """Update the description on codereview site."""
1815 raise NotImplementedError()
1816
Aaron Gable636b13f2017-07-14 10:42:48 -07001817 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001818 """Posts a comment to the codereview site."""
1819 raise NotImplementedError()
1820
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001821 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001822 raise NotImplementedError()
1823
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001824 def CloseIssue(self):
1825 """Closes the issue."""
1826 raise NotImplementedError()
1827
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001828 def GetMostRecentPatchset(self):
1829 """Returns the most recent patchset number from the codereview site."""
1830 raise NotImplementedError()
1831
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001832 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001833 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001834 """Fetches and applies the issue.
1835
1836 Arguments:
1837 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1838 reject: if True, reject the failed patch instead of switching to 3-way
1839 merge. Rietveld only.
1840 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1841 only.
1842 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001843 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001844 """
1845 raise NotImplementedError()
1846
1847 @staticmethod
1848 def ParseIssueURL(parsed_url):
1849 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1850 failed."""
1851 raise NotImplementedError()
1852
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001853 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001854 """Best effort check that user is authenticated with codereview server.
1855
1856 Arguments:
1857 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001858 refresh: whether to attempt to refresh credentials. Ignored if not
1859 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001860 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001861 raise NotImplementedError()
1862
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001863 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001864 """Best effort check that uploading isn't supposed to fail for predictable
1865 reasons.
1866
1867 This method should raise informative exception if uploading shouldn't
1868 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001869
1870 Arguments:
1871 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001872 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001873 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001874
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001875 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001876 """Uploads a change to codereview."""
1877 raise NotImplementedError()
1878
Ravi Mistry31e7d562018-04-02 12:53:57 -04001879 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1880 """Sets labels on the change based on the provided flags.
1881
1882 Issue must have been already uploaded and known.
1883 """
1884 raise NotImplementedError()
1885
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001886 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001887 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001888
1889 Issue must have been already uploaded and known.
1890 """
1891 raise NotImplementedError()
1892
tandriie113dfd2016-10-11 10:20:12 -07001893 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001894 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001895 raise NotImplementedError()
1896
tandriide281ae2016-10-12 06:02:30 -07001897 def GetIssueOwner(self):
1898 raise NotImplementedError()
1899
Edward Lemur707d70b2018-02-07 00:50:14 +01001900 def GetReviewers(self):
1901 raise NotImplementedError()
1902
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001903 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001904 raise NotImplementedError()
1905
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001906
1907class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001908
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001909 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001910 super(_RietveldChangelistImpl, self).__init__(changelist)
1911 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001912 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001913 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001914
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001915 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001916 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001917 self._props = None
1918 self._rpc_server = None
1919
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001920 def GetCodereviewServer(self):
1921 if not self._rietveld_server:
1922 # If we're on a branch then get the server potentially associated
1923 # with that branch.
1924 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001925 self._rietveld_server = gclient_utils.UpgradeToHttps(
1926 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001927 if not self._rietveld_server:
1928 self._rietveld_server = settings.GetDefaultServerUrl()
1929 return self._rietveld_server
1930
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001931 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001932 """Best effort check that user is authenticated with Rietveld server."""
1933 if self._auth_config.use_oauth2:
1934 authenticator = auth.get_authenticator_for_host(
1935 self.GetCodereviewServer(), self._auth_config)
1936 if not authenticator.has_cached_credentials():
1937 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001938 if refresh:
1939 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001940
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001941 def EnsureCanUploadPatchset(self, force):
1942 # No checks for Rietveld because we are deprecating Rietveld.
1943 pass
1944
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001945 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001946 issue = self.GetIssue()
1947 assert issue
1948 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001949 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001950 except urllib2.HTTPError as e:
1951 if e.code == 404:
1952 DieWithError(
1953 ('\nWhile fetching the description for issue %d, received a '
1954 '404 (not found)\n'
1955 'error. It is likely that you deleted this '
1956 'issue on the server. If this is the\n'
1957 'case, please run\n\n'
1958 ' git cl issue 0\n\n'
1959 'to clear the association with the deleted issue. Then run '
1960 'this command again.') % issue)
1961 else:
1962 DieWithError(
1963 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1964 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001965 print('Warning: Failed to retrieve CL description due to network '
1966 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001967 return ''
1968
1969 def GetMostRecentPatchset(self):
1970 return self.GetIssueProperties()['patchsets'][-1]
1971
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001972 def GetIssueProperties(self):
1973 if self._props is None:
1974 issue = self.GetIssue()
1975 if not issue:
1976 self._props = {}
1977 else:
1978 self._props = self.RpcServer().get_issue_properties(issue, True)
1979 return self._props
1980
tandriie113dfd2016-10-11 10:20:12 -07001981 def CannotTriggerTryJobReason(self):
1982 props = self.GetIssueProperties()
1983 if not props:
1984 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1985 if props.get('closed'):
1986 return 'CL %s is closed' % self.GetIssue()
1987 if props.get('private'):
1988 return 'CL %s is private' % self.GetIssue()
1989 return None
1990
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001991 def GetTryJobProperties(self, patchset=None):
1992 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07001993 project = (self.GetIssueProperties() or {}).get('project')
1994 return {
1995 'issue': self.GetIssue(),
1996 'patch_project': project,
1997 'patch_storage': 'rietveld',
1998 'patchset': patchset or self.GetPatchset(),
1999 'rietveld': self.GetCodereviewServer(),
2000 }
2001
tandriide281ae2016-10-12 06:02:30 -07002002 def GetIssueOwner(self):
2003 return (self.GetIssueProperties() or {}).get('owner_email')
2004
Edward Lemur707d70b2018-02-07 00:50:14 +01002005 def GetReviewers(self):
2006 return (self.GetIssueProperties() or {}).get('reviewers')
2007
Aaron Gable636b13f2017-07-14 10:42:48 -07002008 def AddComment(self, message, publish=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002009 return self.RpcServer().add_comment(self.GetIssue(), message)
2010
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002011 def GetCommentsSummary(self, _readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002012 summary = []
2013 for message in self.GetIssueProperties().get('messages', []):
2014 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
2015 summary.append(_CommentSummary(
2016 date=date,
2017 disapproval=bool(message['disapproval']),
2018 approval=bool(message['approval']),
2019 sender=message['sender'],
2020 message=message['text'],
2021 ))
2022 return summary
2023
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002024 def GetStatus(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002025 """Applies a rough heuristic to give a simple summary of an issue's review
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002026 or CQ status, assuming adherence to a common workflow.
2027
2028 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07002029 * 'error' - error from review tool (including deleted issues)
2030 * 'unsent' - not sent for review
2031 * 'waiting' - waiting for review
2032 * 'reply' - waiting for owner to reply to review
2033 * 'not lgtm' - Code-Review label has been set negatively
2034 * 'lgtm' - LGTM from at least one approved reviewer
2035 * 'commit' - in the commit queue
2036 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002037 """
2038 if not self.GetIssue():
2039 return None
2040
2041 try:
2042 props = self.GetIssueProperties()
2043 except urllib2.HTTPError:
2044 return 'error'
2045
2046 if props.get('closed'):
2047 # Issue is closed.
2048 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002049 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002050 # Issue is in the commit queue.
2051 return 'commit'
2052
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002053 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002054 if not messages:
2055 # No message was sent.
2056 return 'unsent'
2057
2058 if get_approving_reviewers(props):
2059 return 'lgtm'
2060 elif get_approving_reviewers(props, disapproval=True):
2061 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002062
tandrii9d2c7a32016-06-22 03:42:45 -07002063 # Skip CQ messages that don't require owner's action.
2064 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2065 if 'Dry run:' in messages[-1]['text']:
2066 messages.pop()
2067 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2068 # This message always follows prior messages from CQ,
2069 # so skip this too.
2070 messages.pop()
2071 else:
2072 # This is probably a CQ messages warranting user attention.
2073 break
2074
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002075 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002076 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002077 return 'reply'
2078 return 'waiting'
2079
dsansomee2d6fd92016-09-08 00:10:47 -07002080 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002081 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002082
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002083 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002084 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002085
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002086 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002087 return self.SetFlags({flag: value})
2088
2089 def SetFlags(self, flags):
2090 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002091 """
phajdan.jr68598232016-08-10 03:28:28 -07002092 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002093 try:
tandrii4b233bd2016-07-06 03:50:29 -07002094 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002095 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002096 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002097 if e.code == 404:
2098 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2099 if e.code == 403:
2100 DieWithError(
2101 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002102 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002103 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002104
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002105 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002106 """Returns an upload.RpcServer() to access this review's rietveld instance.
2107 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002108 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002109 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002110 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002111 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002112 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002113
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002114 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002115 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002116 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002117
tandrii5d48c322016-08-18 16:19:37 -07002118 @classmethod
2119 def PatchsetConfigKey(cls):
2120 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002121
tandrii5d48c322016-08-18 16:19:37 -07002122 @classmethod
2123 def CodereviewServerConfigKey(cls):
2124 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002125
Ravi Mistry31e7d562018-04-02 12:53:57 -04002126 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
2127 raise NotImplementedError()
2128
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002129 def SetCQState(self, new_state):
2130 props = self.GetIssueProperties()
2131 if props.get('private'):
2132 DieWithError('Cannot set-commit on private issue')
2133
2134 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002135 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002136 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002137 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002138 else:
tandrii4b233bd2016-07-06 03:50:29 -07002139 assert new_state == _CQState.DRY_RUN
2140 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002141
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002142 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002143 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002144 # PatchIssue should never be called with a dirty tree. It is up to the
2145 # caller to check this, but just in case we assert here since the
2146 # consequences of the caller not checking this could be dire.
2147 assert(not git_common.is_dirty_git_tree('apply'))
2148 assert(parsed_issue_arg.valid)
2149 self._changelist.issue = parsed_issue_arg.issue
2150 if parsed_issue_arg.hostname:
2151 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2152
skobes6468b902016-10-24 08:45:10 -07002153 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2154 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2155 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002156 try:
skobes6468b902016-10-24 08:45:10 -07002157 scm_obj.apply_patch(patchset_object)
2158 except Exception as e:
2159 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002160 return 1
2161
2162 # If we had an issue, commit the current state and register the issue.
2163 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002164 self.SetIssue(self.GetIssue())
2165 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002166 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2167 'patch from issue %(i)s at patchset '
2168 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2169 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002170 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002171 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002172 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002173 return 0
2174
2175 @staticmethod
2176 def ParseIssueURL(parsed_url):
2177 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2178 return None
wychen3c1c1722016-08-04 11:46:36 -07002179 # Rietveld patch: https://domain/<number>/#ps<patchset>
2180 match = re.match(r'/(\d+)/$', parsed_url.path)
2181 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2182 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002183 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002184 issue=int(match.group(1)),
2185 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002186 hostname=parsed_url.netloc,
2187 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002188 # Typical url: https://domain/<issue_number>[/[other]]
2189 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2190 if match:
skobes6468b902016-10-24 08:45:10 -07002191 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002192 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002193 hostname=parsed_url.netloc,
2194 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002195 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2196 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2197 if match:
skobes6468b902016-10-24 08:45:10 -07002198 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002199 issue=int(match.group(1)),
2200 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002201 hostname=parsed_url.netloc,
2202 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002203 return None
2204
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002205 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002206 """Upload the patch to Rietveld."""
2207 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2208 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002209 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2210 if options.emulate_svn_auto_props:
2211 upload_args.append('--emulate_svn_auto_props')
2212
2213 change_desc = None
2214
2215 if options.email is not None:
2216 upload_args.extend(['--email', options.email])
2217
2218 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002219 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002220 upload_args.extend(['--title', options.title])
2221 if options.message:
2222 upload_args.extend(['--message', options.message])
2223 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002224 print('This branch is associated with issue %s. '
2225 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002226 else:
nodirca166002016-06-27 10:59:51 -07002227 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002228 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002229 if options.message:
2230 message = options.message
2231 else:
2232 message = CreateDescriptionFromLog(args)
2233 if options.title:
2234 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002235 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002236 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002237 change_desc.update_reviewers(options.reviewers, options.tbrs,
2238 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002239 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002240 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002241
2242 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002243 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002244 return 1
2245
2246 upload_args.extend(['--message', change_desc.description])
2247 if change_desc.get_reviewers():
2248 upload_args.append('--reviewers=%s' % ','.join(
2249 change_desc.get_reviewers()))
2250 if options.send_mail:
2251 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002252 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002253 upload_args.append('--send_mail')
2254
2255 # We check this before applying rietveld.private assuming that in
2256 # rietveld.cc only addresses which we can send private CLs to are listed
2257 # if rietveld.private is set, and so we should ignore rietveld.cc only
2258 # when --private is specified explicitly on the command line.
2259 if options.private:
2260 logging.warn('rietveld.cc is ignored since private flag is specified. '
2261 'You need to review and add them manually if necessary.')
2262 cc = self.GetCCListWithoutDefault()
2263 else:
2264 cc = self.GetCCList()
2265 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002266 if change_desc.get_cced():
2267 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002268 if cc:
2269 upload_args.extend(['--cc', cc])
2270
2271 if options.private or settings.GetDefaultPrivateFlag() == "True":
2272 upload_args.append('--private')
2273
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002274 # Include the upstream repo's URL in the change -- this is useful for
2275 # projects that have their source spread across multiple repos.
2276 remote_url = self.GetGitBaseUrlFromConfig()
2277 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002278 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2279 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2280 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002281 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002282 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002283 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002284 if target_ref:
2285 upload_args.extend(['--target_ref', target_ref])
2286
2287 # Look for dependent patchsets. See crbug.com/480453 for more details.
2288 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2289 upstream_branch = ShortBranchName(upstream_branch)
2290 if remote is '.':
2291 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002292 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002293 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002294 print()
2295 print('Skipping dependency patchset upload because git config '
2296 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2297 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002298 else:
2299 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002300 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002301 auth_config=auth_config)
2302 branch_cl_issue_url = branch_cl.GetIssueURL()
2303 branch_cl_issue = branch_cl.GetIssue()
2304 branch_cl_patchset = branch_cl.GetPatchset()
2305 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2306 upload_args.extend(
2307 ['--depends_on_patchset', '%s:%s' % (
2308 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002309 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002310 '\n'
2311 'The current branch (%s) is tracking a local branch (%s) with '
2312 'an associated CL.\n'
2313 'Adding %s/#ps%s as a dependency patchset.\n'
2314 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2315 branch_cl_patchset))
2316
2317 project = settings.GetProject()
2318 if project:
2319 upload_args.extend(['--project', project])
Aaron Gable665a4392017-06-29 10:53:46 -07002320 else:
2321 print()
2322 print('WARNING: Uploading without a project specified. Please ensure '
2323 'your repo\'s codereview.settings has a "PROJECT: foo" line.')
2324 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002325
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002326 try:
2327 upload_args = ['upload'] + upload_args + args
2328 logging.info('upload.RealMain(%s)', upload_args)
2329 issue, patchset = upload.RealMain(upload_args)
2330 issue = int(issue)
2331 patchset = int(patchset)
2332 except KeyboardInterrupt:
2333 sys.exit(1)
2334 except:
2335 # If we got an exception after the user typed a description for their
2336 # change, back up the description before re-raising.
2337 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002338 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002339 raise
2340
2341 if not self.GetIssue():
2342 self.SetIssue(issue)
2343 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002344 return 0
2345
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002346
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002347class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002348 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002349 # auth_config is Rietveld thing, kept here to preserve interface only.
2350 super(_GerritChangelistImpl, self).__init__(changelist)
2351 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002352 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002353 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002354 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002355 # Map from change number (issue) to its detail cache.
2356 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002357
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002358 if codereview_host is not None:
2359 assert not codereview_host.startswith('https://'), codereview_host
2360 self._gerrit_host = codereview_host
2361 self._gerrit_server = 'https://%s' % codereview_host
2362
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002363 def _GetGerritHost(self):
2364 # Lazy load of configs.
2365 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002366 if self._gerrit_host and '.' not in self._gerrit_host:
2367 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2368 # This happens for internal stuff http://crbug.com/614312.
2369 parsed = urlparse.urlparse(self.GetRemoteUrl())
2370 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002371 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002372 ' Your current remote is: %s' % self.GetRemoteUrl())
2373 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2374 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002375 return self._gerrit_host
2376
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002377 def _GetGitHost(self):
2378 """Returns git host to be used when uploading change to Gerrit."""
2379 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2380
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002381 def GetCodereviewServer(self):
2382 if not self._gerrit_server:
2383 # If we're on a branch then get the server potentially associated
2384 # with that branch.
2385 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002386 self._gerrit_server = self._GitGetBranchConfigValue(
2387 self.CodereviewServerConfigKey())
2388 if self._gerrit_server:
2389 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002390 if not self._gerrit_server:
2391 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2392 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002393 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002394 parts[0] = parts[0] + '-review'
2395 self._gerrit_host = '.'.join(parts)
2396 self._gerrit_server = 'https://%s' % self._gerrit_host
2397 return self._gerrit_server
2398
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002399 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002400 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002401 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002402
tandrii5d48c322016-08-18 16:19:37 -07002403 @classmethod
2404 def PatchsetConfigKey(cls):
2405 return 'gerritpatchset'
2406
2407 @classmethod
2408 def CodereviewServerConfigKey(cls):
2409 return 'gerritserver'
2410
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002411 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002412 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002413 if settings.GetGerritSkipEnsureAuthenticated():
2414 # For projects with unusual authentication schemes.
2415 # See http://crbug.com/603378.
2416 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002417 # Lazy-loader to identify Gerrit and Git hosts.
2418 if gerrit_util.GceAuthenticator.is_gce():
2419 return
2420 self.GetCodereviewServer()
2421 git_host = self._GetGitHost()
2422 assert self._gerrit_server and self._gerrit_host
2423 cookie_auth = gerrit_util.CookiesAuthenticator()
2424
2425 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2426 git_auth = cookie_auth.get_auth_header(git_host)
2427 if gerrit_auth and git_auth:
2428 if gerrit_auth == git_auth:
2429 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002430 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002431 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002432 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002433 ' %s\n'
2434 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002435 ' Consider running the following command:\n'
2436 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002437 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002438 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002439 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002440 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002441 cookie_auth.get_new_password_message(git_host)))
2442 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002443 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002444 return
2445 else:
2446 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002447 ([] if gerrit_auth else [self._gerrit_host]) +
2448 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002449 DieWithError('Credentials for the following hosts are required:\n'
2450 ' %s\n'
2451 'These are read from %s (or legacy %s)\n'
2452 '%s' % (
2453 '\n '.join(missing),
2454 cookie_auth.get_gitcookies_path(),
2455 cookie_auth.get_netrc_path(),
2456 cookie_auth.get_new_password_message(git_host)))
2457
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002458 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002459 if not self.GetIssue():
2460 return
2461
2462 # Warm change details cache now to avoid RPCs later, reducing latency for
2463 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002464 self._GetChangeDetail(
2465 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002466
2467 status = self._GetChangeDetail()['status']
2468 if status in ('MERGED', 'ABANDONED'):
2469 DieWithError('Change %s has been %s, new uploads are not allowed' %
2470 (self.GetIssueURL(),
2471 'submitted' if status == 'MERGED' else 'abandoned'))
2472
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002473 if gerrit_util.GceAuthenticator.is_gce():
2474 return
2475 cookies_user = gerrit_util.CookiesAuthenticator().get_auth_email(
2476 self._GetGerritHost())
2477 if self.GetIssueOwner() == cookies_user:
2478 return
2479 logging.debug('change %s owner is %s, cookies user is %s',
2480 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002481 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002482 # so ask what Gerrit thinks of this user.
2483 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2484 if details['email'] == self.GetIssueOwner():
2485 return
2486 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002487 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002488 'as %s.\n'
2489 'Uploading may fail due to lack of permissions.' %
2490 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2491 confirm_or_exit(action='upload')
2492
2493
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002494 def _PostUnsetIssueProperties(self):
2495 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002496 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002497
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002498 def GetGerritObjForPresubmit(self):
2499 return presubmit_support.GerritAccessor(self._GetGerritHost())
2500
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002501 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002502 """Apply a rough heuristic to give a simple summary of an issue's review
2503 or CQ status, assuming adherence to a common workflow.
2504
2505 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002506 * 'error' - error from review tool (including deleted issues)
2507 * 'unsent' - no reviewers added
2508 * 'waiting' - waiting for review
2509 * 'reply' - waiting for uploader to reply to review
2510 * 'lgtm' - Code-Review label has been set
2511 * 'commit' - in the commit queue
2512 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002513 """
2514 if not self.GetIssue():
2515 return None
2516
2517 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002518 data = self._GetChangeDetail([
2519 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002520 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002521 return 'error'
2522
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002523 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002524 return 'closed'
2525
Aaron Gable9ab38c62017-04-06 14:36:33 -07002526 if data['labels'].get('Commit-Queue', {}).get('approved'):
2527 # The section will have an "approved" subsection if anyone has voted
2528 # the maximum value on the label.
2529 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002530
Aaron Gable9ab38c62017-04-06 14:36:33 -07002531 if data['labels'].get('Code-Review', {}).get('approved'):
2532 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002533
2534 if not data.get('reviewers', {}).get('REVIEWER', []):
2535 return 'unsent'
2536
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002537 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002538 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2539 last_message_author = messages.pop().get('author', {})
2540 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002541 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2542 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002543 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002544 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002545 if last_message_author.get('_account_id') == owner:
2546 # Most recent message was by owner.
2547 return 'waiting'
2548 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002549 # Some reply from non-owner.
2550 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002551
2552 # Somehow there are no messages even though there are reviewers.
2553 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002554
2555 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002556 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002557 patchset = data['revisions'][data['current_revision']]['_number']
2558 self.SetPatchset(patchset)
2559 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002560
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002561 def FetchDescription(self, force=False):
2562 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2563 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002564 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002565 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002566
dsansomee2d6fd92016-09-08 00:10:47 -07002567 def UpdateDescriptionRemote(self, description, force=False):
2568 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2569 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002570 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002571 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002572 'unpublished edit. Either publish the edit in the Gerrit web UI '
2573 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002574
2575 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2576 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002577 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002578 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002579
Aaron Gable636b13f2017-07-14 10:42:48 -07002580 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002581 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
Aaron Gable636b13f2017-07-14 10:42:48 -07002582 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002583
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002584 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002585 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002586 messages = self._GetChangeDetail(
2587 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2588 file_comments = gerrit_util.GetChangeComments(
2589 self._GetGerritHost(), self.GetIssue())
2590
2591 # Build dictionary of file comments for easy access and sorting later.
2592 # {author+date: {path: {patchset: {line: url+message}}}}
2593 comments = collections.defaultdict(
2594 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2595 for path, line_comments in file_comments.iteritems():
2596 for comment in line_comments:
2597 if comment.get('tag', '').startswith('autogenerated'):
2598 continue
2599 key = (comment['author']['email'], comment['updated'])
2600 if comment.get('side', 'REVISION') == 'PARENT':
2601 patchset = 'Base'
2602 else:
2603 patchset = 'PS%d' % comment['patch_set']
2604 line = comment.get('line', 0)
2605 url = ('https://%s/c/%s/%s/%s#%s%s' %
2606 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2607 'b' if comment.get('side') == 'PARENT' else '',
2608 str(line) if line else ''))
2609 comments[key][path][patchset][line] = (url, comment['message'])
2610
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002611 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002612 for msg in messages:
2613 # Don't bother showing autogenerated messages.
2614 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2615 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002616 # Gerrit spits out nanoseconds.
2617 assert len(msg['date'].split('.')[-1]) == 9
2618 date = datetime.datetime.strptime(msg['date'][:-3],
2619 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002620 message = msg['message']
2621 key = (msg['author']['email'], msg['date'])
2622 if key in comments:
2623 message += '\n'
2624 for path, patchsets in sorted(comments.get(key, {}).items()):
2625 if readable:
2626 message += '\n%s' % path
2627 for patchset, lines in sorted(patchsets.items()):
2628 for line, (url, content) in sorted(lines.items()):
2629 if line:
2630 line_str = 'Line %d' % line
2631 path_str = '%s:%d:' % (path, line)
2632 else:
2633 line_str = 'File comment'
2634 path_str = '%s:0:' % path
2635 if readable:
2636 message += '\n %s, %s: %s' % (patchset, line_str, url)
2637 message += '\n %s\n' % content
2638 else:
2639 message += '\n%s ' % path_str
2640 message += '\n%s\n' % content
2641
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002642 summary.append(_CommentSummary(
2643 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002644 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002645 sender=msg['author']['email'],
2646 # These could be inferred from the text messages and correlated with
2647 # Code-Review label maximum, however this is not reliable.
2648 # Leaving as is until the need arises.
2649 approval=False,
2650 disapproval=False,
2651 ))
2652 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002653
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002654 def CloseIssue(self):
2655 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2656
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002657 def SubmitIssue(self, wait_for_merge=True):
2658 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2659 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002660
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002661 def _GetChangeDetail(self, options=None, issue=None,
2662 no_cache=False):
2663 """Returns details of the issue by querying Gerrit and caching results.
2664
2665 If fresh data is needed, set no_cache=True which will clear cache and
2666 thus new data will be fetched from Gerrit.
2667 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002668 options = options or []
2669 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002670 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002671
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002672 # Optimization to avoid multiple RPCs:
2673 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2674 'CURRENT_COMMIT' not in options):
2675 options.append('CURRENT_COMMIT')
2676
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002677 # Normalize issue and options for consistent keys in cache.
2678 issue = str(issue)
2679 options = [o.upper() for o in options]
2680
2681 # Check in cache first unless no_cache is True.
2682 if no_cache:
2683 self._detail_cache.pop(issue, None)
2684 else:
2685 options_set = frozenset(options)
2686 for cached_options_set, data in self._detail_cache.get(issue, []):
2687 # Assumption: data fetched before with extra options is suitable
2688 # for return for a smaller set of options.
2689 # For example, if we cached data for
2690 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2691 # and request is for options=[CURRENT_REVISION],
2692 # THEN we can return prior cached data.
2693 if options_set.issubset(cached_options_set):
2694 return data
2695
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002696 try:
Aaron Gable19ee16c2017-04-18 11:56:35 -07002697 data = gerrit_util.GetChangeDetail(
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002698 self._GetGerritHost(), str(issue), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002699 except gerrit_util.GerritError as e:
2700 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002701 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002702 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002703
2704 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002705 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002706
agable32978d92016-11-01 12:55:02 -07002707 def _GetChangeCommit(self, issue=None):
2708 issue = issue or self.GetIssue()
2709 assert issue, 'issue is required to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002710 try:
2711 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2712 except gerrit_util.GerritError as e:
2713 if e.http_status == 404:
2714 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
2715 raise
agable32978d92016-11-01 12:55:02 -07002716 return data
2717
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002718 def CMDLand(self, force, bypass_hooks, verbose):
2719 if git_common.is_dirty_git_tree('land'):
2720 return 1
tandriid60367b2016-06-22 05:25:12 -07002721 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2722 if u'Commit-Queue' in detail.get('labels', {}):
2723 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002724 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2725 'which can test and land changes for you. '
2726 'Are you sure you wish to bypass it?\n',
2727 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002728
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002729 differs = True
tandriic4344b52016-08-29 06:04:54 -07002730 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002731 # Note: git diff outputs nothing if there is no diff.
2732 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002733 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002734 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002735 if detail['current_revision'] == last_upload:
2736 differs = False
2737 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002738 print('WARNING: Local branch contents differ from latest uploaded '
2739 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002740 if differs:
2741 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002742 confirm_or_exit(
2743 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2744 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002745 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002746 elif not bypass_hooks:
2747 hook_results = self.RunHook(
2748 committing=True,
2749 may_prompt=not force,
2750 verbose=verbose,
2751 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2752 if not hook_results.should_continue():
2753 return 1
2754
2755 self.SubmitIssue(wait_for_merge=True)
2756 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002757 links = self._GetChangeCommit().get('web_links', [])
2758 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002759 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002760 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002761 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002762 return 0
2763
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002764 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002765 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002766 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002767 assert not directory
2768 assert parsed_issue_arg.valid
2769
2770 self._changelist.issue = parsed_issue_arg.issue
2771
2772 if parsed_issue_arg.hostname:
2773 self._gerrit_host = parsed_issue_arg.hostname
2774 self._gerrit_server = 'https://%s' % self._gerrit_host
2775
tandriic2405f52016-10-10 08:13:15 -07002776 try:
2777 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002778 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002779 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002780
2781 if not parsed_issue_arg.patchset:
2782 # Use current revision by default.
2783 revision_info = detail['revisions'][detail['current_revision']]
2784 patchset = int(revision_info['_number'])
2785 else:
2786 patchset = parsed_issue_arg.patchset
2787 for revision_info in detail['revisions'].itervalues():
2788 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2789 break
2790 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002791 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002792 (parsed_issue_arg.patchset, self.GetIssue()))
2793
Aaron Gable697a91b2018-01-19 15:20:15 -08002794 remote_url = self._changelist.GetRemoteUrl()
2795 if remote_url.endswith('.git'):
2796 remote_url = remote_url[:-len('.git')]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002797 fetch_info = revision_info['fetch']['http']
Aaron Gable697a91b2018-01-19 15:20:15 -08002798
2799 if remote_url != fetch_info['url']:
2800 DieWithError('Trying to patch a change from %s but this repo appears '
2801 'to be %s.' % (fetch_info['url'], remote_url))
2802
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002803 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002804
Aaron Gable62619a32017-06-16 08:22:09 -07002805 if force:
2806 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2807 print('Checked out commit for change %i patchset %i locally' %
2808 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002809 elif nocommit:
2810 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2811 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002812 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002813 RunGit(['cherry-pick', 'FETCH_HEAD'])
2814 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002815 (parsed_issue_arg.issue, patchset))
2816 print('Note: this created a local commit which does not have '
2817 'the same hash as the one uploaded for review. This will make '
2818 'uploading changes based on top of this branch difficult.\n'
2819 'If you want to do that, use "git cl patch --force" instead.')
2820
Stefan Zagerd08043c2017-10-12 12:07:02 -07002821 if self.GetBranch():
2822 self.SetIssue(parsed_issue_arg.issue)
2823 self.SetPatchset(patchset)
2824 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2825 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2826 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2827 else:
2828 print('WARNING: You are in detached HEAD state.\n'
2829 'The patch has been applied to your checkout, but you will not be '
2830 'able to upload a new patch set to the gerrit issue.\n'
2831 'Try using the \'-b\' option if you would like to work on a '
2832 'branch and/or upload a new patch set.')
2833
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002834 return 0
2835
2836 @staticmethod
2837 def ParseIssueURL(parsed_url):
2838 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2839 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002840 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2841 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002842 # Short urls like https://domain/<issue_number> can be used, but don't allow
2843 # specifying the patchset (you'd 404), but we allow that here.
2844 if parsed_url.path == '/':
2845 part = parsed_url.fragment
2846 else:
2847 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002848 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002849 if match:
2850 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002851 issue=int(match.group(3)),
2852 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002853 hostname=parsed_url.netloc,
2854 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002855 return None
2856
tandrii16e0b4e2016-06-07 10:34:28 -07002857 def _GerritCommitMsgHookCheck(self, offer_removal):
2858 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2859 if not os.path.exists(hook):
2860 return
2861 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2862 # custom developer made one.
2863 data = gclient_utils.FileRead(hook)
2864 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2865 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002866 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002867 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002868 'and may interfere with it in subtle ways.\n'
2869 'We recommend you remove the commit-msg hook.')
2870 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002871 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002872 gclient_utils.rm_file_or_tree(hook)
2873 print('Gerrit commit-msg hook removed.')
2874 else:
2875 print('OK, will keep Gerrit commit-msg hook in place.')
2876
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002877 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002878 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002879 if options.squash and options.no_squash:
2880 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002881
2882 if not options.squash and not options.no_squash:
2883 # Load default for user, repo, squash=true, in this order.
2884 options.squash = settings.GetSquashGerritUploads()
2885 elif options.no_squash:
2886 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002887
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002888 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002889 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002890
Aaron Gableb56ad332017-01-06 15:24:31 -08002891 # This may be None; default fallback value is determined in logic below.
2892 title = options.title
2893
Dominic Battre7d1c4842017-10-27 09:17:28 +02002894 # Extract bug number from branch name.
2895 bug = options.bug
2896 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2897 if not bug and match:
2898 bug = match.group(1)
2899
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002900 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002901 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002902 if self.GetIssue():
2903 # Try to get the message from a previous upload.
2904 message = self.GetDescription()
2905 if not message:
2906 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002907 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002908 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002909 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002910 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002911 # When uploading a subsequent patchset, -m|--message is taken
2912 # as the patchset title if --title was not provided.
2913 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002914 else:
2915 default_title = RunGit(
2916 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002917 if options.force:
2918 title = default_title
2919 else:
2920 title = ask_for_data(
2921 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002922 change_id = self._GetChangeDetail()['change_id']
2923 while True:
2924 footer_change_ids = git_footers.get_footer_change_id(message)
2925 if footer_change_ids == [change_id]:
2926 break
2927 if not footer_change_ids:
2928 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002929 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002930 continue
2931 # There is already a valid footer but with different or several ids.
2932 # Doing this automatically is non-trivial as we don't want to lose
2933 # existing other footers, yet we want to append just 1 desired
2934 # Change-Id. Thus, just create a new footer, but let user verify the
2935 # new description.
2936 message = '%s\n\nChange-Id: %s' % (message, change_id)
2937 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002938 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002939 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002940 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002941 'Please, check the proposed correction to the description, '
2942 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2943 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2944 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002945 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002946 if not options.force:
2947 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002948 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002949 message = change_desc.description
2950 if not message:
2951 DieWithError("Description is empty. Aborting...")
2952 # Continue the while loop.
2953 # Sanity check of this code - we should end up with proper message
2954 # footer.
2955 assert [change_id] == git_footers.get_footer_change_id(message)
2956 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002957 else: # if not self.GetIssue()
2958 if options.message:
2959 message = options.message
2960 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002961 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002962 if options.title:
2963 message = options.title + '\n\n' + message
2964 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002965
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002966 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002967 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002968 # On first upload, patchset title is always this string, while
2969 # --title flag gets converted to first line of message.
2970 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002971 if not change_desc.description:
2972 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002973 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002974 if len(change_ids) > 1:
2975 DieWithError('too many Change-Id footers, at most 1 allowed.')
2976 if not change_ids:
2977 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002978 change_desc.set_description(git_footers.add_footer_change_id(
2979 change_desc.description,
2980 GenerateGerritChangeId(change_desc.description)))
2981 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002982 assert len(change_ids) == 1
2983 change_id = change_ids[0]
2984
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002985 if options.reviewers or options.tbrs or options.add_owners_to:
2986 change_desc.update_reviewers(options.reviewers, options.tbrs,
2987 options.add_owners_to, change)
2988
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002989 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002990 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2991 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002992 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002993 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2994 desc_tempfile.write(change_desc.description)
2995 desc_tempfile.close()
2996 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2997 '-F', desc_tempfile.name]).strip()
2998 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002999 else:
3000 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003001 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003002 if not change_desc.description:
3003 DieWithError("Description is empty. Aborting...")
3004
3005 if not git_footers.get_footer_change_id(change_desc.description):
3006 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003007 change_desc.set_description(
3008 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003009 if options.reviewers or options.tbrs or options.add_owners_to:
3010 change_desc.update_reviewers(options.reviewers, options.tbrs,
3011 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003012 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003013 # For no-squash mode, we assume the remote called "origin" is the one we
3014 # want. It is not worthwhile to support different workflows for
3015 # no-squash mode.
3016 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003017 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
3018
3019 assert change_desc
3020 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
3021 ref_to_push)]).splitlines()
3022 if len(commits) > 1:
3023 print('WARNING: This will upload %d commits. Run the following command '
3024 'to see which commits will be uploaded: ' % len(commits))
3025 print('git log %s..%s' % (parent, ref_to_push))
3026 print('You can also use `git squash-branch` to squash these into a '
3027 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003028 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003029
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003030 if options.reviewers or options.tbrs or options.add_owners_to:
3031 change_desc.update_reviewers(options.reviewers, options.tbrs,
3032 options.add_owners_to, change)
3033
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003034 # Extra options that can be specified at push time. Doc:
3035 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003036 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003037
Aaron Gable844cf292017-06-28 11:32:59 -07003038 # By default, new changes are started in WIP mode, and subsequent patchsets
3039 # don't send email. At any time, passing --send-mail will mark the change
3040 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07003041 if options.send_mail:
3042 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07003043 refspec_opts.append('notify=ALL')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003044 elif not self.GetIssue():
3045 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07003046 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003047 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07003048
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003049 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07003050 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003051
Aaron Gable9b713dd2016-12-14 16:04:21 -08003052 if title:
Nick Carter8692b182017-11-06 16:30:38 -08003053 # Punctuation and whitespace in |title| must be percent-encoded.
3054 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003055
agablec6787972016-09-09 16:13:34 -07003056 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07003057 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07003058
rmistry9eadede2016-09-19 11:22:43 -07003059 if options.topic:
3060 # Documentation on Gerrit topics is here:
3061 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003062 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07003063
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003064 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08003065 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003066 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08003067 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003068 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
3069
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003070 refspec_suffix = ''
3071 if refspec_opts:
3072 refspec_suffix = '%' + ','.join(refspec_opts)
3073 assert ' ' not in refspec_suffix, (
3074 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3075 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3076
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003077 try:
3078 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003079 ['git', 'push', self.GetRemoteUrl(), refspec],
3080 print_stdout=True,
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003081 # Flush after every line: useful for seeing progress when running as
3082 # recipe.
3083 filter_fn=lambda _: sys.stdout.flush())
3084 except subprocess2.CalledProcessError:
3085 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003086 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003087 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003088 'credential problems:\n'
3089 ' git cl creds-check\n',
3090 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003091
3092 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07003093 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003094 change_numbers = [m.group(1)
3095 for m in map(regex.match, push_stdout.splitlines())
3096 if m]
3097 if len(change_numbers) != 1:
3098 DieWithError(
3099 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003100 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003101 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003102 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003103
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003104 reviewers = sorted(change_desc.get_reviewers())
3105
tandrii88189772016-09-29 04:29:57 -07003106 # Add cc's from the CC_LIST and --cc flag (if any).
Aaron Gabled1052492017-05-15 15:05:34 -07003107 if not options.private:
3108 cc = self.GetCCList().split(',')
3109 else:
3110 cc = []
tandrii88189772016-09-29 04:29:57 -07003111 if options.cc:
3112 cc.extend(options.cc)
3113 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003114 if change_desc.get_cced():
3115 cc.extend(change_desc.get_cced())
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003116
3117 gerrit_util.AddReviewers(
3118 self._GetGerritHost(), self.GetIssue(), reviewers, cc,
3119 notify=bool(options.send_mail))
3120
Aaron Gablefd238082017-06-07 13:42:34 -07003121 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003122 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3123 score = 1
3124 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3125 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3126 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003127 gerrit_util.SetReview(
3128 self._GetGerritHost(), self.GetIssue(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003129 msg='Self-approving for TBR',
3130 labels={'Code-Review': score})
Aaron Gablefd238082017-06-07 13:42:34 -07003131
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003132 return 0
3133
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003134 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3135 change_desc):
3136 """Computes parent of the generated commit to be uploaded to Gerrit.
3137
3138 Returns revision or a ref name.
3139 """
3140 if custom_cl_base:
3141 # Try to avoid creating additional unintended CLs when uploading, unless
3142 # user wants to take this risk.
3143 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3144 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3145 local_ref_of_target_remote])
3146 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003147 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003148 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3149 'If you proceed with upload, more than 1 CL may be created by '
3150 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3151 'If you are certain that specified base `%s` has already been '
3152 'uploaded to Gerrit as another CL, you may proceed.\n' %
3153 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3154 if not force:
3155 confirm_or_exit(
3156 'Do you take responsibility for cleaning up potential mess '
3157 'resulting from proceeding with upload?',
3158 action='upload')
3159 return custom_cl_base
3160
Aaron Gablef97e33d2017-03-30 15:44:27 -07003161 if remote != '.':
3162 return self.GetCommonAncestorWithUpstream()
3163
3164 # If our upstream branch is local, we base our squashed commit on its
3165 # squashed version.
3166 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3167
Aaron Gablef97e33d2017-03-30 15:44:27 -07003168 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003169 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003170
3171 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003172 # TODO(tandrii): consider checking parent change in Gerrit and using its
3173 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3174 # the tree hash of the parent branch. The upside is less likely bogus
3175 # requests to reupload parent change just because it's uploadhash is
3176 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003177 parent = RunGit(['config',
3178 'branch.%s.gerritsquashhash' % upstream_branch_name],
3179 error_ok=True).strip()
3180 # Verify that the upstream branch has been uploaded too, otherwise
3181 # Gerrit will create additional CLs when uploading.
3182 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3183 RunGitSilent(['rev-parse', parent + ':'])):
3184 DieWithError(
3185 '\nUpload upstream branch %s first.\n'
3186 'It is likely that this branch has been rebased since its last '
3187 'upload, so you just need to upload it again.\n'
3188 '(If you uploaded it with --no-squash, then branch dependencies '
3189 'are not supported, and you should reupload with --squash.)'
3190 % upstream_branch_name,
3191 change_desc)
3192 return parent
3193
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003194 def _AddChangeIdToCommitMessage(self, options, args):
3195 """Re-commits using the current message, assumes the commit hook is in
3196 place.
3197 """
3198 log_desc = options.message or CreateDescriptionFromLog(args)
3199 git_command = ['commit', '--amend', '-m', log_desc]
3200 RunGit(git_command)
3201 new_log_desc = CreateDescriptionFromLog(args)
3202 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003203 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003204 return new_log_desc
3205 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003206 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003207
Ravi Mistry31e7d562018-04-02 12:53:57 -04003208 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
3209 """Sets labels on the change based on the provided flags."""
3210 labels = {}
3211 notify = None;
3212 if enable_auto_submit:
3213 labels['Auto-Submit'] = 1
3214 if use_commit_queue:
3215 labels['Commit-Queue'] = 2
3216 elif cq_dry_run:
3217 labels['Commit-Queue'] = 1
3218 notify = False
3219 if labels:
3220 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
3221 labels=labels, notify=notify)
3222
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003223 def SetCQState(self, new_state):
3224 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003225 vote_map = {
3226 _CQState.NONE: 0,
3227 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003228 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003229 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003230 labels = {'Commit-Queue': vote_map[new_state]}
3231 notify = False if new_state == _CQState.DRY_RUN else None
3232 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
3233 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003234
tandriie113dfd2016-10-11 10:20:12 -07003235 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003236 try:
3237 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003238 except GerritChangeNotExists:
3239 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003240
3241 if data['status'] in ('ABANDONED', 'MERGED'):
3242 return 'CL %s is closed' % self.GetIssue()
3243
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003244 def GetTryJobProperties(self, patchset=None):
3245 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003246 data = self._GetChangeDetail(['ALL_REVISIONS'])
3247 patchset = int(patchset or self.GetPatchset())
3248 assert patchset
3249 revision_data = None # Pylint wants it to be defined.
3250 for revision_data in data['revisions'].itervalues():
3251 if int(revision_data['_number']) == patchset:
3252 break
3253 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003254 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003255 (patchset, self.GetIssue()))
3256 return {
3257 'patch_issue': self.GetIssue(),
3258 'patch_set': patchset or self.GetPatchset(),
3259 'patch_project': data['project'],
3260 'patch_storage': 'gerrit',
3261 'patch_ref': revision_data['fetch']['http']['ref'],
3262 'patch_repository_url': revision_data['fetch']['http']['url'],
3263 'patch_gerrit_url': self.GetCodereviewServer(),
3264 }
tandriie113dfd2016-10-11 10:20:12 -07003265
tandriide281ae2016-10-12 06:02:30 -07003266 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003267 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003268
Edward Lemur707d70b2018-02-07 00:50:14 +01003269 def GetReviewers(self):
3270 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3271 return [reviewer['email'] for reviewer in details['reviewers']['REVIEWER']]
3272
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003273
3274_CODEREVIEW_IMPLEMENTATIONS = {
3275 'rietveld': _RietveldChangelistImpl,
3276 'gerrit': _GerritChangelistImpl,
3277}
3278
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003279
iannuccie53c9352016-08-17 14:40:40 -07003280def _add_codereview_issue_select_options(parser, extra=""):
3281 _add_codereview_select_options(parser)
3282
3283 text = ('Operate on this issue number instead of the current branch\'s '
3284 'implicit issue.')
3285 if extra:
3286 text += ' '+extra
3287 parser.add_option('-i', '--issue', type=int, help=text)
3288
3289
3290def _process_codereview_issue_select_options(parser, options):
3291 _process_codereview_select_options(parser, options)
3292 if options.issue is not None and not options.forced_codereview:
3293 parser.error('--issue must be specified with either --rietveld or --gerrit')
3294
3295
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003296def _add_codereview_select_options(parser):
3297 """Appends --gerrit and --rietveld options to force specific codereview."""
3298 parser.codereview_group = optparse.OptionGroup(
3299 parser, 'EXPERIMENTAL! Codereview override options')
3300 parser.add_option_group(parser.codereview_group)
3301 parser.codereview_group.add_option(
3302 '--gerrit', action='store_true',
3303 help='Force the use of Gerrit for codereview')
3304 parser.codereview_group.add_option(
3305 '--rietveld', action='store_true',
3306 help='Force the use of Rietveld for codereview')
3307
3308
3309def _process_codereview_select_options(parser, options):
3310 if options.gerrit and options.rietveld:
3311 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3312 options.forced_codereview = None
3313 if options.gerrit:
3314 options.forced_codereview = 'gerrit'
3315 elif options.rietveld:
3316 options.forced_codereview = 'rietveld'
3317
3318
tandriif9aefb72016-07-01 09:06:51 -07003319def _get_bug_line_values(default_project, bugs):
3320 """Given default_project and comma separated list of bugs, yields bug line
3321 values.
3322
3323 Each bug can be either:
3324 * a number, which is combined with default_project
3325 * string, which is left as is.
3326
3327 This function may produce more than one line, because bugdroid expects one
3328 project per line.
3329
3330 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3331 ['v8:123', 'chromium:789']
3332 """
3333 default_bugs = []
3334 others = []
3335 for bug in bugs.split(','):
3336 bug = bug.strip()
3337 if bug:
3338 try:
3339 default_bugs.append(int(bug))
3340 except ValueError:
3341 others.append(bug)
3342
3343 if default_bugs:
3344 default_bugs = ','.join(map(str, default_bugs))
3345 if default_project:
3346 yield '%s:%s' % (default_project, default_bugs)
3347 else:
3348 yield default_bugs
3349 for other in sorted(others):
3350 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3351 yield other
3352
3353
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003354class ChangeDescription(object):
3355 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003356 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003357 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003358 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003359 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003360 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3361 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3362 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3363 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003364
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003365 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003366 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003367
agable@chromium.org42c20792013-09-12 17:34:49 +00003368 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003369 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003370 return '\n'.join(self._description_lines)
3371
3372 def set_description(self, desc):
3373 if isinstance(desc, basestring):
3374 lines = desc.splitlines()
3375 else:
3376 lines = [line.rstrip() for line in desc]
3377 while lines and not lines[0]:
3378 lines.pop(0)
3379 while lines and not lines[-1]:
3380 lines.pop(-1)
3381 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003382
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003383 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3384 """Rewrites the R=/TBR= line(s) as a single line each.
3385
3386 Args:
3387 reviewers (list(str)) - list of additional emails to use for reviewers.
3388 tbrs (list(str)) - list of additional emails to use for TBRs.
3389 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3390 the change that are missing OWNER coverage. If this is not None, you
3391 must also pass a value for `change`.
3392 change (Change) - The Change that should be used for OWNERS lookups.
3393 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003394 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003395 assert isinstance(tbrs, list), tbrs
3396
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003397 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003398 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003399
3400 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003401 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003402
3403 reviewers = set(reviewers)
3404 tbrs = set(tbrs)
3405 LOOKUP = {
3406 'TBR': tbrs,
3407 'R': reviewers,
3408 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003409
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003410 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003411 regexp = re.compile(self.R_LINE)
3412 matches = [regexp.match(line) for line in self._description_lines]
3413 new_desc = [l for i, l in enumerate(self._description_lines)
3414 if not matches[i]]
3415 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003416
agable@chromium.org42c20792013-09-12 17:34:49 +00003417 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003418
3419 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003420 for match in matches:
3421 if not match:
3422 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003423 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3424
3425 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003426 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003427 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003428 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003429 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003430 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003431 LOOKUP[add_owners_to].update(
3432 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003433
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003434 # If any folks ended up in both groups, remove them from tbrs.
3435 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003436
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003437 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3438 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003439
3440 # Put the new lines in the description where the old first R= line was.
3441 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3442 if 0 <= line_loc < len(self._description_lines):
3443 if new_tbr_line:
3444 self._description_lines.insert(line_loc, new_tbr_line)
3445 if new_r_line:
3446 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003447 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003448 if new_r_line:
3449 self.append_footer(new_r_line)
3450 if new_tbr_line:
3451 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003452
Aaron Gable3a16ed12017-03-23 10:51:55 -07003453 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003454 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003455 self.set_description([
3456 '# Enter a description of the change.',
3457 '# This will be displayed on the codereview site.',
3458 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003459 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003460 '--------------------',
3461 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003462
agable@chromium.org42c20792013-09-12 17:34:49 +00003463 regexp = re.compile(self.BUG_LINE)
3464 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003465 prefix = settings.GetBugPrefix()
3466 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003467 if git_footer:
3468 self.append_footer('Bug: %s' % ', '.join(values))
3469 else:
3470 for value in values:
3471 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003472
agable@chromium.org42c20792013-09-12 17:34:49 +00003473 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003474 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003475 if not content:
3476 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003477 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003478
Bruce Dawson2377b012018-01-11 16:46:49 -08003479 # Strip off comments and default inserted "Bug:" line.
3480 clean_lines = [line.rstrip() for line in lines if not
3481 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003482 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003483 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003484 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003485
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003486 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003487 """Adds a footer line to the description.
3488
3489 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3490 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3491 that Gerrit footers are always at the end.
3492 """
3493 parsed_footer_line = git_footers.parse_footer(line)
3494 if parsed_footer_line:
3495 # Line is a gerrit footer in the form: Footer-Key: any value.
3496 # Thus, must be appended observing Gerrit footer rules.
3497 self.set_description(
3498 git_footers.add_footer(self.description,
3499 key=parsed_footer_line[0],
3500 value=parsed_footer_line[1]))
3501 return
3502
3503 if not self._description_lines:
3504 self._description_lines.append(line)
3505 return
3506
3507 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3508 if gerrit_footers:
3509 # git_footers.split_footers ensures that there is an empty line before
3510 # actual (gerrit) footers, if any. We have to keep it that way.
3511 assert top_lines and top_lines[-1] == ''
3512 top_lines, separator = top_lines[:-1], top_lines[-1:]
3513 else:
3514 separator = [] # No need for separator if there are no gerrit_footers.
3515
3516 prev_line = top_lines[-1] if top_lines else ''
3517 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3518 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3519 top_lines.append('')
3520 top_lines.append(line)
3521 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003522
tandrii99a72f22016-08-17 14:33:24 -07003523 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003524 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003525 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003526 reviewers = [match.group(2).strip()
3527 for match in matches
3528 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003529 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003530
bradnelsond975b302016-10-23 12:20:23 -07003531 def get_cced(self):
3532 """Retrieves the list of reviewers."""
3533 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3534 cced = [match.group(2).strip() for match in matches if match]
3535 return cleanup_list(cced)
3536
Nodir Turakulov23b82142017-11-16 11:04:25 -08003537 def get_hash_tags(self):
3538 """Extracts and sanitizes a list of Gerrit hashtags."""
3539 subject = (self._description_lines or ('',))[0]
3540 subject = re.sub(
3541 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3542
3543 tags = []
3544 start = 0
3545 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3546 while True:
3547 m = bracket_exp.match(subject, start)
3548 if not m:
3549 break
3550 tags.append(self.sanitize_hash_tag(m.group(1)))
3551 start = m.end()
3552
3553 if not tags:
3554 # Try "Tag: " prefix.
3555 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3556 if m:
3557 tags.append(self.sanitize_hash_tag(m.group(1)))
3558 return tags
3559
3560 @classmethod
3561 def sanitize_hash_tag(cls, tag):
3562 """Returns a sanitized Gerrit hash tag.
3563
3564 A sanitized hashtag can be used as a git push refspec parameter value.
3565 """
3566 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3567
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003568 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3569 """Updates this commit description given the parent.
3570
3571 This is essentially what Gnumbd used to do.
3572 Consult https://goo.gl/WMmpDe for more details.
3573 """
3574 assert parent_msg # No, orphan branch creation isn't supported.
3575 assert parent_hash
3576 assert dest_ref
3577 parent_footer_map = git_footers.parse_footers(parent_msg)
3578 # This will also happily parse svn-position, which GnumbD is no longer
3579 # supporting. While we'd generate correct footers, the verifier plugin
3580 # installed in Gerrit will block such commit (ie git push below will fail).
3581 parent_position = git_footers.get_position(parent_footer_map)
3582
3583 # Cherry-picks may have last line obscuring their prior footers,
3584 # from git_footers perspective. This is also what Gnumbd did.
3585 cp_line = None
3586 if (self._description_lines and
3587 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3588 cp_line = self._description_lines.pop()
3589
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003590 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003591
3592 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3593 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003594 for i, line in enumerate(footer_lines):
3595 k, v = git_footers.parse_footer(line) or (None, None)
3596 if k and k.startswith('Cr-'):
3597 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003598
3599 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003600 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003601 if parent_position[0] == dest_ref:
3602 # Same branch as parent.
3603 number = int(parent_position[1]) + 1
3604 else:
3605 number = 1 # New branch, and extra lineage.
3606 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3607 int(parent_position[1])))
3608
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003609 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3610 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003611
3612 self._description_lines = top_lines
3613 if cp_line:
3614 self._description_lines.append(cp_line)
3615 if self._description_lines[-1] != '':
3616 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003617 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003618
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003619
Aaron Gablea1bab272017-04-11 16:38:18 -07003620def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003621 """Retrieves the reviewers that approved a CL from the issue properties with
3622 messages.
3623
3624 Note that the list may contain reviewers that are not committer, thus are not
3625 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003626
3627 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003628 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003629 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003630 return sorted(
3631 set(
3632 message['sender']
3633 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003634 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003635 )
3636 )
3637
3638
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003639def FindCodereviewSettingsFile(filename='codereview.settings'):
3640 """Finds the given file starting in the cwd and going up.
3641
3642 Only looks up to the top of the repository unless an
3643 'inherit-review-settings-ok' file exists in the root of the repository.
3644 """
3645 inherit_ok_file = 'inherit-review-settings-ok'
3646 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003647 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003648 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3649 root = '/'
3650 while True:
3651 if filename in os.listdir(cwd):
3652 if os.path.isfile(os.path.join(cwd, filename)):
3653 return open(os.path.join(cwd, filename))
3654 if cwd == root:
3655 break
3656 cwd = os.path.dirname(cwd)
3657
3658
3659def LoadCodereviewSettingsFromFile(fileobj):
3660 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003661 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003662
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003663 def SetProperty(name, setting, unset_error_ok=False):
3664 fullname = 'rietveld.' + name
3665 if setting in keyvals:
3666 RunGit(['config', fullname, keyvals[setting]])
3667 else:
3668 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3669
tandrii48df5812016-10-17 03:55:37 -07003670 if not keyvals.get('GERRIT_HOST', False):
3671 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003672 # Only server setting is required. Other settings can be absent.
3673 # In that case, we ignore errors raised during option deletion attempt.
3674 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003675 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003676 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3677 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003678 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003679 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3680 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003681 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003682 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3683 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003684
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003685 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003686 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003687
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003688 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003689 RunGit(['config', 'gerrit.squash-uploads',
3690 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003691
tandrii@chromium.org28253532016-04-14 13:46:56 +00003692 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003693 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003694 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3695
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003696 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003697 # should be of the form
3698 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3699 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003700 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3701 keyvals['ORIGIN_URL_CONFIG']])
3702
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003703
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003704def urlretrieve(source, destination):
3705 """urllib is broken for SSL connections via a proxy therefore we
3706 can't use urllib.urlretrieve()."""
3707 with open(destination, 'w') as f:
3708 f.write(urllib2.urlopen(source).read())
3709
3710
ukai@chromium.org712d6102013-11-27 00:52:58 +00003711def hasSheBang(fname):
3712 """Checks fname is a #! script."""
3713 with open(fname) as f:
3714 return f.read(2).startswith('#!')
3715
3716
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003717# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3718def DownloadHooks(*args, **kwargs):
3719 pass
3720
3721
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003722def DownloadGerritHook(force):
3723 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003724
3725 Args:
3726 force: True to update hooks. False to install hooks if not present.
3727 """
3728 if not settings.GetIsGerrit():
3729 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003730 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003731 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3732 if not os.access(dst, os.X_OK):
3733 if os.path.exists(dst):
3734 if not force:
3735 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003736 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003737 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003738 if not hasSheBang(dst):
3739 DieWithError('Not a script: %s\n'
3740 'You need to download from\n%s\n'
3741 'into .git/hooks/commit-msg and '
3742 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003743 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3744 except Exception:
3745 if os.path.exists(dst):
3746 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003747 DieWithError('\nFailed to download hooks.\n'
3748 'You need to download from\n%s\n'
3749 'into .git/hooks/commit-msg and '
3750 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003751
3752
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003753def GetRietveldCodereviewSettingsInteractively():
3754 """Prompt the user for settings."""
3755 server = settings.GetDefaultServerUrl(error_ok=True)
3756 prompt = 'Rietveld server (host[:port])'
3757 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3758 newserver = ask_for_data(prompt + ':')
3759 if not server and not newserver:
3760 newserver = DEFAULT_SERVER
3761 if newserver:
3762 newserver = gclient_utils.UpgradeToHttps(newserver)
3763 if newserver != server:
3764 RunGit(['config', 'rietveld.server', newserver])
3765
3766 def SetProperty(initial, caption, name, is_url):
3767 prompt = caption
3768 if initial:
3769 prompt += ' ("x" to clear) [%s]' % initial
3770 new_val = ask_for_data(prompt + ':')
3771 if new_val == 'x':
3772 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3773 elif new_val:
3774 if is_url:
3775 new_val = gclient_utils.UpgradeToHttps(new_val)
3776 if new_val != initial:
3777 RunGit(['config', 'rietveld.' + name, new_val])
3778
3779 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3780 SetProperty(settings.GetDefaultPrivateFlag(),
3781 'Private flag (rietveld only)', 'private', False)
3782 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3783 'tree-status-url', False)
3784 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3785 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3786 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3787 'run-post-upload-hook', False)
3788
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003789
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003790class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003791 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003792
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003793 _GOOGLESOURCE = 'googlesource.com'
3794
3795 def __init__(self):
3796 # Cached list of [host, identity, source], where source is either
3797 # .gitcookies or .netrc.
3798 self._all_hosts = None
3799
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003800 def ensure_configured_gitcookies(self):
3801 """Runs checks and suggests fixes to make git use .gitcookies from default
3802 path."""
3803 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3804 configured_path = RunGitSilent(
3805 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003806 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003807 if configured_path:
3808 self._ensure_default_gitcookies_path(configured_path, default)
3809 else:
3810 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003811
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003812 @staticmethod
3813 def _ensure_default_gitcookies_path(configured_path, default_path):
3814 assert configured_path
3815 if configured_path == default_path:
3816 print('git is already configured to use your .gitcookies from %s' %
3817 configured_path)
3818 return
3819
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003820 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003821 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3822 (configured_path, default_path))
3823
3824 if not os.path.exists(configured_path):
3825 print('However, your configured .gitcookies file is missing.')
3826 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3827 action='reconfigure')
3828 RunGit(['config', '--global', 'http.cookiefile', default_path])
3829 return
3830
3831 if os.path.exists(default_path):
3832 print('WARNING: default .gitcookies file already exists %s' %
3833 default_path)
3834 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3835 default_path)
3836
3837 confirm_or_exit('Move existing .gitcookies to default location?',
3838 action='move')
3839 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003840 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003841 print('Moved and reconfigured git to use .gitcookies from %s' %
3842 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003843
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003844 @staticmethod
3845 def _configure_gitcookies_path(default_path):
3846 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3847 if os.path.exists(netrc_path):
3848 print('You seem to be using outdated .netrc for git credentials: %s' %
3849 netrc_path)
3850 print('This tool will guide you through setting up recommended '
3851 '.gitcookies store for git credentials.\n'
3852 '\n'
3853 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3854 ' git config --global --unset http.cookiefile\n'
3855 ' mv %s %s.backup\n\n' % (default_path, default_path))
3856 confirm_or_exit(action='setup .gitcookies')
3857 RunGit(['config', '--global', 'http.cookiefile', default_path])
3858 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003859
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003860 def get_hosts_with_creds(self, include_netrc=False):
3861 if self._all_hosts is None:
3862 a = gerrit_util.CookiesAuthenticator()
3863 self._all_hosts = [
3864 (h, u, s)
3865 for h, u, s in itertools.chain(
3866 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3867 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3868 )
3869 if h.endswith(self._GOOGLESOURCE)
3870 ]
3871
3872 if include_netrc:
3873 return self._all_hosts
3874 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3875
3876 def print_current_creds(self, include_netrc=False):
3877 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3878 if not hosts:
3879 print('No Git/Gerrit credentials found')
3880 return
3881 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3882 header = [('Host', 'User', 'Which file'),
3883 ['=' * l for l in lengths]]
3884 for row in (header + hosts):
3885 print('\t'.join((('%%+%ds' % l) % s)
3886 for l, s in zip(lengths, row)))
3887
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003888 @staticmethod
3889 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003890 """Parses identity "git-<username>.domain" into <username> and domain."""
3891 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003892 # distinguishable from sub-domains. But we do know typical domains:
3893 if identity.endswith('.chromium.org'):
3894 domain = 'chromium.org'
3895 username = identity[:-len('.chromium.org')]
3896 else:
3897 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003898 if username.startswith('git-'):
3899 username = username[len('git-'):]
3900 return username, domain
3901
3902 def _get_usernames_of_domain(self, domain):
3903 """Returns list of usernames referenced by .gitcookies in a given domain."""
3904 identities_by_domain = {}
3905 for _, identity, _ in self.get_hosts_with_creds():
3906 username, domain = self._parse_identity(identity)
3907 identities_by_domain.setdefault(domain, []).append(username)
3908 return identities_by_domain.get(domain)
3909
3910 def _canonical_git_googlesource_host(self, host):
3911 """Normalizes Gerrit hosts (with '-review') to Git host."""
3912 assert host.endswith(self._GOOGLESOURCE)
3913 # Prefix doesn't include '.' at the end.
3914 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3915 if prefix.endswith('-review'):
3916 prefix = prefix[:-len('-review')]
3917 return prefix + '.' + self._GOOGLESOURCE
3918
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003919 def _canonical_gerrit_googlesource_host(self, host):
3920 git_host = self._canonical_git_googlesource_host(host)
3921 prefix = git_host.split('.', 1)[0]
3922 return prefix + '-review.' + self._GOOGLESOURCE
3923
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003924 def _get_counterpart_host(self, host):
3925 assert host.endswith(self._GOOGLESOURCE)
3926 git = self._canonical_git_googlesource_host(host)
3927 gerrit = self._canonical_gerrit_googlesource_host(git)
3928 return git if gerrit == host else gerrit
3929
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003930 def has_generic_host(self):
3931 """Returns whether generic .googlesource.com has been configured.
3932
3933 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3934 """
3935 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3936 if host == '.' + self._GOOGLESOURCE:
3937 return True
3938 return False
3939
3940 def _get_git_gerrit_identity_pairs(self):
3941 """Returns map from canonic host to pair of identities (Git, Gerrit).
3942
3943 One of identities might be None, meaning not configured.
3944 """
3945 host_to_identity_pairs = {}
3946 for host, identity, _ in self.get_hosts_with_creds():
3947 canonical = self._canonical_git_googlesource_host(host)
3948 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3949 idx = 0 if canonical == host else 1
3950 pair[idx] = identity
3951 return host_to_identity_pairs
3952
3953 def get_partially_configured_hosts(self):
3954 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003955 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3956 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3957 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003958
3959 def get_conflicting_hosts(self):
3960 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003961 host
3962 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003963 if None not in (i1, i2) and i1 != i2)
3964
3965 def get_duplicated_hosts(self):
3966 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3967 return set(host for host, count in counters.iteritems() if count > 1)
3968
3969 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3970 'chromium.googlesource.com': 'chromium.org',
3971 'chrome-internal.googlesource.com': 'google.com',
3972 }
3973
3974 def get_hosts_with_wrong_identities(self):
3975 """Finds hosts which **likely** reference wrong identities.
3976
3977 Note: skips hosts which have conflicting identities for Git and Gerrit.
3978 """
3979 hosts = set()
3980 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3981 pair = self._get_git_gerrit_identity_pairs().get(host)
3982 if pair and pair[0] == pair[1]:
3983 _, domain = self._parse_identity(pair[0])
3984 if domain != expected:
3985 hosts.add(host)
3986 return hosts
3987
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003988 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003989 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003990 hosts = sorted(hosts)
3991 assert hosts
3992 if extra_column_func is None:
3993 extras = [''] * len(hosts)
3994 else:
3995 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003996 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3997 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003998 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003999 lines.append(tmpl % he)
4000 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004001
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004002 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004003 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004004 yield ('.googlesource.com wildcard record detected',
4005 ['Chrome Infrastructure team recommends to list full host names '
4006 'explicitly.'],
4007 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004008
4009 dups = self.get_duplicated_hosts()
4010 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004011 yield ('The following hosts were defined twice',
4012 self._format_hosts(dups),
4013 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004014
4015 partial = self.get_partially_configured_hosts()
4016 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004017 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
4018 'These hosts are missing',
4019 self._format_hosts(partial, lambda host: 'but %s defined' %
4020 self._get_counterpart_host(host)),
4021 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004022
4023 conflicting = self.get_conflicting_hosts()
4024 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004025 yield ('The following Git hosts have differing credentials from their '
4026 'Gerrit counterparts',
4027 self._format_hosts(conflicting, lambda host: '%s vs %s' %
4028 tuple(self._get_git_gerrit_identity_pairs()[host])),
4029 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004030
4031 wrong = self.get_hosts_with_wrong_identities()
4032 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004033 yield ('These hosts likely use wrong identity',
4034 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
4035 (self._get_git_gerrit_identity_pairs()[host][0],
4036 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
4037 wrong)
4038
4039 def find_and_report_problems(self):
4040 """Returns True if there was at least one problem, else False."""
4041 found = False
4042 bad_hosts = set()
4043 for title, sublines, hosts in self._find_problems():
4044 if not found:
4045 found = True
4046 print('\n\n.gitcookies problem report:\n')
4047 bad_hosts.update(hosts or [])
4048 print(' %s%s' % (title , (':' if sublines else '')))
4049 if sublines:
4050 print()
4051 print(' %s' % '\n '.join(sublines))
4052 print()
4053
4054 if bad_hosts:
4055 assert found
4056 print(' You can manually remove corresponding lines in your %s file and '
4057 'visit the following URLs with correct account to generate '
4058 'correct credential lines:\n' %
4059 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
4060 print(' %s' % '\n '.join(sorted(set(
4061 gerrit_util.CookiesAuthenticator().get_new_password_url(
4062 self._canonical_git_googlesource_host(host))
4063 for host in bad_hosts
4064 ))))
4065 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004066
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004067
4068def CMDcreds_check(parser, args):
4069 """Checks credentials and suggests changes."""
4070 _, _ = parser.parse_args(args)
4071
4072 if gerrit_util.GceAuthenticator.is_gce():
Aaron Gabled10ca0e2017-09-11 11:24:10 -07004073 DieWithError(
4074 'This command is not designed for GCE, are you on a bot?\n'
4075 'If you need to run this, export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004076
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004077 checker = _GitCookiesChecker()
4078 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004079
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004080 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004081 checker.print_current_creds(include_netrc=True)
4082
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004083 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004084 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004085 return 0
4086 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004087
4088
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004089@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004090def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004091 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004092
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004093 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07004094 # TODO(tandrii): remove this once we switch to Gerrit.
4095 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00004096 parser.add_option('--activate-update', action='store_true',
4097 help='activate auto-updating [rietveld] section in '
4098 '.git/config')
4099 parser.add_option('--deactivate-update', action='store_true',
4100 help='deactivate auto-updating [rietveld] section in '
4101 '.git/config')
4102 options, args = parser.parse_args(args)
4103
4104 if options.deactivate_update:
4105 RunGit(['config', 'rietveld.autoupdate', 'false'])
4106 return
4107
4108 if options.activate_update:
4109 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4110 return
4111
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004112 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004113 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004114 return 0
4115
4116 url = args[0]
4117 if not url.endswith('codereview.settings'):
4118 url = os.path.join(url, 'codereview.settings')
4119
4120 # Load code review settings and download hooks (if available).
4121 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4122 return 0
4123
4124
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004125def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004126 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004127 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4128 branch = ShortBranchName(branchref)
4129 _, args = parser.parse_args(args)
4130 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004131 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004132 return RunGit(['config', 'branch.%s.base-url' % branch],
4133 error_ok=False).strip()
4134 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004135 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004136 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4137 error_ok=False).strip()
4138
4139
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004140def color_for_status(status):
4141 """Maps a Changelist status to color, for CMDstatus and other tools."""
4142 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004143 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004144 'waiting': Fore.BLUE,
4145 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004146 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004147 'lgtm': Fore.GREEN,
4148 'commit': Fore.MAGENTA,
4149 'closed': Fore.CYAN,
4150 'error': Fore.WHITE,
4151 }.get(status, Fore.WHITE)
4152
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004153
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004154def get_cl_statuses(changes, fine_grained, max_processes=None):
4155 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004156
4157 If fine_grained is true, this will fetch CL statuses from the server.
4158 Otherwise, simply indicate if there's a matching url for the given branches.
4159
4160 If max_processes is specified, it is used as the maximum number of processes
4161 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4162 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004163
4164 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004165 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004166 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004167 upload.verbosity = 0
4168
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004169 if not changes:
4170 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004171
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004172 if not fine_grained:
4173 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004174 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004175 for cl in changes:
4176 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004177 return
4178
4179 # First, sort out authentication issues.
4180 logging.debug('ensuring credentials exist')
4181 for cl in changes:
4182 cl.EnsureAuthenticated(force=False, refresh=True)
4183
4184 def fetch(cl):
4185 try:
4186 return (cl, cl.GetStatus())
4187 except:
4188 # See http://crbug.com/629863.
4189 logging.exception('failed to fetch status for %s:', cl)
4190 raise
4191
4192 threads_count = len(changes)
4193 if max_processes:
4194 threads_count = max(1, min(threads_count, max_processes))
4195 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4196
4197 pool = ThreadPool(threads_count)
4198 fetched_cls = set()
4199 try:
4200 it = pool.imap_unordered(fetch, changes).__iter__()
4201 while True:
4202 try:
4203 cl, status = it.next(timeout=5)
4204 except multiprocessing.TimeoutError:
4205 break
4206 fetched_cls.add(cl)
4207 yield cl, status
4208 finally:
4209 pool.close()
4210
4211 # Add any branches that failed to fetch.
4212 for cl in set(changes) - fetched_cls:
4213 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004214
rmistry@google.com2dd99862015-06-22 12:22:18 +00004215
4216def upload_branch_deps(cl, args):
4217 """Uploads CLs of local branches that are dependents of the current branch.
4218
4219 If the local branch dependency tree looks like:
4220 test1 -> test2.1 -> test3.1
4221 -> test3.2
4222 -> test2.2 -> test3.3
4223
4224 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4225 run on the dependent branches in this order:
4226 test2.1, test3.1, test3.2, test2.2, test3.3
4227
4228 Note: This function does not rebase your local dependent branches. Use it when
4229 you make a change to the parent branch that will not conflict with its
4230 dependent branches, and you would like their dependencies updated in
4231 Rietveld.
4232 """
4233 if git_common.is_dirty_git_tree('upload-branch-deps'):
4234 return 1
4235
4236 root_branch = cl.GetBranch()
4237 if root_branch is None:
4238 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4239 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004240 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004241 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4242 'patchset dependencies without an uploaded CL.')
4243
4244 branches = RunGit(['for-each-ref',
4245 '--format=%(refname:short) %(upstream:short)',
4246 'refs/heads'])
4247 if not branches:
4248 print('No local branches found.')
4249 return 0
4250
4251 # Create a dictionary of all local branches to the branches that are dependent
4252 # on it.
4253 tracked_to_dependents = collections.defaultdict(list)
4254 for b in branches.splitlines():
4255 tokens = b.split()
4256 if len(tokens) == 2:
4257 branch_name, tracked = tokens
4258 tracked_to_dependents[tracked].append(branch_name)
4259
vapiera7fbd5a2016-06-16 09:17:49 -07004260 print()
4261 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004262 dependents = []
4263 def traverse_dependents_preorder(branch, padding=''):
4264 dependents_to_process = tracked_to_dependents.get(branch, [])
4265 padding += ' '
4266 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004267 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004268 dependents.append(dependent)
4269 traverse_dependents_preorder(dependent, padding)
4270 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004271 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004272
4273 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004274 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004275 return 0
4276
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004277 confirm_or_exit('This command will checkout all dependent branches and run '
4278 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004279
andybons@chromium.org962f9462016-02-03 20:00:42 +00004280 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004281 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004282 args.extend(['-t', 'Updated patchset dependency'])
4283
rmistry@google.com2dd99862015-06-22 12:22:18 +00004284 # Record all dependents that failed to upload.
4285 failures = {}
4286 # Go through all dependents, checkout the branch and upload.
4287 try:
4288 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004289 print()
4290 print('--------------------------------------')
4291 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004292 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004293 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004294 try:
4295 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004296 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004297 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004298 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004299 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004300 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004301 finally:
4302 # Swap back to the original root branch.
4303 RunGit(['checkout', '-q', root_branch])
4304
vapiera7fbd5a2016-06-16 09:17:49 -07004305 print()
4306 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004307 for dependent_branch in dependents:
4308 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004309 print(' %s : %s' % (dependent_branch, upload_status))
4310 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004311
4312 return 0
4313
4314
kmarshall3bff56b2016-06-06 18:31:47 -07004315def CMDarchive(parser, args):
4316 """Archives and deletes branches associated with closed changelists."""
4317 parser.add_option(
4318 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004319 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004320 parser.add_option(
4321 '-f', '--force', action='store_true',
4322 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004323 parser.add_option(
4324 '-d', '--dry-run', action='store_true',
4325 help='Skip the branch tagging and removal steps.')
4326 parser.add_option(
4327 '-t', '--notags', action='store_true',
4328 help='Do not tag archived branches. '
4329 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004330
4331 auth.add_auth_options(parser)
4332 options, args = parser.parse_args(args)
4333 if args:
4334 parser.error('Unsupported args: %s' % ' '.join(args))
4335 auth_config = auth.extract_auth_config_from_options(options)
4336
4337 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4338 if not branches:
4339 return 0
4340
vapiera7fbd5a2016-06-16 09:17:49 -07004341 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004342 changes = [Changelist(branchref=b, auth_config=auth_config)
4343 for b in branches.splitlines()]
4344 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4345 statuses = get_cl_statuses(changes,
4346 fine_grained=True,
4347 max_processes=options.maxjobs)
4348 proposal = [(cl.GetBranch(),
4349 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4350 for cl, status in statuses
4351 if status == 'closed']
4352 proposal.sort()
4353
4354 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004355 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004356 return 0
4357
4358 current_branch = GetCurrentBranch()
4359
vapiera7fbd5a2016-06-16 09:17:49 -07004360 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004361 if options.notags:
4362 for next_item in proposal:
4363 print(' ' + next_item[0])
4364 else:
4365 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4366 for next_item in proposal:
4367 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004368
kmarshall9249e012016-08-23 12:02:16 -07004369 # Quit now on precondition failure or if instructed by the user, either
4370 # via an interactive prompt or by command line flags.
4371 if options.dry_run:
4372 print('\nNo changes were made (dry run).\n')
4373 return 0
4374 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004375 print('You are currently on a branch \'%s\' which is associated with a '
4376 'closed codereview issue, so archive cannot proceed. Please '
4377 'checkout another branch and run this command again.' %
4378 current_branch)
4379 return 1
kmarshall9249e012016-08-23 12:02:16 -07004380 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004381 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4382 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004383 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004384 return 1
4385
4386 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004387 if not options.notags:
4388 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004389 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004390
vapiera7fbd5a2016-06-16 09:17:49 -07004391 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004392
4393 return 0
4394
4395
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004396def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004397 """Show status of changelists.
4398
4399 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004400 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004401 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004402 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004403 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004404 - Magenta in the commit queue
4405 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004406 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004407
4408 Also see 'git cl comments'.
4409 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004410 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004411 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004412 parser.add_option('-f', '--fast', action='store_true',
4413 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004414 parser.add_option(
4415 '-j', '--maxjobs', action='store', type=int,
4416 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004417
4418 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004419 _add_codereview_issue_select_options(
4420 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004421 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004422 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004423 if args:
4424 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004425 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004426
iannuccie53c9352016-08-17 14:40:40 -07004427 if options.issue is not None and not options.field:
4428 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004429
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004430 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004431 cl = Changelist(auth_config=auth_config, issue=options.issue,
4432 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004433 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004434 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004435 elif options.field == 'id':
4436 issueid = cl.GetIssue()
4437 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004438 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004439 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004440 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004441 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004442 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004443 elif options.field == 'status':
4444 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004445 elif options.field == 'url':
4446 url = cl.GetIssueURL()
4447 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004448 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004449 return 0
4450
4451 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4452 if not branches:
4453 print('No local branch found.')
4454 return 0
4455
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004456 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004457 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004458 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004459 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004460 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004461 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004462 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004463
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004464 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004465 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4466 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4467 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004468 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004469 c, status = output.next()
4470 branch_statuses[c.GetBranch()] = status
4471 status = branch_statuses.pop(branch)
4472 url = cl.GetIssueURL()
4473 if url and (not status or status == 'error'):
4474 # The issue probably doesn't exist anymore.
4475 url += ' (broken)'
4476
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004477 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004478 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004479 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004480 color = ''
4481 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004482 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004483 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004484 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004485 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004486
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004487
4488 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004489 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004490 print('Current branch: %s' % branch)
4491 for cl in changes:
4492 if cl.GetBranch() == branch:
4493 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004494 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004495 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004496 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004497 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004498 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004499 print('Issue description:')
4500 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004501 return 0
4502
4503
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004504def colorize_CMDstatus_doc():
4505 """To be called once in main() to add colors to git cl status help."""
4506 colors = [i for i in dir(Fore) if i[0].isupper()]
4507
4508 def colorize_line(line):
4509 for color in colors:
4510 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004511 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004512 indent = len(line) - len(line.lstrip(' ')) + 1
4513 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4514 return line
4515
4516 lines = CMDstatus.__doc__.splitlines()
4517 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4518
4519
phajdan.jre328cf92016-08-22 04:12:17 -07004520def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004521 if path == '-':
4522 json.dump(contents, sys.stdout)
4523 else:
4524 with open(path, 'w') as f:
4525 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004526
4527
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004528@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004529def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004530 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004531
4532 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004533 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004534 parser.add_option('-r', '--reverse', action='store_true',
4535 help='Lookup the branch(es) for the specified issues. If '
4536 'no issues are specified, all branches with mapped '
4537 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004538 parser.add_option('--json',
4539 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004540 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004541 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004542 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004543
dnj@chromium.org406c4402015-03-03 17:22:28 +00004544 if options.reverse:
4545 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004546 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004547 # Reverse issue lookup.
4548 issue_branch_map = {}
4549 for branch in branches:
4550 cl = Changelist(branchref=branch)
4551 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
4552 if not args:
4553 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004554 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004555 for issue in args:
4556 if not issue:
4557 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004558 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004559 print('Branch for issue number %s: %s' % (
4560 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004561 if options.json:
4562 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004563 return 0
4564
4565 if len(args) > 0:
4566 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4567 if not issue.valid:
4568 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4569 'or no argument to list it.\n'
4570 'Maybe you want to run git cl status?')
4571 cl = Changelist(codereview=issue.codereview)
4572 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004573 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004574 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004575 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4576 if options.json:
4577 write_json(options.json, {
4578 'issue': cl.GetIssue(),
4579 'issue_url': cl.GetIssueURL(),
4580 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004581 return 0
4582
4583
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004584def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004585 """Shows or posts review comments for any changelist."""
4586 parser.add_option('-a', '--add-comment', dest='comment',
4587 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004588 parser.add_option('-i', '--issue', dest='issue',
4589 help='review issue id (defaults to current issue). '
4590 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004591 parser.add_option('-m', '--machine-readable', dest='readable',
4592 action='store_false', default=True,
4593 help='output comments in a format compatible with '
4594 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004595 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004596 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004597 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004598 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004599 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004600 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004601 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004602
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004603 issue = None
4604 if options.issue:
4605 try:
4606 issue = int(options.issue)
4607 except ValueError:
4608 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004609 if not options.forced_codereview:
4610 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004611
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004612 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004613 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004614 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004615
4616 if options.comment:
4617 cl.AddComment(options.comment)
4618 return 0
4619
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004620 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4621 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004622 for comment in summary:
4623 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004624 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004625 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004626 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004627 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004628 color = Fore.MAGENTA
4629 else:
4630 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004631 print('\n%s%s %s%s\n%s' % (
4632 color,
4633 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4634 comment.sender,
4635 Fore.RESET,
4636 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4637
smut@google.comc85ac942015-09-15 16:34:43 +00004638 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004639 def pre_serialize(c):
4640 dct = c.__dict__.copy()
4641 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4642 return dct
smut@google.comc85ac942015-09-15 16:34:43 +00004643 with open(options.json_file, 'wb') as f:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004644 json.dump(map(pre_serialize, summary), f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004645 return 0
4646
4647
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004648@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004649def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004650 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004651 parser.add_option('-d', '--display', action='store_true',
4652 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004653 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004654 help='New description to set for this issue (- for stdin, '
4655 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004656 parser.add_option('-f', '--force', action='store_true',
4657 help='Delete any unpublished Gerrit edits for this issue '
4658 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004659
4660 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004661 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004662 options, args = parser.parse_args(args)
4663 _process_codereview_select_options(parser, options)
4664
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004665 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004666 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004667 target_issue_arg = ParseIssueNumberArgument(args[0],
4668 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004669 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004670 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004671
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004672 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004673
martiniss6eda05f2016-06-30 10:18:35 -07004674 kwargs = {
4675 'auth_config': auth_config,
4676 'codereview': options.forced_codereview,
4677 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004678 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004679 if target_issue_arg:
4680 kwargs['issue'] = target_issue_arg.issue
4681 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004682 if target_issue_arg.codereview and not options.forced_codereview:
4683 detected_codereview_from_url = True
4684 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004685
4686 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004687 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004688 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004689 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004690
4691 if detected_codereview_from_url:
4692 logging.info('canonical issue/change URL: %s (type: %s)\n',
4693 cl.GetIssueURL(), target_issue_arg.codereview)
4694
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004695 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004696
smut@google.com34fb6b12015-07-13 20:03:26 +00004697 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004698 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004699 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004700
4701 if options.new_description:
4702 text = options.new_description
4703 if text == '-':
4704 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004705 elif text == '+':
4706 base_branch = cl.GetCommonAncestorWithUpstream()
4707 change = cl.GetChange(base_branch, None, local_description=True)
4708 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004709
4710 description.set_description(text)
4711 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004712 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004713
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004714 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004715 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004716 return 0
4717
4718
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004719def CreateDescriptionFromLog(args):
4720 """Pulls out the commit log to use as a base for the CL description."""
4721 log_args = []
4722 if len(args) == 1 and not args[0].endswith('.'):
4723 log_args = [args[0] + '..']
4724 elif len(args) == 1 and args[0].endswith('...'):
4725 log_args = [args[0][:-1]]
4726 elif len(args) == 2:
4727 log_args = [args[0] + '..' + args[1]]
4728 else:
4729 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004730 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004731
4732
thestig@chromium.org44202a22014-03-11 19:22:18 +00004733def CMDlint(parser, args):
4734 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004735 parser.add_option('--filter', action='append', metavar='-x,+y',
4736 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004737 auth.add_auth_options(parser)
4738 options, args = parser.parse_args(args)
4739 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004740
4741 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004742 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004743 try:
4744 import cpplint
4745 import cpplint_chromium
4746 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004747 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004748 return 1
4749
4750 # Change the current working directory before calling lint so that it
4751 # shows the correct base.
4752 previous_cwd = os.getcwd()
4753 os.chdir(settings.GetRoot())
4754 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004755 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004756 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4757 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004758 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004759 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004760 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004761
4762 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004763 command = args + files
4764 if options.filter:
4765 command = ['--filter=' + ','.join(options.filter)] + command
4766 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004767
4768 white_regex = re.compile(settings.GetLintRegex())
4769 black_regex = re.compile(settings.GetLintIgnoreRegex())
4770 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4771 for filename in filenames:
4772 if white_regex.match(filename):
4773 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004774 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004775 else:
4776 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4777 extra_check_functions)
4778 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004779 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004780 finally:
4781 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004782 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004783 if cpplint._cpplint_state.error_count != 0:
4784 return 1
4785 return 0
4786
4787
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004788def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004789 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004790 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004791 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004792 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004793 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004794 parser.add_option('--all', action='store_true',
4795 help='Run checks against all files, not just modified ones')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004796 auth.add_auth_options(parser)
4797 options, args = parser.parse_args(args)
4798 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004799
sbc@chromium.org71437c02015-04-09 19:29:40 +00004800 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004801 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004802 return 1
4803
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004804 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004805 if args:
4806 base_branch = args[0]
4807 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004808 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004809 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004810
Aaron Gable8076c282017-11-29 14:39:41 -08004811 if options.all:
4812 base_change = cl.GetChange(base_branch, None)
4813 files = [('M', f) for f in base_change.AllFiles()]
4814 change = presubmit_support.GitChange(
4815 base_change.Name(),
4816 base_change.FullDescriptionText(),
4817 base_change.RepositoryRoot(),
4818 files,
4819 base_change.issue,
4820 base_change.patchset,
4821 base_change.author_email,
4822 base_change._upstream)
4823 else:
4824 change = cl.GetChange(base_branch, None)
4825
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004826 cl.RunHook(
4827 committing=not options.upload,
4828 may_prompt=False,
4829 verbose=options.verbose,
Aaron Gable8076c282017-11-29 14:39:41 -08004830 change=change)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004831 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004832
4833
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004834def GenerateGerritChangeId(message):
4835 """Returns Ixxxxxx...xxx change id.
4836
4837 Works the same way as
4838 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4839 but can be called on demand on all platforms.
4840
4841 The basic idea is to generate git hash of a state of the tree, original commit
4842 message, author/committer info and timestamps.
4843 """
4844 lines = []
4845 tree_hash = RunGitSilent(['write-tree'])
4846 lines.append('tree %s' % tree_hash.strip())
4847 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4848 if code == 0:
4849 lines.append('parent %s' % parent.strip())
4850 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4851 lines.append('author %s' % author.strip())
4852 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4853 lines.append('committer %s' % committer.strip())
4854 lines.append('')
4855 # Note: Gerrit's commit-hook actually cleans message of some lines and
4856 # whitespace. This code is not doing this, but it clearly won't decrease
4857 # entropy.
4858 lines.append(message)
4859 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4860 stdin='\n'.join(lines))
4861 return 'I%s' % change_hash.strip()
4862
4863
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004864def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004865 """Computes the remote branch ref to use for the CL.
4866
4867 Args:
4868 remote (str): The git remote for the CL.
4869 remote_branch (str): The git remote branch for the CL.
4870 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004871 """
4872 if not (remote and remote_branch):
4873 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004874
wittman@chromium.org455dc922015-01-26 20:15:50 +00004875 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004876 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004877 # refs, which are then translated into the remote full symbolic refs
4878 # below.
4879 if '/' not in target_branch:
4880 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4881 else:
4882 prefix_replacements = (
4883 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4884 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4885 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4886 )
4887 match = None
4888 for regex, replacement in prefix_replacements:
4889 match = re.search(regex, target_branch)
4890 if match:
4891 remote_branch = target_branch.replace(match.group(0), replacement)
4892 break
4893 if not match:
4894 # This is a branch path but not one we recognize; use as-is.
4895 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004896 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4897 # Handle the refs that need to land in different refs.
4898 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004899
wittman@chromium.org455dc922015-01-26 20:15:50 +00004900 # Create the true path to the remote branch.
4901 # Does the following translation:
4902 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4903 # * refs/remotes/origin/master -> refs/heads/master
4904 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4905 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4906 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4907 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4908 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4909 'refs/heads/')
4910 elif remote_branch.startswith('refs/remotes/branch-heads'):
4911 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004912
wittman@chromium.org455dc922015-01-26 20:15:50 +00004913 return remote_branch
4914
4915
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004916def cleanup_list(l):
4917 """Fixes a list so that comma separated items are put as individual items.
4918
4919 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4920 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4921 """
4922 items = sum((i.split(',') for i in l), [])
4923 stripped_items = (i.strip() for i in items)
4924 return sorted(filter(None, stripped_items))
4925
4926
Aaron Gable4db38df2017-11-03 14:59:07 -07004927@subcommand.usage('[flags]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004928def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004929 """Uploads the current changelist to codereview.
4930
4931 Can skip dependency patchset uploads for a branch by running:
4932 git config branch.branch_name.skip-deps-uploads True
4933 To unset run:
4934 git config --unset branch.branch_name.skip-deps-uploads
4935 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004936
4937 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4938 a bug number, this bug number is automatically populated in the CL
4939 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004940
4941 If subject contains text in square brackets or has "<text>: " prefix, such
4942 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4943 [git-cl] add support for hashtags
4944 Foo bar: implement foo
4945 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004946 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004947 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4948 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004949 parser.add_option('--bypass-watchlists', action='store_true',
4950 dest='bypass_watchlists',
4951 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004952 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004953 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004954 parser.add_option('--message', '-m', dest='message',
4955 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004956 parser.add_option('-b', '--bug',
4957 help='pre-populate the bug number(s) for this issue. '
4958 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004959 parser.add_option('--message-file', dest='message_file',
4960 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004961 parser.add_option('--title', '-t', dest='title',
4962 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004963 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004964 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004965 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004966 parser.add_option('--tbrs',
4967 action='append', default=[],
4968 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004969 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004970 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004971 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004972 parser.add_option('--hashtag', dest='hashtags',
4973 action='append', default=[],
4974 help=('Gerrit hashtag for new CL; '
4975 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004976 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004977 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004978 parser.add_option('--emulate_svn_auto_props',
4979 '--emulate-svn-auto-props',
4980 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004981 dest="emulate_svn_auto_props",
4982 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004983 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004984 help='tell the commit queue to commit this patchset; '
4985 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004986 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004987 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004988 metavar='TARGET',
4989 help='Apply CL to remote ref TARGET. ' +
4990 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004991 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004992 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004993 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004994 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004995 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004996 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004997 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4998 const='TBR', help='add a set of OWNERS to TBR')
4999 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
5000 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00005001 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
5002 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00005003 help='Send the patchset to do a CQ dry run right after '
5004 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00005005 parser.add_option('--dependencies', action='store_true',
5006 help='Uploads CLs of all the local branches that depend on '
5007 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04005008 parser.add_option('-a', '--enable-auto-submit', action='store_true',
5009 help='Sends your change to the CQ after an approval. Only '
5010 'works on repos that have the Auto-Submit label '
5011 'enabled')
pgervais@chromium.org91141372014-01-09 23:27:20 +00005012
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005013 # TODO: remove Rietveld flags
5014 parser.add_option('--private', action='store_true',
5015 help='set the review private (rietveld only)')
5016 parser.add_option('--email', default=None,
5017 help='email address to use to connect to Rietveld')
5018
rmistry@google.com2dd99862015-06-22 12:22:18 +00005019 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005020 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005021 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005022 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005023 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005024 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005025
sbc@chromium.org71437c02015-04-09 19:29:40 +00005026 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00005027 return 1
5028
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005029 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005030 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005031 options.cc = cleanup_list(options.cc)
5032
tandriib80458a2016-06-23 12:20:07 -07005033 if options.message_file:
5034 if options.message:
5035 parser.error('only one of --message and --message-file allowed.')
5036 options.message = gclient_utils.FileRead(options.message_file)
5037 options.message_file = None
5038
tandrii4d0545a2016-07-06 03:56:49 -07005039 if options.cq_dry_run and options.use_commit_queue:
5040 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
5041
Aaron Gableedbc4132017-09-11 13:22:28 -07005042 if options.use_commit_queue:
5043 options.send_mail = True
5044
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00005045 # For sanity of test expectations, do this otherwise lazy-loading *now*.
5046 settings.GetIsGerrit()
5047
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005048 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00005049 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005050
5051
Francois Dorayd42c6812017-05-30 15:10:20 -04005052@subcommand.usage('--description=<description file>')
5053def CMDsplit(parser, args):
5054 """Splits a branch into smaller branches and uploads CLs.
5055
5056 Creates a branch and uploads a CL for each group of files modified in the
5057 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005058 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04005059 the shared OWNERS file.
5060 """
5061 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05005062 help="A text file containing a CL description in which "
5063 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005064 parser.add_option("-c", "--comment", dest="comment_file",
5065 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11005066 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
5067 default=False,
5068 help="List the files and reviewers for each CL that would "
5069 "be created, but don't create branches or CLs.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005070 options, _ = parser.parse_args(args)
5071
5072 if not options.description_file:
5073 parser.error('No --description flag specified.')
5074
5075 def WrappedCMDupload(args):
5076 return CMDupload(OptionParser(), args)
5077
5078 return split_cl.SplitCl(options.description_file, options.comment_file,
Chris Watkinsba28e462017-12-13 11:22:17 +11005079 Changelist, WrappedCMDupload, options.dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04005080
5081
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005082@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005083def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005084 """DEPRECATED: Used to commit the current changelist via git-svn."""
5085 message = ('git-cl no longer supports committing to SVN repositories via '
5086 'git-svn. You probably want to use `git cl land` instead.')
5087 print(message)
5088 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005089
5090
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005091# Two special branches used by git cl land.
5092MERGE_BRANCH = 'git-cl-commit'
5093CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5094
5095
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005096@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005097def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005098 """Commits the current changelist via git.
5099
5100 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5101 upstream and closes the issue automatically and atomically.
5102
5103 Otherwise (in case of Rietveld):
5104 Squashes branch into a single commit.
5105 Updates commit message with metadata (e.g. pointer to review).
5106 Pushes the code upstream.
5107 Updates review and closes.
5108 """
5109 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5110 help='bypass upload presubmit hook')
5111 parser.add_option('-m', dest='message',
5112 help="override review description")
Aaron Gablef7543cd2017-07-20 14:26:31 -07005113 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005114 help="force yes to questions (don't prompt)")
5115 parser.add_option('-c', dest='contributor',
5116 help="external contributor for patch (appended to " +
5117 "description and used as author for git). Should be " +
5118 "formatted as 'First Last <email@example.com>'")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005119 auth.add_auth_options(parser)
5120 (options, args) = parser.parse_args(args)
5121 auth_config = auth.extract_auth_config_from_options(options)
5122
5123 cl = Changelist(auth_config=auth_config)
5124
Robert Iannucci2e73d432018-03-14 01:10:47 -07005125 if not cl.IsGerrit():
5126 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005127
Robert Iannucci2e73d432018-03-14 01:10:47 -07005128 if options.message:
5129 # This could be implemented, but it requires sending a new patch to
5130 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
5131 # Besides, Gerrit has the ability to change the commit message on submit
5132 # automatically, thus there is no need to support this option (so far?).
5133 parser.error('-m MESSAGE option is not supported for Gerrit.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005134 if options.contributor:
Robert Iannucci2e73d432018-03-14 01:10:47 -07005135 parser.error(
5136 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
5137 'Before uploading a commit to Gerrit, ensure it\'s author field is '
5138 'the contributor\'s "name <email>". If you can\'t upload such a '
5139 'commit for review, contact your repository admin and request'
5140 '"Forge-Author" permission.')
5141 if not cl.GetIssue():
5142 DieWithError('You must upload the change first to Gerrit.\n'
5143 ' If you would rather have `git cl land` upload '
5144 'automatically for you, see http://crbug.com/642759')
5145 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
5146 options.verbose)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005147
5148
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005149def PushToGitWithAutoRebase(remote, branch, original_description,
5150 git_numberer_enabled, max_attempts=3):
5151 """Pushes current HEAD commit on top of remote's branch.
5152
5153 Attempts to fetch and autorebase on push failures.
5154 Adds git number footers on the fly.
5155
5156 Returns integer code from last command.
5157 """
5158 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5159 code = 0
5160 attempts_left = max_attempts
5161 while attempts_left:
5162 attempts_left -= 1
5163 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5164
5165 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5166 # If fetch fails, retry.
5167 print('Fetching %s/%s...' % (remote, branch))
5168 code, out = RunGitWithCode(
5169 ['retry', 'fetch', remote,
5170 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5171 if code:
5172 print('Fetch failed with exit code %d.' % code)
5173 print(out.strip())
5174 continue
5175
5176 print('Cherry-picking commit on top of latest %s' % branch)
5177 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5178 suppress_stderr=True)
5179 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5180 code, out = RunGitWithCode(['cherry-pick', cherry])
5181 if code:
5182 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5183 'the following files have merge conflicts:' %
5184 (branch, parent_hash))
Aaron Gable7817f022017-12-12 09:43:17 -08005185 print(RunGit(['-c', 'core.quotePath=false', 'diff',
5186 '--name-status', '--diff-filter=U']).strip())
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005187 print('Please rebase your patch and try again.')
5188 RunGitWithCode(['cherry-pick', '--abort'])
5189 break
5190
5191 commit_desc = ChangeDescription(original_description)
5192 if git_numberer_enabled:
5193 logging.debug('Adding git number footers')
5194 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5195 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5196 branch)
5197 # Ensure timestamps are monotonically increasing.
5198 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5199 _get_committer_timestamp('HEAD'))
5200 _git_amend_head(commit_desc.description, timestamp)
5201
5202 code, out = RunGitWithCode(
5203 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5204 print(out)
5205 if code == 0:
5206 break
5207 if IsFatalPushFailure(out):
5208 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005209 'user.email are correct and you have push access to the repo.\n'
5210 'Hint: run command below to diangose common Git/Gerrit credential '
5211 'problems:\n'
5212 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005213 break
5214 return code
5215
5216
5217def IsFatalPushFailure(push_stdout):
5218 """True if retrying push won't help."""
5219 return '(prohibited by Gerrit)' in push_stdout
5220
5221
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005222@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005223def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005224 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005225 parser.add_option('-b', dest='newbranch',
5226 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005227 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005228 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005229 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005230 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005231 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005232 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005233 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005234 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005235 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005236 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005237
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005238
5239 group = optparse.OptionGroup(
5240 parser,
5241 'Options for continuing work on the current issue uploaded from a '
5242 'different clone (e.g. different machine). Must be used independently '
5243 'from the other options. No issue number should be specified, and the '
5244 'branch must have an issue number associated with it')
5245 group.add_option('--reapply', action='store_true', dest='reapply',
5246 help='Reset the branch and reapply the issue.\n'
5247 'CAUTION: This will undo any local changes in this '
5248 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005249
5250 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005251 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005252 parser.add_option_group(group)
5253
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005254 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005255 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005256 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005257 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005258 auth_config = auth.extract_auth_config_from_options(options)
5259
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005260 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005261 if options.newbranch:
5262 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005263 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005264 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005265
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005266 cl = Changelist(auth_config=auth_config,
5267 codereview=options.forced_codereview)
5268 if not cl.GetIssue():
5269 parser.error('current branch must have an associated issue')
5270
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005271 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005272 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005273 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005274
5275 RunGit(['reset', '--hard', upstream])
5276 if options.pull:
5277 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005278
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005279 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5280 options.directory)
5281
5282 if len(args) != 1 or not args[0]:
5283 parser.error('Must specify issue number or url')
5284
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005285 target_issue_arg = ParseIssueNumberArgument(args[0],
5286 options.forced_codereview)
5287 if not target_issue_arg.valid:
5288 parser.error('invalid codereview url or CL id')
5289
5290 cl_kwargs = {
5291 'auth_config': auth_config,
5292 'codereview_host': target_issue_arg.hostname,
5293 'codereview': options.forced_codereview,
5294 }
5295 detected_codereview_from_url = False
5296 if target_issue_arg.codereview and not options.forced_codereview:
5297 detected_codereview_from_url = True
5298 cl_kwargs['codereview'] = target_issue_arg.codereview
5299 cl_kwargs['issue'] = target_issue_arg.issue
5300
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005301 # We don't want uncommitted changes mixed up with the patch.
5302 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005303 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005304
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005305 if options.newbranch:
5306 if options.force:
5307 RunGit(['branch', '-D', options.newbranch],
5308 stderr=subprocess2.PIPE, error_ok=True)
5309 RunGit(['new-branch', options.newbranch])
5310
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005311 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005312
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005313 if cl.IsGerrit():
5314 if options.reject:
5315 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005316 if options.directory:
5317 parser.error('--directory is not supported with Gerrit codereview.')
5318
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005319 if detected_codereview_from_url:
5320 print('canonical issue/change URL: %s (type: %s)\n' %
5321 (cl.GetIssueURL(), target_issue_arg.codereview))
5322
5323 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005324 options.nocommit, options.directory,
5325 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005326
5327
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005328def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005329 """Fetches the tree status and returns either 'open', 'closed',
5330 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005331 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005332 if url:
5333 status = urllib2.urlopen(url).read().lower()
5334 if status.find('closed') != -1 or status == '0':
5335 return 'closed'
5336 elif status.find('open') != -1 or status == '1':
5337 return 'open'
5338 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005339 return 'unset'
5340
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005341
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005342def GetTreeStatusReason():
5343 """Fetches the tree status from a json url and returns the message
5344 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005345 url = settings.GetTreeStatusUrl()
5346 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005347 connection = urllib2.urlopen(json_url)
5348 status = json.loads(connection.read())
5349 connection.close()
5350 return status['message']
5351
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005352
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005353def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005354 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005355 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005356 status = GetTreeStatus()
5357 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005358 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005359 return 2
5360
vapiera7fbd5a2016-06-16 09:17:49 -07005361 print('The tree is %s' % status)
5362 print()
5363 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005364 if status != 'open':
5365 return 1
5366 return 0
5367
5368
maruel@chromium.org15192402012-09-06 12:38:29 +00005369def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005370 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005371 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005372 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005373 '-b', '--bot', action='append',
5374 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5375 'times to specify multiple builders. ex: '
5376 '"-b win_rel -b win_layout". See '
5377 'the try server waterfall for the builders name and the tests '
5378 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005379 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005380 '-B', '--bucket', default='',
5381 help=('Buildbucket bucket to send the try requests.'))
5382 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005383 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005384 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005385 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005386 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005387 help='Revision to use for the try job; default: the revision will '
5388 'be determined by the try recipe that builder runs, which usually '
5389 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005390 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005391 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005392 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005393 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005394 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005395 '--category', default='git_cl_try', help='Specify custom build category.')
5396 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005397 '--project',
5398 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005399 'in recipe to determine to which repository or directory to '
5400 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005401 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005402 '-p', '--property', dest='properties', action='append', default=[],
5403 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005404 'key2=value2 etc. The value will be treated as '
5405 'json if decodable, or as string otherwise. '
5406 'NOTE: using this may make your try job not usable for CQ, '
5407 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005408 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005409 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5410 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005411 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005412 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005413 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005414 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005415 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005416 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005417
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005418 if options.master and options.master.startswith('luci.'):
5419 parser.error(
5420 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005421 # Make sure that all properties are prop=value pairs.
5422 bad_params = [x for x in options.properties if '=' not in x]
5423 if bad_params:
5424 parser.error('Got properties with missing "=": %s' % bad_params)
5425
maruel@chromium.org15192402012-09-06 12:38:29 +00005426 if args:
5427 parser.error('Unknown arguments: %s' % args)
5428
Koji Ishii31c14782018-01-08 17:17:33 +09005429 cl = Changelist(auth_config=auth_config, issue=options.issue,
5430 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005431 if not cl.GetIssue():
5432 parser.error('Need to upload first')
5433
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005434 if cl.IsGerrit():
5435 # HACK: warm up Gerrit change detail cache to save on RPCs.
5436 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5437
tandriie113dfd2016-10-11 10:20:12 -07005438 error_message = cl.CannotTriggerTryJobReason()
5439 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005440 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005441
borenet6c0efe62016-10-19 08:13:29 -07005442 if options.bucket and options.master:
5443 parser.error('Only one of --bucket and --master may be used.')
5444
qyearsley1fdfcb62016-10-24 13:22:03 -07005445 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005446
qyearsleydd49f942016-10-28 11:57:22 -07005447 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5448 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005449 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005450 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005451 print('git cl try with no bots now defaults to CQ dry run.')
5452 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5453 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005454
borenet6c0efe62016-10-19 08:13:29 -07005455 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005456 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005457 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005458 'of bot requires an initial job from a parent (usually a builder). '
5459 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005460 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005461 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005462
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005463 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005464 # TODO(tandrii): Checking local patchset against remote patchset is only
5465 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5466 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005467 print('Warning: Codereview server has newer patchsets (%s) than most '
5468 'recent upload from local checkout (%s). Did a previous upload '
5469 'fail?\n'
5470 'By default, git cl try uses the latest patchset from '
5471 'codereview, continuing to use patchset %s.\n' %
5472 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005473
tandrii568043b2016-10-11 07:49:18 -07005474 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005475 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005476 except BuildbucketResponseException as ex:
5477 print('ERROR: %s' % ex)
5478 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005479 return 0
5480
5481
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005482def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005483 """Prints info about try jobs associated with current CL."""
5484 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005485 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005486 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005487 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005488 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005489 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005490 '--color', action='store_true', default=setup_color.IS_TTY,
5491 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005492 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005493 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5494 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005495 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005496 '--json', help=('Path of JSON output file to write try job results to,'
5497 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005498 parser.add_option_group(group)
5499 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005500 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005501 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005502 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005503 if args:
5504 parser.error('Unrecognized args: %s' % ' '.join(args))
5505
5506 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005507 cl = Changelist(
5508 issue=options.issue, codereview=options.forced_codereview,
5509 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005510 if not cl.GetIssue():
5511 parser.error('Need to upload first')
5512
tandrii221ab252016-10-06 08:12:04 -07005513 patchset = options.patchset
5514 if not patchset:
5515 patchset = cl.GetMostRecentPatchset()
5516 if not patchset:
5517 parser.error('Codereview doesn\'t know about issue %s. '
5518 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005519 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005520 cl.GetIssue())
5521
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005522 # TODO(tandrii): Checking local patchset against remote patchset is only
5523 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5524 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005525 print('Warning: Codereview server has newer patchsets (%s) than most '
5526 'recent upload from local checkout (%s). Did a previous upload '
5527 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005528 'By default, git cl try-results uses the latest patchset from '
5529 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005530 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005531 try:
tandrii221ab252016-10-06 08:12:04 -07005532 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005533 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005534 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005535 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005536 if options.json:
5537 write_try_results_json(options.json, jobs)
5538 else:
5539 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005540 return 0
5541
5542
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005543@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005544def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005545 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005546 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005547 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005548 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005549
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005550 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005551 if args:
5552 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005553 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005554 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005555 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005556 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005557
5558 # Clear configured merge-base, if there is one.
5559 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005560 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005561 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005562 return 0
5563
5564
thestig@chromium.org00858c82013-12-02 23:08:03 +00005565def CMDweb(parser, args):
5566 """Opens the current CL in the web browser."""
5567 _, args = parser.parse_args(args)
5568 if args:
5569 parser.error('Unrecognized args: %s' % ' '.join(args))
5570
5571 issue_url = Changelist().GetIssueURL()
5572 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005573 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005574 return 1
5575
5576 webbrowser.open(issue_url)
5577 return 0
5578
5579
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005580def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005581 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005582 parser.add_option('-d', '--dry-run', action='store_true',
5583 help='trigger in dry run mode')
5584 parser.add_option('-c', '--clear', action='store_true',
5585 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005586 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005587 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005588 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005589 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005590 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005591 if args:
5592 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005593 if options.dry_run and options.clear:
5594 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5595
iannuccie53c9352016-08-17 14:40:40 -07005596 cl = Changelist(auth_config=auth_config, issue=options.issue,
5597 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005598 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005599 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005600 elif options.dry_run:
5601 state = _CQState.DRY_RUN
5602 else:
5603 state = _CQState.COMMIT
5604 if not cl.GetIssue():
5605 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005606 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005607 return 0
5608
5609
groby@chromium.org411034a2013-02-26 15:12:01 +00005610def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005611 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005612 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005613 auth.add_auth_options(parser)
5614 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005615 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005616 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005617 if args:
5618 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005619 cl = Changelist(auth_config=auth_config, issue=options.issue,
5620 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005621 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005622 if not cl.GetIssue():
5623 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005624 cl.CloseIssue()
5625 return 0
5626
5627
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005628def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005629 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005630 parser.add_option(
5631 '--stat',
5632 action='store_true',
5633 dest='stat',
5634 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005635 auth.add_auth_options(parser)
5636 options, args = parser.parse_args(args)
5637 auth_config = auth.extract_auth_config_from_options(options)
5638 if args:
5639 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005640
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005641 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005642 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005643 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005644 if not issue:
5645 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005646
Aaron Gablea718c3e2017-08-28 17:47:28 -07005647 base = cl._GitGetBranchConfigValue('last-upload-hash')
5648 if not base:
5649 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5650 if not base:
5651 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5652 revision_info = detail['revisions'][detail['current_revision']]
5653 fetch_info = revision_info['fetch']['http']
5654 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5655 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005656
Aaron Gablea718c3e2017-08-28 17:47:28 -07005657 cmd = ['git', 'diff']
5658 if options.stat:
5659 cmd.append('--stat')
5660 cmd.append(base)
5661 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005662
5663 return 0
5664
5665
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005666def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005667 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005668 parser.add_option(
5669 '--no-color',
5670 action='store_true',
5671 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005672 parser.add_option(
5673 '--batch',
5674 action='store_true',
5675 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005676 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005677 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005678 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005679
5680 author = RunGit(['config', 'user.email']).strip() or None
5681
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005682 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005683
5684 if args:
5685 if len(args) > 1:
5686 parser.error('Unknown args')
5687 base_branch = args[0]
5688 else:
5689 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005690 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005691
5692 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005693 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5694
5695 if options.batch:
5696 db = owners.Database(change.RepositoryRoot(), file, os.path)
5697 print('\n'.join(db.reviewers_for(affected_files, author)))
5698 return 0
5699
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005700 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005701 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005702 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005703 author,
5704 cl.GetReviewers(),
5705 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005706 disable_color=options.no_color,
5707 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005708
5709
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005710def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005711 """Generates a diff command."""
5712 # Generate diff for the current branch's changes.
Aaron Gablef4068aa2017-12-12 15:14:09 -08005713 diff_cmd = ['-c', 'core.quotePath=false', 'diff',
5714 '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005715 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005716
5717 if args:
5718 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005719 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005720 diff_cmd.append(arg)
5721 else:
5722 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005723
5724 return diff_cmd
5725
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005726
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005727def MatchingFileType(file_name, extensions):
5728 """Returns true if the file name ends with one of the given extensions."""
5729 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005730
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005731
enne@chromium.org555cfe42014-01-29 18:21:39 +00005732@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005733def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005734 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005735 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005736 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005737 parser.add_option('--full', action='store_true',
5738 help='Reformat the full content of all touched files')
5739 parser.add_option('--dry-run', action='store_true',
5740 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005741 parser.add_option('--python', action='store_true',
5742 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005743 parser.add_option('--js', action='store_true',
5744 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005745 parser.add_option('--diff', action='store_true',
5746 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005747 parser.add_option('--presubmit', action='store_true',
5748 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005749 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005750
Daniel Chengc55eecf2016-12-30 03:11:02 -08005751 # Normalize any remaining args against the current path, so paths relative to
5752 # the current directory are still resolved as expected.
5753 args = [os.path.join(os.getcwd(), arg) for arg in args]
5754
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005755 # git diff generates paths against the root of the repository. Change
5756 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005757 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005758 if rel_base_path:
5759 os.chdir(rel_base_path)
5760
digit@chromium.org29e47272013-05-17 17:01:46 +00005761 # Grab the merge-base commit, i.e. the upstream commit of the current
5762 # branch when it was created or the last time it was rebased. This is
5763 # to cover the case where the user may have called "git fetch origin",
5764 # moving the origin branch to a newer commit, but hasn't rebased yet.
5765 upstream_commit = None
5766 cl = Changelist()
5767 upstream_branch = cl.GetUpstreamBranch()
5768 if upstream_branch:
5769 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5770 upstream_commit = upstream_commit.strip()
5771
5772 if not upstream_commit:
5773 DieWithError('Could not find base commit for this branch. '
5774 'Are you in detached state?')
5775
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005776 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5777 diff_output = RunGit(changed_files_cmd)
5778 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005779 # Filter out files deleted by this CL
5780 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005781
Christopher Lamc5ba6922017-01-24 11:19:14 +11005782 if opts.js:
5783 CLANG_EXTS.append('.js')
5784
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005785 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5786 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5787 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005788 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005789
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005790 top_dir = os.path.normpath(
5791 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5792
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005793 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5794 # formatted. This is used to block during the presubmit.
5795 return_value = 0
5796
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005797 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005798 # Locate the clang-format binary in the checkout
5799 try:
5800 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005801 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005802 DieWithError(e)
5803
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005804 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005805 cmd = [clang_format_tool]
5806 if not opts.dry_run and not opts.diff:
5807 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005808 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005809 if opts.diff:
5810 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005811 else:
5812 env = os.environ.copy()
5813 env['PATH'] = str(os.path.dirname(clang_format_tool))
5814 try:
5815 script = clang_format.FindClangFormatScriptInChromiumTree(
5816 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005817 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005818 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005819
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005820 cmd = [sys.executable, script, '-p0']
5821 if not opts.dry_run and not opts.diff:
5822 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005823
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005824 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5825 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005826
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005827 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5828 if opts.diff:
5829 sys.stdout.write(stdout)
5830 if opts.dry_run and len(stdout) > 0:
5831 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005832
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005833 # Similar code to above, but using yapf on .py files rather than clang-format
5834 # on C/C++ files
5835 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005836 yapf_tool = gclient_utils.FindExecutable('yapf')
5837 if yapf_tool is None:
5838 DieWithError('yapf not found in PATH')
5839
5840 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005841 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005842 cmd = [yapf_tool]
5843 if not opts.dry_run and not opts.diff:
5844 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005845 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005846 if opts.diff:
5847 sys.stdout.write(stdout)
5848 else:
5849 # TODO(sbc): yapf --lines mode still has some issues.
5850 # https://github.com/google/yapf/issues/154
5851 DieWithError('--python currently only works with --full')
5852
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005853 # Dart's formatter does not have the nice property of only operating on
5854 # modified chunks, so hard code full.
5855 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005856 try:
5857 command = [dart_format.FindDartFmtToolInChromiumTree()]
5858 if not opts.dry_run and not opts.diff:
5859 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005860 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005861
ppi@chromium.org6593d932016-03-03 15:41:15 +00005862 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005863 if opts.dry_run and stdout:
5864 return_value = 2
5865 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005866 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5867 'found in this checkout. Files in other languages are still '
5868 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005869
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005870 # Format GN build files. Always run on full build files for canonical form.
5871 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005872 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005873 if opts.dry_run or opts.diff:
5874 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005875 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005876 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5877 shell=sys.platform == 'win32',
5878 cwd=top_dir)
5879 if opts.dry_run and gn_ret == 2:
5880 return_value = 2 # Not formatted.
5881 elif opts.diff and gn_ret == 2:
5882 # TODO this should compute and print the actual diff.
5883 print("This change has GN build file diff for " + gn_diff_file)
5884 elif gn_ret != 0:
5885 # For non-dry run cases (and non-2 return values for dry-run), a
5886 # nonzero error code indicates a failure, probably because the file
5887 # doesn't parse.
5888 DieWithError("gn format failed on " + gn_diff_file +
5889 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005890
Ilya Shermane081cbe2017-08-15 17:51:04 -07005891 # Skip the metrics formatting from the global presubmit hook. These files have
5892 # a separate presubmit hook that issues an error if the files need formatting,
5893 # whereas the top-level presubmit script merely issues a warning. Formatting
5894 # these files is somewhat slow, so it's important not to duplicate the work.
5895 if not opts.presubmit:
5896 for xml_dir in GetDirtyMetricsDirs(diff_files):
5897 tool_dir = os.path.join(top_dir, xml_dir)
5898 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5899 if opts.dry_run or opts.diff:
5900 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005901 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005902 if opts.diff:
5903 sys.stdout.write(stdout)
5904 if opts.dry_run and stdout:
5905 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005906
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005907 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005908
Steven Holte2e664bf2017-04-21 13:10:47 -07005909def GetDirtyMetricsDirs(diff_files):
5910 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5911 metrics_xml_dirs = [
5912 os.path.join('tools', 'metrics', 'actions'),
5913 os.path.join('tools', 'metrics', 'histograms'),
5914 os.path.join('tools', 'metrics', 'rappor'),
5915 os.path.join('tools', 'metrics', 'ukm')]
5916 for xml_dir in metrics_xml_dirs:
5917 if any(file.startswith(xml_dir) for file in xml_diff_files):
5918 yield xml_dir
5919
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005920
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005921@subcommand.usage('<codereview url or issue id>')
5922def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005923 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005924 _, args = parser.parse_args(args)
5925
5926 if len(args) != 1:
5927 parser.print_help()
5928 return 1
5929
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005930 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005931 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005932 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005933
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005934 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005935
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005936 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005937 output = RunGit(['config', '--local', '--get-regexp',
5938 r'branch\..*\.%s' % issueprefix],
5939 error_ok=True)
5940 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005941 if issue == target_issue:
5942 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005943
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005944 branches = []
5945 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005946 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005947 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005948 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005949 return 1
5950 if len(branches) == 1:
5951 RunGit(['checkout', branches[0]])
5952 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005953 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005954 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005955 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005956 which = raw_input('Choose by index: ')
5957 try:
5958 RunGit(['checkout', branches[int(which)]])
5959 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005960 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005961 return 1
5962
5963 return 0
5964
5965
maruel@chromium.org29404b52014-09-08 22:58:00 +00005966def CMDlol(parser, args):
5967 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005968 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005969 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5970 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5971 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005972 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005973 return 0
5974
5975
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005976class OptionParser(optparse.OptionParser):
5977 """Creates the option parse and add --verbose support."""
5978 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005979 optparse.OptionParser.__init__(
5980 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005981 self.add_option(
5982 '-v', '--verbose', action='count', default=0,
5983 help='Use 2 times for more debugging info')
5984
5985 def parse_args(self, args=None, values=None):
5986 options, args = optparse.OptionParser.parse_args(self, args, values)
5987 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005988 logging.basicConfig(
5989 level=levels[min(options.verbose, len(levels) - 1)],
5990 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5991 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005992 return options, args
5993
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005994
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005995def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005996 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005997 print('\nYour python version %s is unsupported, please upgrade.\n' %
5998 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005999 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006000
maruel@chromium.orgddd59412011-11-30 14:20:38 +00006001 # Reload settings.
6002 global settings
6003 settings = Settings()
6004
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006005 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006006 dispatcher = subcommand.CommandDispatcher(__name__)
6007 try:
6008 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006009 except auth.AuthenticationError as e:
6010 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07006011 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006012 if e.code != 500:
6013 raise
6014 DieWithError(
6015 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
6016 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006017 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006018
6019
6020if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006021 # These affect sys.stdout so do it outside of main() to simplify mocks in
6022 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006023 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006024 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00006025 try:
6026 sys.exit(main(sys.argv[1:]))
6027 except KeyboardInterrupt:
6028 sys.stderr.write('interrupted\n')
6029 sys.exit(1)