blob: d2b68b7450f7cfc5ac2073c210bab100519a0011 [file] [log] [blame]
Edward Lesmes98eda3f2019-08-12 21:09:53 +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
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00008"""A git-command for integrating reviews on 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 Shyshkalovd8aa49f2017-03-17 16:05:49 +010016import datetime
sheyang@google.com6ebaf782015-05-12 19:17:54 +000017import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010018import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000019import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000021import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import optparse
23import os
24import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010025import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000026import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070028import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000030import time
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000031import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000032import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000033import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000034import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000035import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000036import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000038from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000039from third_party import httplib2
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000040import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000041import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000042import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000043import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000044import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000045import gerrit_util
iannucci@chromium.org9e849272014-04-04 00:31:55 +000046import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000047import git_footers
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000048import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000049import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000050import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000051import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000052import presubmit_support
53import scm
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000054import setup_color
Francois Dorayd42c6812017-05-30 15:10:20 -040055import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000056import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000057import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import watchlists
59
tandrii7400cf02016-06-21 08:48:07 -070060__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061
Edward Lemur0f58ae42019-04-30 17:24:12 +000062# Traces for git push will be stored in a traces directory inside the
63# depot_tools checkout.
64DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
65TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
66
67# When collecting traces, Git hashes will be reduced to 6 characters to reduce
68# the size after compression.
69GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
70# Used to redact the cookies from the gitcookies file.
71GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
72
Edward Lemurd4d1ba42019-09-20 21:46:37 +000073MAX_ATTEMPTS = 3
74
Edward Lemur1b52d872019-05-09 21:12:12 +000075# The maximum number of traces we will keep. Multiplied by 3 since we store
76# 3 files per trace.
77MAX_TRACES = 3 * 10
Edward Lemur5737f022019-05-17 01:24:00 +000078# Message to be displayed to the user to inform where to find the traces for a
79# git-cl upload execution.
Edward Lemur0f58ae42019-04-30 17:24:12 +000080TRACES_MESSAGE = (
Edward Lemur1b52d872019-05-09 21:12:12 +000081'\n'
Edward Lemur5737f022019-05-17 01:24:00 +000082'The traces of this git-cl execution have been recorded at:\n'
Edward Lemur1b52d872019-05-09 21:12:12 +000083' %(trace_name)s-traces.zip\n'
Edward Lemur5737f022019-05-17 01:24:00 +000084'Copies of your gitcookies file and git config have been recorded at:\n'
85' %(trace_name)s-git-info.zip\n')
Edward Lemur1b52d872019-05-09 21:12:12 +000086# Format of the message to be stored as part of the traces to give developers a
87# better context when they go through traces.
88TRACES_README_FORMAT = (
89'Date: %(now)s\n'
90'\n'
91'Change: https://%(gerrit_host)s/q/%(change_id)s\n'
92'Title: %(title)s\n'
93'\n'
94'%(description)s\n'
95'\n'
96'Execution time: %(execution_time)s\n'
97'Exit code: %(exit_code)s\n') + TRACES_MESSAGE
Edward Lemur0f58ae42019-04-30 17:24:12 +000098
tandrii9d2c7a32016-06-22 03:42:45 -070099COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -0800100POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000101DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +0000102REFS_THAT_ALIAS_TO_OTHER_REFS = {
103 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
104 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
105}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000106
thestig@chromium.org44202a22014-03-11 19:22:18 +0000107# Valid extensions for files we want to lint.
108DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
109DEFAULT_LINT_IGNORE_REGEX = r"$^"
110
Aiden Bennerc08566e2018-10-03 17:52:42 +0000111# File name for yapf style config files.
112YAPF_CONFIG_FILENAME = '.style.yapf'
113
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000114# Shortcut since it quickly becomes repetitive.
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000115Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000116
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000117# Initialized in main()
118settings = None
119
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100120# Used by tests/git_cl_test.py to add extra logging.
121# Inside the weirdly failing test, add this:
122# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700123# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100124_IS_BEING_TESTED = False
125
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000126
Christopher Lamf732cd52017-01-24 12:40:11 +1100127def DieWithError(message, change_desc=None):
128 if change_desc:
129 SaveDescriptionBackup(change_desc)
130
vapiera7fbd5a2016-06-16 09:17:49 -0700131 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000132 sys.exit(1)
133
134
Christopher Lamf732cd52017-01-24 12:40:11 +1100135def SaveDescriptionBackup(change_desc):
136 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000137 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100138 backup_file = open(backup_path, 'w')
139 backup_file.write(change_desc.description)
140 backup_file.close()
141
142
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000143def GetNoGitPagerEnv():
144 env = os.environ.copy()
145 # 'cat' is a magical git string that disables pagers on all platforms.
146 env['GIT_PAGER'] = 'cat'
147 return env
148
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000149
bsep@chromium.org627d9002016-04-29 00:00:52 +0000150def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000151 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000152 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000153 except subprocess2.CalledProcessError as e:
154 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000155 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000156 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000157 'Command "%s" failed.\n%s' % (
158 ' '.join(args), error_message or e.stdout or ''))
159 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000160
161
162def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000163 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000164 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000165
166
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000167def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000168 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700169 if suppress_stderr:
170 stderr = subprocess2.VOID
171 else:
172 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000173 try:
tandrii5d48c322016-08-18 16:19:37 -0700174 (out, _), code = subprocess2.communicate(['git'] + args,
175 env=GetNoGitPagerEnv(),
176 stdout=subprocess2.PIPE,
177 stderr=stderr)
178 return code, out
179 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900180 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700181 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000182
183
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000184def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000185 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000186 return RunGitWithCode(args, suppress_stderr=True)[1]
187
188
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000189def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000190 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000191 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000192 return (version.startswith(prefix) and
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000193 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000194
195
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000196def BranchExists(branch):
197 """Return True if specified branch exists."""
198 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
199 suppress_stderr=True)
200 return not code
201
202
tandrii2a16b952016-10-19 07:09:44 -0700203def time_sleep(seconds):
204 # Use this so that it can be mocked in tests without interfering with python
205 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700206 return time.sleep(seconds)
207
208
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000209def time_time():
210 # Use this so that it can be mocked in tests without interfering with python
211 # system machinery.
212 return time.time()
213
214
Edward Lemur1b52d872019-05-09 21:12:12 +0000215def datetime_now():
216 # Use this so that it can be mocked in tests without interfering with python
217 # system machinery.
218 return datetime.datetime.now()
219
220
maruel@chromium.org90541732011-04-01 17:54:18 +0000221def ask_for_data(prompt):
222 try:
223 return raw_input(prompt)
224 except KeyboardInterrupt:
225 # Hide the exception.
226 sys.exit(1)
227
228
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100229def confirm_or_exit(prefix='', action='confirm'):
230 """Asks user to press enter to continue or press Ctrl+C to abort."""
231 if not prefix or prefix.endswith('\n'):
232 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100233 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100234 mid = ' Press'
235 elif prefix.endswith(' '):
236 mid = 'press'
237 else:
238 mid = ' press'
239 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
240
241
242def ask_for_explicit_yes(prompt):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000243 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100244 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
245 while True:
246 if 'yes'.startswith(result):
247 return True
248 if 'no'.startswith(result):
249 return False
250 result = ask_for_data('Please, type yes or no: ').lower()
251
252
tandrii5d48c322016-08-18 16:19:37 -0700253def _git_branch_config_key(branch, key):
254 """Helper method to return Git config key for a branch."""
255 assert branch, 'branch name is required to set git config for it'
256 return 'branch.%s.%s' % (branch, key)
257
258
259def _git_get_branch_config_value(key, default=None, value_type=str,
260 branch=False):
261 """Returns git config value of given or current branch if any.
262
263 Returns default in all other cases.
264 """
265 assert value_type in (int, str, bool)
266 if branch is False: # Distinguishing default arg value from None.
267 branch = GetCurrentBranch()
268
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000269 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700270 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000271
tandrii5d48c322016-08-18 16:19:37 -0700272 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700273 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700274 args.append('--bool')
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000275 # `git config` also has --int, but apparently git config suffers from integer
tandrii33a46ff2016-08-23 05:53:40 -0700276 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700277 args.append(_git_branch_config_key(branch, key))
278 code, out = RunGitWithCode(args)
279 if code == 0:
280 value = out.strip()
281 if value_type == int:
282 return int(value)
283 if value_type == bool:
284 return bool(value.lower() == 'true')
285 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000286 return default
287
288
tandrii5d48c322016-08-18 16:19:37 -0700289def _git_set_branch_config_value(key, value, branch=None, **kwargs):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000290 """Sets or unsets the git branch config value.
tandrii5d48c322016-08-18 16:19:37 -0700291
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000292 If value is None, the key will be unset, otherwise it will be set.
293 If no branch is given, the currently checked out branch is used.
tandrii5d48c322016-08-18 16:19:37 -0700294 """
295 if not branch:
296 branch = GetCurrentBranch()
297 assert branch, 'a branch name OR currently checked out branch is required'
298 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700299 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700300 if value is None:
301 args.append('--unset')
302 elif isinstance(value, bool):
303 args.append('--bool')
304 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700305 else:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000306 # `git config` also has --int, but apparently git config suffers from
307 # integer overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700308 value = str(value)
309 args.append(_git_branch_config_key(branch, key))
310 if value is not None:
311 args.append(value)
312 RunGit(args, **kwargs)
313
314
machenbach@chromium.org45453142015-09-15 08:45:22 +0000315def _get_properties_from_options(options):
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000316 prop_list = getattr(options, 'properties', [])
317 properties = dict(x.split('=', 1) for x in prop_list)
machenbach@chromium.org45453142015-09-15 08:45:22 +0000318 for key, val in properties.iteritems():
319 try:
320 properties[key] = json.loads(val)
321 except ValueError:
322 pass # If a value couldn't be evaluated, treat it as a string.
323 return properties
324
325
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000326# TODO(crbug.com/976104): Remove this function once git-cl try-results has
327# migrated to use buildbucket v2
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000328def _buildbucket_retry(operation_name, http, *args, **kwargs):
329 """Retries requests to buildbucket service and returns parsed json content."""
330 try_count = 0
331 while True:
332 response, content = http.request(*args, **kwargs)
333 try:
334 content_json = json.loads(content)
335 except ValueError:
336 content_json = None
337
338 # Buildbucket could return an error even if status==200.
339 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000340 error = content_json.get('error')
341 if error.get('code') == 403:
342 raise BuildbucketResponseException(
343 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000344 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000345 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000346 raise BuildbucketResponseException(msg)
347
348 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700349 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000350 raise BuildbucketResponseException(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000351 'Buildbucket returned invalid JSON content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700352 'Please file bugs at http://crbug.com, '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000353 'component "Infra>Platform>Buildbucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000354 content)
355 return content_json
356 if response.status < 500 or try_count >= 2:
357 raise httplib2.HttpLib2Error(content)
358
359 # status >= 500 means transient failures.
360 logging.debug('Transient errors when %s. Will retry.', operation_name)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000361 time_sleep(0.5 + (1.5 * try_count))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000362 try_count += 1
363 assert False, 'unreachable'
364
365
Edward Lemur4c707a22019-09-24 21:13:43 +0000366def _call_buildbucket(http, buildbucket_host, method, request):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000367 """Calls a buildbucket v2 method and returns the parsed json response."""
368 headers = {
369 'Accept': 'application/json',
370 'Content-Type': 'application/json',
371 }
372 request = json.dumps(request)
373 url = 'https://%s/prpc/buildbucket.v2.Builds/%s' % (buildbucket_host, method)
374
375 logging.info('POST %s with %s' % (url, request))
376
377 attempts = 1
378 time_to_sleep = 1
379 while True:
380 response, content = http.request(url, 'POST', body=request, headers=headers)
381 if response.status == 200:
382 return json.loads(content[4:])
383 if attempts >= MAX_ATTEMPTS or 400 <= response.status < 500:
384 msg = '%s error when calling POST %s with %s: %s' % (
385 response.status, url, request, content)
386 raise BuildbucketResponseException(msg)
387 logging.debug(
388 '%s error when calling POST %s with %s. '
389 'Sleeping for %d seconds and retrying...' % (
390 response.status, url, request, time_to_sleep))
391 time.sleep(time_to_sleep)
392 time_to_sleep *= 2
393 attempts += 1
394
395 assert False, 'unreachable'
396
397
qyearsley1fdfcb62016-10-24 13:22:03 -0700398def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700399 """Returns a dict mapping bucket names to builders and tests,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000400 for triggering tryjobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700401 """
qyearsleydd49f942016-10-28 11:57:22 -0700402 # If no bots are listed, we try to get a set of builders and tests based
403 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700404 if not options.bot:
405 change = changelist.GetChange(
406 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700407 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700408 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700409 change=change,
410 changed_files=change.LocalPaths(),
411 repository_root=settings.GetRoot(),
412 default_presubmit=None,
413 project=None,
414 verbose=options.verbose,
415 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700416 if masters is None:
417 return None
Edward Lemurc8b67ed2019-09-12 20:28:58 +0000418 return {m: b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700419
qyearsley1fdfcb62016-10-24 13:22:03 -0700420 if options.bucket:
421 return {options.bucket: {b: [] for b in options.bot}}
Andrii Shyshkalov75424372019-08-30 22:48:15 +0000422 option_parser.error(
423 'Please specify the bucket, e.g. "-B luci.chromium.try".')
qyearsley1fdfcb62016-10-24 13:22:03 -0700424
425
Edward Lemur6215c792019-10-03 21:59:05 +0000426def _parse_bucket(raw_bucket):
427 legacy = True
428 project = bucket = None
429 if '/' in raw_bucket:
430 legacy = False
431 project, bucket = raw_bucket.split('/', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000432 # Assume luci.<project>.<bucket>.
Edward Lemur6215c792019-10-03 21:59:05 +0000433 elif raw_bucket.startswith('luci.'):
434 project, bucket = raw_bucket[len('luci.'):].split('.', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000435 # Otherwise, assume prefix is also the project name.
Edward Lemur6215c792019-10-03 21:59:05 +0000436 elif '.' in raw_bucket:
437 project = raw_bucket.split('.')[0]
438 bucket = raw_bucket
439 # Legacy buckets.
440 if legacy:
441 print('WARNING Please use %s/%s to specify the bucket.' % (project, bucket))
442 return project, bucket
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000443
444
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800445def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000446 """Sends a request to Buildbucket to trigger tryjobs for a changelist.
qyearsley1fdfcb62016-10-24 13:22:03 -0700447
448 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700449 auth_config: AuthConfig for Buildbucket.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000450 changelist: Changelist that the tryjobs are associated with.
qyearsley1fdfcb62016-10-24 13:22:03 -0700451 buckets: A nested dict mapping bucket names to builders to tests.
452 options: Command-line options.
453 """
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000454 print('Scheduling jobs on:')
455 for bucket, builders_and_tests in sorted(buckets.iteritems()):
456 print('Bucket:', bucket)
457 print('\n'.join(
458 ' %s: %s' % (builder, tests)
459 for builder, tests in sorted(builders_and_tests.iteritems())))
460 print('To see results here, run: git cl try-results')
461 print('To see results in browser, run: git cl web')
tandriide281ae2016-10-12 06:02:30 -0700462
Edward Lemurf0faf482019-09-25 20:40:17 +0000463 gerrit_changes = [changelist.GetGerritChange(patchset)]
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000464 shared_properties = {'category': getattr(options, 'category', 'git_cl_try')}
Edward Lemurf0faf482019-09-25 20:40:17 +0000465 shared_properties.update(changelist.GetLegacyProperties(patchset))
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000466 if getattr(options, 'clobber', False):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000467 shared_properties['clobber'] = True
468 shared_properties.update(_get_properties_from_options(options) or {})
469
470 requests = []
471 for raw_bucket, builders_and_tests in sorted(buckets.iteritems()):
472 project, bucket = _parse_bucket(raw_bucket)
473 if not project or not bucket:
474 print('WARNING Could not parse bucket "%s". Skipping.' % raw_bucket)
475 continue
476
477 for builder, tests in sorted(builders_and_tests.iteritems()):
478 properties = shared_properties.copy()
479 if 'presubmit' in builder.lower():
480 properties['dry_run'] = 'true'
481 if tests:
482 properties['testfilter'] = tests
483
484 requests.append({
485 'scheduleBuild': {
486 'requestId': str(uuid.uuid4()),
487 'builder': {
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000488 'project': getattr(options, 'project', None) or project,
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000489 'bucket': bucket,
490 'builder': builder,
491 },
492 'gerritChanges': gerrit_changes,
493 'properties': properties,
494 'tags': [
495 {'key': 'builder', 'value': builder},
496 {'key': 'user_agent', 'value': 'git_cl_try'},
497 ],
498 }
499 })
500
501 if not requests:
502 return
503
504 codereview_url = changelist.GetCodereviewServer()
Edward Lemur2c210a42019-09-16 23:58:35 +0000505 codereview_host = urlparse.urlparse(codereview_url).hostname
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000506
Edward Lemur2c210a42019-09-16 23:58:35 +0000507 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
508 http = authenticator.authorize(httplib2.Http())
509 http.force_exception_to_status_code = True
510
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000511 batch_request = {'requests': requests}
512 batch_response = _call_buildbucket(
Edward Lemur4c707a22019-09-24 21:13:43 +0000513 http, options.buildbucket_host, 'Batch', batch_request)
Edward Lemur2c210a42019-09-16 23:58:35 +0000514
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000515 errors = [
516 ' ' + response['error']['message']
517 for response in batch_response.get('responses', [])
518 if 'error' in response
519 ]
520 if errors:
521 raise BuildbucketResponseException(
522 'Failed to schedule builds for some bots:\n%s' % '\n'.join(errors))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000523
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000524
tandrii221ab252016-10-06 08:12:04 -0700525def fetch_try_jobs(auth_config, changelist, buildbucket_host,
526 patchset=None):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000527 """Fetches tryjobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000528
Quinten Yearsley983111f2019-09-26 17:18:48 +0000529 Returns a map from build ID to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000530 """
tandrii221ab252016-10-06 08:12:04 -0700531 assert buildbucket_host
532 assert changelist.GetIssue(), 'CL must be uploaded first'
533 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
534 patchset = patchset or changelist.GetMostRecentPatchset()
535 assert patchset, 'CL must be uploaded first'
536
537 codereview_url = changelist.GetCodereviewServer()
538 codereview_host = urlparse.urlparse(codereview_url).hostname
539 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000540 if authenticator.has_cached_credentials():
541 http = authenticator.authorize(httplib2.Http())
542 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700543 print('Warning: Some results might be missing because %s' %
544 # Get the message on how to login.
Edward Lemurba5bc992019-09-23 22:59:17 +0000545 (auth.LoginRequiredError().message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000546 http = httplib2.Http()
547
548 http.force_exception_to_status_code = True
549
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000550 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandrii221ab252016-10-06 08:12:04 -0700551 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000552 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700553 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000554 params = {'tag': 'buildset:%s' % buildset}
555
556 builds = {}
557 while True:
558 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700559 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000560 params=urllib.urlencode(params))
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000561 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000562 for build in content.get('builds', []):
563 builds[build['id']] = build
564 if 'next_cursor' in content:
565 params['start_cursor'] = content['next_cursor']
566 else:
567 break
568 return builds
569
570
Quinten Yearsley983111f2019-09-26 17:18:48 +0000571def _fetch_latest_builds(auth_config, changelist, buildbucket_host):
572 """Fetches builds from the latest patchset that has builds (within
573 the last few patchsets).
574
575 Args:
576 auth_config (auth.AuthConfig): Auth info for Buildbucket
577 changelist (Changelist): The CL to fetch builds for
578 buildbucket_host (str): Buildbucket host, e.g. "cr-buildbucket.appspot.com"
579
580 Returns:
581 A tuple (builds, patchset) where builds is a dict mapping from build ID to
582 build info from Buildbucket, and patchset is the patchset number where
583 those builds came from.
584 """
585 assert buildbucket_host
586 assert changelist.GetIssue(), 'CL must be uploaded first'
587 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
588 assert changelist.GetMostRecentPatchset()
589 ps = changelist.GetMostRecentPatchset()
590 min_ps = max(1, ps - 5)
591 while ps >= min_ps:
592 builds = fetch_try_jobs(
593 auth_config, changelist, buildbucket_host, patchset=ps)
594 if len(builds):
595 return builds, ps
596 ps -= 1
597 return [], 0
598
599
600def _filter_failed(builds):
601 """Returns a list of buckets/builders that had failed builds.
602
603 Args:
604 builds (dict): Builds, in the format returned by fetch_try_jobs,
605 i.e. a dict mapping build ID to build info dict, which includes
606 the keys status, result, bucket, and builder_name.
607
608 Returns:
609 A dict of bucket to builder to tests (empty list). This is the same format
610 accepted by _trigger_try_jobs and returned by _get_bucket_map.
611 """
612 buckets = collections.defaultdict(dict)
613 for build in builds.values():
614 if build['status'] == 'COMPLETED' and build['result'] == 'FAILURE':
615 project = build['project']
616 bucket = build['bucket']
617 if bucket.startswith('luci.'):
618 # Assume legacy bucket name luci.<project>.<bucket>.
619 bucket = bucket.split('.')[2]
620 builder = _get_builder_from_build(build)
621 buckets[project + '/' + bucket][builder] = []
622 return buckets
623
624
qyearsleyeab3c042016-08-24 09:18:28 -0700625def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000626 """Prints nicely result of fetch_try_jobs."""
627 if not builds:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000628 print('No tryjobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000629 return
630
631 # Make a copy, because we'll be modifying builds dictionary.
632 builds = builds.copy()
633 builder_names_cache = {}
634
635 def get_builder(b):
636 try:
637 return builder_names_cache[b['id']]
638 except KeyError:
Quinten Yearsley983111f2019-09-26 17:18:48 +0000639 name = _get_builder_from_build(b)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000640 builder_names_cache[b['id']] = name
641 return name
642
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000643 if options.print_master:
644 name_fmt = '%%-%ds %%-%ds' % (
Edward Lemurc8b67ed2019-09-12 20:28:58 +0000645 max(len(str(b['bucket'])) for b in builds.itervalues()),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000646 max(len(str(get_builder(b))) for b in builds.itervalues()))
647 def get_name(b):
Edward Lemurc8b67ed2019-09-12 20:28:58 +0000648 return name_fmt % (b['bucket'], get_builder(b))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000649 else:
650 name_fmt = '%%-%ds' % (
651 max(len(str(get_builder(b))) for b in builds.itervalues()))
652 def get_name(b):
653 return name_fmt % get_builder(b)
654
655 def sort_key(b):
656 return b['status'], b.get('result'), get_name(b), b.get('url')
657
658 def pop(title, f, color=None, **kwargs):
659 """Pop matching builds from `builds` dict and print them."""
660
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000661 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000662 colorize = str
663 else:
664 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
665
666 result = []
667 for b in builds.values():
668 if all(b.get(k) == v for k, v in kwargs.iteritems()):
669 builds.pop(b['id'])
670 result.append(b)
671 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700672 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000673 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700674 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000675
676 total = len(builds)
677 pop(status='COMPLETED', result='SUCCESS',
678 title='Successes:', color=Fore.GREEN,
679 f=lambda b: (get_name(b), b.get('url')))
680 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
681 title='Infra Failures:', color=Fore.MAGENTA,
682 f=lambda b: (get_name(b), b.get('url')))
683 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
684 title='Failures:', color=Fore.RED,
685 f=lambda b: (get_name(b), b.get('url')))
686 pop(status='COMPLETED', result='CANCELED',
687 title='Canceled:', color=Fore.MAGENTA,
688 f=lambda b: (get_name(b),))
689 pop(status='COMPLETED', result='FAILURE',
690 failure_reason='INVALID_BUILD_DEFINITION',
691 title='Wrong master/builder name:', color=Fore.MAGENTA,
692 f=lambda b: (get_name(b),))
693 pop(status='COMPLETED', result='FAILURE',
694 title='Other failures:',
695 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
696 pop(status='COMPLETED',
697 title='Other finished:',
698 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
699 pop(status='STARTED',
700 title='Started:', color=Fore.YELLOW,
701 f=lambda b: (get_name(b), b.get('url')))
702 pop(status='SCHEDULED',
703 title='Scheduled:',
704 f=lambda b: (get_name(b), 'id=%s' % b['id']))
705 # The last section is just in case buildbucket API changes OR there is a bug.
706 pop(title='Other:',
707 f=lambda b: (get_name(b), 'id=%s' % b['id']))
708 assert len(builds) == 0
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000709 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000710
711
Quinten Yearsley983111f2019-09-26 17:18:48 +0000712def _get_builder_from_build(build):
713 """Returns a builder name from a BB v1 build info dict."""
714 try:
715 parameters = json.loads(build['parameters_json'])
716 name = parameters['builder_name']
717 except (ValueError, KeyError) as error:
718 print('WARNING: Failed to get builder name for build %s: %s' % (
719 build['id'], error))
720 name = None
721 return name
722
723
Aiden Bennerc08566e2018-10-03 17:52:42 +0000724def _ComputeDiffLineRanges(files, upstream_commit):
725 """Gets the changed line ranges for each file since upstream_commit.
726
727 Parses a git diff on provided files and returns a dict that maps a file name
728 to an ordered list of range tuples in the form (start_line, count).
729 Ranges are in the same format as a git diff.
730 """
731 # If files is empty then diff_output will be a full diff.
732 if len(files) == 0:
733 return {}
734
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000735 # Take the git diff and find the line ranges where there are changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000736 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
737 diff_output = RunGit(diff_cmd)
738
739 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
740 # 2 capture groups
741 # 0 == fname of diff file
742 # 1 == 'diff_start,diff_count' or 'diff_start'
743 # will match each of
744 # diff --git a/foo.foo b/foo.py
745 # @@ -12,2 +14,3 @@
746 # @@ -12,2 +17 @@
747 # running re.findall on the above string with pattern will give
748 # [('foo.py', ''), ('', '14,3'), ('', '17')]
749
750 curr_file = None
751 line_diffs = {}
752 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
753 if match[0] != '':
754 # Will match the second filename in diff --git a/a.py b/b.py.
755 curr_file = match[0]
756 line_diffs[curr_file] = []
757 else:
758 # Matches +14,3
759 if ',' in match[1]:
760 diff_start, diff_count = match[1].split(',')
761 else:
762 # Single line changes are of the form +12 instead of +12,1.
763 diff_start = match[1]
764 diff_count = 1
765
766 diff_start = int(diff_start)
767 diff_count = int(diff_count)
768
769 # If diff_count == 0 this is a removal we can ignore.
770 line_diffs[curr_file].append((diff_start, diff_count))
771
772 return line_diffs
773
774
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000775def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000776 """Checks if a yapf file is in any parent directory of fpath until top_dir.
777
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000778 Recursively checks parent directories to find yapf file and if no yapf file
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000779 is found returns None. Uses yapf_config_cache as a cache for previously found
780 configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000781 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000782 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000783 # Return result if we've already computed it.
784 if fpath in yapf_config_cache:
785 return yapf_config_cache[fpath]
786
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000787 parent_dir = os.path.dirname(fpath)
788 if os.path.isfile(fpath):
789 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000790 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000791 # Otherwise fpath is a directory
792 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
793 if os.path.isfile(yapf_file):
794 ret = yapf_file
795 elif fpath == top_dir or parent_dir == fpath:
796 # If we're at the top level directory, or if we're at root
797 # there is no provided style.
798 ret = None
799 else:
800 # Otherwise recurse on the current directory.
801 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000802 yapf_config_cache[fpath] = ret
803 return ret
804
805
qyearsley53f48a12016-09-01 10:45:13 -0700806def write_try_results_json(output_file, builds):
807 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
808
809 The input |builds| dict is assumed to be generated by Buildbucket.
810 Buildbucket documentation: http://goo.gl/G0s101
811 """
812
813 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800814 """Extracts some of the information from one build dict."""
815 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700816 return {
817 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700818 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800819 'builder_name': parameters.get('builder_name'),
820 'created_ts': build.get('created_ts'),
821 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700822 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800823 'result': build.get('result'),
824 'status': build.get('status'),
825 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700826 'url': build.get('url'),
827 }
828
829 converted = []
830 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000831 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 10:45:13 -0700832 write_json(output_file, converted)
833
834
Aaron Gable13101a62018-02-09 13:20:41 -0800835def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000836 """Prints statistics about the change to the user."""
837 # --no-ext-diff is broken in some versions of Git, so try to work around
838 # this by overriding the environment (but there is still a problem if the
839 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000840 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000841 if 'GIT_EXTERNAL_DIFF' in env:
842 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000843
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000844 try:
845 stdout = sys.stdout.fileno()
846 except AttributeError:
847 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000848 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800849 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000850 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000851
852
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000853class BuildbucketResponseException(Exception):
854 pass
855
856
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000857class Settings(object):
858 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000859 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000860 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000861 self.tree_status_url = None
862 self.viewvc_url = None
863 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000864 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000865 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000866 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000867 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000868
869 def LazyUpdateIfNeeded(self):
870 """Updates the settings from a codereview.settings file, if available."""
871 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000872 # The only value that actually changes the behavior is
873 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000874 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000875 error_ok=True
876 ).strip().lower()
877
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000878 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000879 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000880 LoadCodereviewSettingsFromFile(cr_settings_file)
881 self.updated = True
882
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000883 @staticmethod
884 def GetRelativeRoot():
885 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000886
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000887 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000888 if self.root is None:
889 self.root = os.path.abspath(self.GetRelativeRoot())
890 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000891
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000892 def GetTreeStatusUrl(self, error_ok=False):
893 if not self.tree_status_url:
894 error_message = ('You must configure your tree status URL by running '
895 '"git cl config".')
Edward Lemur61ea3072018-12-01 00:34:36 +0000896 self.tree_status_url = self._GetConfig(
897 'rietveld.tree-status-url', error_ok=error_ok,
898 error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000899 return self.tree_status_url
900
901 def GetViewVCUrl(self):
902 if not self.viewvc_url:
Edward Lemur61ea3072018-12-01 00:34:36 +0000903 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000904 return self.viewvc_url
905
rmistry@google.com90752582014-01-14 21:04:50 +0000906 def GetBugPrefix(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000907 return self._GetConfig('rietveld.bug-prefix', error_ok=True)
rmistry@google.com78948ed2015-07-08 23:09:57 +0000908
rmistry@google.com5626a922015-02-26 14:03:30 +0000909 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000910 run_post_upload_hook = self._GetConfig(
911 'rietveld.run-post-upload-hook', error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +0000912 return run_post_upload_hook == "True"
913
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000914 def GetDefaultCCList(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000915 return self._GetConfig('rietveld.cc', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000916
ukai@chromium.orge8077812012-02-03 03:41:46 +0000917 def GetIsGerrit(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000918 """Returns True if this repo is associated with Gerrit."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000919 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700920 self.is_gerrit = (
921 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000922 return self.is_gerrit
923
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000924 def GetSquashGerritUploads(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000925 """Returns True if uploads to Gerrit should be squashed by default."""
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000926 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700927 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
928 if self.squash_gerrit_uploads is None:
929 # Default is squash now (http://crbug.com/611892#c23).
930 self.squash_gerrit_uploads = not (
931 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
932 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000933 return self.squash_gerrit_uploads
934
tandriia60502f2016-06-20 02:01:53 -0700935 def GetSquashGerritUploadsOverride(self):
936 """Return True or False if codereview.settings should be overridden.
937
938 Returns None if no override has been defined.
939 """
940 # See also http://crbug.com/611892#c23
941 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
942 error_ok=True).strip()
943 if result == 'true':
944 return True
945 if result == 'false':
946 return False
947 return None
948
tandrii@chromium.org28253532016-04-14 13:46:56 +0000949 def GetGerritSkipEnsureAuthenticated(self):
950 """Return True if EnsureAuthenticated should not be done for Gerrit
951 uploads."""
952 if self.gerrit_skip_ensure_authenticated is None:
953 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000954 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000955 error_ok=True).strip() == 'true')
956 return self.gerrit_skip_ensure_authenticated
957
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000958 def GetGitEditor(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000959 """Returns the editor specified in the git config, or None if none is."""
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000960 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000961 # Git requires single quotes for paths with spaces. We need to replace
962 # them with double quotes for Windows to treat such paths as a single
963 # path.
964 self.git_editor = self._GetConfig(
965 'core.editor', error_ok=True).replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000966 return self.git_editor or None
967
thestig@chromium.org44202a22014-03-11 19:22:18 +0000968 def GetLintRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000969 return (self._GetConfig('rietveld.cpplint-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000970 DEFAULT_LINT_REGEX)
971
972 def GetLintIgnoreRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000973 return (self._GetConfig('rietveld.cpplint-ignore-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000974 DEFAULT_LINT_IGNORE_REGEX)
975
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000976 def _GetConfig(self, param, **kwargs):
977 self.LazyUpdateIfNeeded()
978 return RunGit(['config', param], **kwargs).strip()
979
980
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000981def ShortBranchName(branch):
982 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000983 return branch.replace('refs/heads/', '', 1)
984
985
986def GetCurrentBranchRef():
987 """Returns branch ref (e.g., refs/heads/master) or None."""
988 return RunGit(['symbolic-ref', 'HEAD'],
989 stderr=subprocess2.VOID, error_ok=True).strip() or None
990
991
992def GetCurrentBranch():
993 """Returns current branch or None.
994
995 For refs/heads/* branches, returns just last part. For others, full ref.
996 """
997 branchref = GetCurrentBranchRef()
998 if branchref:
999 return ShortBranchName(branchref)
1000 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001001
1002
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001003class _CQState(object):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00001004 """Enum for states of CL with respect to CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001005 NONE = 'none'
1006 DRY_RUN = 'dry_run'
1007 COMMIT = 'commit'
1008
1009 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1010
1011
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001012class _ParsedIssueNumberArgument(object):
Edward Lemurf38bc172019-09-03 21:02:13 +00001013 def __init__(self, issue=None, patchset=None, hostname=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001014 self.issue = issue
1015 self.patchset = patchset
1016 self.hostname = hostname
1017
1018 @property
1019 def valid(self):
1020 return self.issue is not None
1021
1022
Edward Lemurf38bc172019-09-03 21:02:13 +00001023def ParseIssueNumberArgument(arg):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001024 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1025 fail_result = _ParsedIssueNumberArgument()
1026
Edward Lemur678a6842019-10-03 22:25:05 +00001027 if isinstance(arg, int):
1028 return _ParsedIssueNumberArgument(issue=arg)
1029 if not isinstance(arg, basestring):
1030 return fail_result
1031
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001032 if arg.isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00001033 return _ParsedIssueNumberArgument(issue=int(arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001034 if not arg.startswith('http'):
1035 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001036
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001037 url = gclient_utils.UpgradeToHttps(arg)
1038 try:
1039 parsed_url = urlparse.urlparse(url)
1040 except ValueError:
1041 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001042
Edward Lemur678a6842019-10-03 22:25:05 +00001043 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
1044 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
1045 # Short urls like https://domain/<issue_number> can be used, but don't allow
1046 # specifying the patchset (you'd 404), but we allow that here.
1047 if parsed_url.path == '/':
1048 part = parsed_url.fragment
1049 else:
1050 part = parsed_url.path
1051
1052 match = re.match(
1053 r'(/c(/.*/\+)?)?/(?P<issue>\d+)(/(?P<patchset>\d+)?/?)?$', part)
1054 if not match:
1055 return fail_result
1056
1057 issue = int(match.group('issue'))
1058 patchset = match.group('patchset')
1059 return _ParsedIssueNumberArgument(
1060 issue=issue,
1061 patchset=int(patchset) if patchset else None,
1062 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001063
1064
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001065def _create_description_from_log(args):
1066 """Pulls out the commit log to use as a base for the CL description."""
1067 log_args = []
1068 if len(args) == 1 and not args[0].endswith('.'):
1069 log_args = [args[0] + '..']
1070 elif len(args) == 1 and args[0].endswith('...'):
1071 log_args = [args[0][:-1]]
1072 elif len(args) == 2:
1073 log_args = [args[0] + '..' + args[1]]
1074 else:
1075 log_args = args[:] # Hope for the best!
1076 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
1077
1078
Aaron Gablea45ee112016-11-22 15:14:38 -08001079class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001080 def __init__(self, issue, url):
1081 self.issue = issue
1082 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001083 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001084
1085 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001086 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001087 self.issue, self.url)
1088
1089
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001090_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001091 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001092 # TODO(tandrii): these two aren't known in Gerrit.
1093 'approval', 'disapproval'])
1094
1095
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001096class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001097 """Changelist works with one changelist in local branch.
1098
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001099 Notes:
1100 * Not safe for concurrent multi-{thread,process} use.
1101 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001102 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001103 """
1104
Edward Lemur125d60a2019-09-13 18:25:41 +00001105 def __init__(self, branchref=None, issue=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001106 """Create a new ChangeList instance.
1107
Edward Lemurf38bc172019-09-03 21:02:13 +00001108 **kwargs will be passed directly to Gerrit implementation.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001109 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001110 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001111 global settings
1112 if not settings:
1113 # Happens when git_cl.py is used as a utility library.
1114 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001115
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001116 self.branchref = branchref
1117 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001118 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001119 self.branch = ShortBranchName(self.branchref)
1120 else:
1121 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001122 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001123 self.lookedup_issue = False
1124 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001125 self.has_description = False
1126 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001127 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001129 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001130 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001131 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001132 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001133
Edward Lemur125d60a2019-09-13 18:25:41 +00001134 # Lazily cached values.
1135 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1136 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1137 # Map from change number (issue) to its detail cache.
1138 self._detail_cache = {}
1139
1140 if codereview_host is not None:
1141 assert not codereview_host.startswith('https://'), codereview_host
1142 self._gerrit_host = codereview_host
1143 self._gerrit_server = 'https://%s' % codereview_host
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001144
1145 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001146 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001147
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001148 The return value is a string suitable for passing to git cl with the --cc
1149 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001150 """
1151 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001152 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001153 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001154 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1155 return self.cc
1156
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001157 def GetCCListWithoutDefault(self):
1158 """Return the users cc'd on this CL excluding default ones."""
1159 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001160 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001161 return self.cc
1162
Daniel Cheng7227d212017-11-17 08:12:37 -08001163 def ExtendCC(self, more_cc):
1164 """Extends the list of users to cc on this CL based on the changed files."""
1165 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001166
1167 def GetBranch(self):
1168 """Returns the short branch name, e.g. 'master'."""
1169 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001170 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001171 if not branchref:
1172 return None
1173 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001174 self.branch = ShortBranchName(self.branchref)
1175 return self.branch
1176
1177 def GetBranchRef(self):
1178 """Returns the full branch name, e.g. 'refs/heads/master'."""
1179 self.GetBranch() # Poke the lazy loader.
1180 return self.branchref
1181
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001182 def ClearBranch(self):
1183 """Clears cached branch data of this object."""
1184 self.branch = self.branchref = None
1185
tandrii5d48c322016-08-18 16:19:37 -07001186 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1187 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1188 kwargs['branch'] = self.GetBranch()
1189 return _git_get_branch_config_value(key, default, **kwargs)
1190
1191 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1192 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1193 assert self.GetBranch(), (
1194 'this CL must have an associated branch to %sset %s%s' %
1195 ('un' if value is None else '',
1196 key,
1197 '' if value is None else ' to %r' % value))
1198 kwargs['branch'] = self.GetBranch()
1199 return _git_set_branch_config_value(key, value, **kwargs)
1200
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001201 @staticmethod
1202 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001203 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001204 e.g. 'origin', 'refs/heads/master'
1205 """
1206 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001207 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1208
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001209 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001210 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001212 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1213 error_ok=True).strip()
1214 if upstream_branch:
1215 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001216 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001217 # Else, try to guess the origin remote.
1218 remote_branches = RunGit(['branch', '-r']).split()
1219 if 'origin/master' in remote_branches:
1220 # Fall back on origin/master if it exits.
1221 remote = 'origin'
1222 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001224 DieWithError(
1225 'Unable to determine default branch to diff against.\n'
1226 'Either pass complete "git diff"-style arguments, like\n'
1227 ' git cl upload origin/master\n'
1228 'or verify this branch is set up to track another \n'
1229 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001230
1231 return remote, upstream_branch
1232
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001233 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001234 upstream_branch = self.GetUpstreamBranch()
1235 if not BranchExists(upstream_branch):
1236 DieWithError('The upstream for the current branch (%s) does not exist '
1237 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001238 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001239 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001240
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241 def GetUpstreamBranch(self):
1242 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001243 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Raul Tambrefe1dbe12019-05-02 04:43:57 +00001244 if remote != '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001245 upstream_branch = upstream_branch.replace('refs/heads/',
1246 'refs/remotes/%s/' % remote)
1247 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1248 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249 self.upstream_branch = upstream_branch
1250 return self.upstream_branch
1251
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001252 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001253 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001254 remote, branch = None, self.GetBranch()
1255 seen_branches = set()
1256 while branch not in seen_branches:
1257 seen_branches.add(branch)
1258 remote, branch = self.FetchUpstreamTuple(branch)
1259 branch = ShortBranchName(branch)
1260 if remote != '.' or branch.startswith('refs/remotes'):
1261 break
1262 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001263 remotes = RunGit(['remote'], error_ok=True).split()
1264 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001265 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001266 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001267 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001268 logging.warn('Could not determine which remote this change is '
1269 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001270 else:
1271 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001272 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001273 branch = 'HEAD'
1274 if branch.startswith('refs/remotes'):
1275 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001276 elif branch.startswith('refs/branch-heads/'):
1277 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001278 else:
1279 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001280 return self._remote
1281
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001282 def GitSanityChecks(self, upstream_git_obj):
1283 """Checks git repo status and ensures diff is from local commits."""
1284
sbc@chromium.org79706062015-01-14 21:18:12 +00001285 if upstream_git_obj is None:
1286 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001287 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001288 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001289 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001290 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001291 return False
1292
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001293 # Verify the commit we're diffing against is in our current branch.
1294 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1295 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1296 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001297 print('ERROR: %s is not in the current branch. You may need to rebase '
1298 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001299 return False
1300
1301 # List the commits inside the diff, and verify they are all local.
1302 commits_in_diff = RunGit(
1303 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1304 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1305 remote_branch = remote_branch.strip()
1306 if code != 0:
1307 _, remote_branch = self.GetRemoteBranch()
1308
1309 commits_in_remote = RunGit(
1310 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1311
1312 common_commits = set(commits_in_diff) & set(commits_in_remote)
1313 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001314 print('ERROR: Your diff contains %d commits already in %s.\n'
1315 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1316 'the diff. If you are using a custom git flow, you can override'
1317 ' the reference used for this check with "git config '
1318 'gitcl.remotebranch <git-ref>".' % (
1319 len(common_commits), remote_branch, upstream_git_obj),
1320 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001321 return False
1322 return True
1323
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001324 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001325 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001326
1327 Returns None if it is not set.
1328 """
tandrii5d48c322016-08-18 16:19:37 -07001329 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001330
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001331 def GetRemoteUrl(self):
1332 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1333
1334 Returns None if there is no remote.
1335 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001336 is_cached, value = self._cached_remote_url
1337 if is_cached:
1338 return value
1339
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001340 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001341 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1342
Edward Lemur298f2cf2019-02-22 21:40:39 +00001343 # Check if the remote url can be parsed as an URL.
1344 host = urlparse.urlparse(url).netloc
1345 if host:
1346 self._cached_remote_url = (True, url)
1347 return url
1348
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001349 # If it cannot be parsed as an url, assume it is a local directory,
1350 # probably a git cache.
Edward Lemur298f2cf2019-02-22 21:40:39 +00001351 logging.warning('"%s" doesn\'t appear to point to a git host. '
1352 'Interpreting it as a local directory.', url)
1353 if not os.path.isdir(url):
1354 logging.error(
1355 'Remote "%s" for branch "%s" points to "%s", but it doesn\'t exist.',
Daniel Bratell4a60db42019-09-16 17:02:52 +00001356 remote, self.GetBranch(), url)
Edward Lemur298f2cf2019-02-22 21:40:39 +00001357 return None
1358
1359 cache_path = url
1360 url = RunGit(['config', 'remote.%s.url' % remote],
1361 error_ok=True,
1362 cwd=url).strip()
1363
1364 host = urlparse.urlparse(url).netloc
1365 if not host:
1366 logging.error(
1367 'Remote "%(remote)s" for branch "%(branch)s" points to '
1368 '"%(cache_path)s", but it is misconfigured.\n'
1369 '"%(cache_path)s" must be a git repo and must have a remote named '
1370 '"%(remote)s" pointing to the git host.', {
1371 'remote': remote,
1372 'cache_path': cache_path,
1373 'branch': self.GetBranch()})
1374 return None
1375
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001376 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001377 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001378
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001379 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001380 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001381 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001382 self.issue = self._GitGetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001383 self.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001384 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001385 return self.issue
1386
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001387 def GetIssueURL(self):
1388 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001389 issue = self.GetIssue()
1390 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001391 return None
Edward Lemur125d60a2019-09-13 18:25:41 +00001392 return '%s/%s' % (self.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001393
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001394 def GetDescription(self, pretty=False, force=False):
1395 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396 if self.GetIssue():
Edward Lemur125d60a2019-09-13 18:25:41 +00001397 self.description = self.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001398 self.has_description = True
1399 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001400 # Set width to 72 columns + 2 space indent.
1401 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001403 lines = self.description.splitlines()
1404 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001405 return self.description
1406
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001407 def GetDescriptionFooters(self):
1408 """Returns (non_footer_lines, footers) for the commit message.
1409
1410 Returns:
1411 non_footer_lines (list(str)) - Simple list of description lines without
1412 any footer. The lines do not contain newlines, nor does the list contain
1413 the empty line between the message and the footers.
1414 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1415 [("Change-Id", "Ideadbeef...."), ...]
1416 """
1417 raw_description = self.GetDescription()
1418 msg_lines, _, footers = git_footers.split_footers(raw_description)
1419 if footers:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001420 msg_lines = msg_lines[:len(msg_lines) - 1]
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001421 return msg_lines, footers
1422
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001423 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001424 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001425 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001426 self.patchset = self._GitGetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001427 self.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001428 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001429 return self.patchset
1430
1431 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001432 """Set this branch's patchset. If patchset=0, clears the patchset."""
1433 assert self.GetBranch()
1434 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001435 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001436 else:
1437 self.patchset = int(patchset)
1438 self._GitSetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001439 self.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001441 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001442 """Set this branch's issue. If issue isn't given, clears the issue."""
1443 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001444 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001445 issue = int(issue)
1446 self._GitSetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001447 self.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001448 self.issue = issue
Edward Lemur125d60a2019-09-13 18:25:41 +00001449 codereview_server = self.GetCodereviewServer()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001450 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001451 self._GitSetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001452 self.CodereviewServerConfigKey(),
tandrii5d48c322016-08-18 16:19:37 -07001453 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001454 else:
tandrii5d48c322016-08-18 16:19:37 -07001455 # Reset all of these just to be clean.
1456 reset_suffixes = [
1457 'last-upload-hash',
Edward Lemur125d60a2019-09-13 18:25:41 +00001458 self.IssueConfigKey(),
1459 self.PatchsetConfigKey(),
1460 self.CodereviewServerConfigKey(),
tandrii5d48c322016-08-18 16:19:37 -07001461 ] + self._PostUnsetIssueProperties()
1462 for prop in reset_suffixes:
1463 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001464 msg = RunGit(['log', '-1', '--format=%B']).strip()
1465 if msg and git_footers.get_footer_change_id(msg):
1466 print('WARNING: The change patched into this branch has a Change-Id. '
1467 'Removing it.')
1468 RunGit(['commit', '--amend', '-m',
1469 git_footers.remove_footer(msg, 'Change-Id')])
Edward Lemurf38bc172019-09-03 21:02:13 +00001470 self.lookedup_issue = True
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001471 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001472 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001473
dnjba1b0f32016-09-02 12:37:42 -07001474 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001475 if not self.GitSanityChecks(upstream_branch):
1476 DieWithError('\nGit sanity check failure')
1477
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001478 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001479 if not root:
1480 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001481 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001482
1483 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001484 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001485 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001486 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001487 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001488 except subprocess2.CalledProcessError:
1489 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001490 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001491 'This branch probably doesn\'t exist anymore. To reset the\n'
1492 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001493 ' git branch --set-upstream-to origin/master %s\n'
1494 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001495 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001496
maruel@chromium.org52424302012-08-29 15:14:30 +00001497 issue = self.GetIssue()
1498 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001499 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001500 description = self.GetDescription()
1501 else:
1502 # If the change was never uploaded, use the log messages of all commits
1503 # up to the branch point, as git cl upload will prefill the description
1504 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001505 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1506 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001507
1508 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001509 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001510 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001511 name,
1512 description,
1513 absroot,
1514 files,
1515 issue,
1516 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001517 author,
1518 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001519
dsansomee2d6fd92016-09-08 00:10:47 -07001520 def UpdateDescription(self, description, force=False):
Edward Lemur125d60a2019-09-13 18:25:41 +00001521 self.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001522 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001523 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001524
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001525 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1526 """Sets the description for this CL remotely.
1527
1528 You can get description_lines and footers with GetDescriptionFooters.
1529
1530 Args:
1531 description_lines (list(str)) - List of CL description lines without
1532 newline characters.
1533 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1534 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1535 `List-Of-Tokens`). It will be case-normalized so that each token is
1536 title-cased.
1537 """
1538 new_description = '\n'.join(description_lines)
1539 if footers:
1540 new_description += '\n'
1541 for k, v in footers:
1542 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1543 if not git_footers.FOOTER_PATTERN.match(foot):
1544 raise ValueError('Invalid footer %r' % foot)
1545 new_description += foot + '\n'
1546 self.UpdateDescription(new_description, force)
1547
Edward Lesmes8e282792018-04-03 18:50:29 -04001548 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001549 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1550 try:
Edward Lemur2c48f242019-06-04 16:14:09 +00001551 start = time_time()
1552 result = presubmit_support.DoPresubmitChecks(change, committing,
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001553 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1554 default_presubmit=None, may_prompt=may_prompt,
Edward Lemur125d60a2019-09-13 18:25:41 +00001555 gerrit_obj=self.GetGerritObjForPresubmit(),
Edward Lesmes8e282792018-04-03 18:50:29 -04001556 parallel=parallel)
Edward Lemur2c48f242019-06-04 16:14:09 +00001557 metrics.collector.add_repeated('sub_commands', {
1558 'command': 'presubmit',
1559 'execution_time': time_time() - start,
1560 'exit_code': 0 if result.should_continue() else 1,
1561 })
1562 return result
vapierfd77ac72016-06-16 08:33:57 -07001563 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001564 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001565
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001566 def CMDUpload(self, options, git_diff_args, orig_args):
1567 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001568 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001569 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001570 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001571 else:
1572 if self.GetBranch() is None:
1573 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1574
1575 # Default to diffing against common ancestor of upstream branch
1576 base_branch = self.GetCommonAncestorWithUpstream()
1577 git_diff_args = [base_branch, 'HEAD']
1578
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001579 # Fast best-effort checks to abort before running potentially expensive
1580 # hooks if uploading is likely to fail anyway. Passing these checks does
1581 # not guarantee that uploading will not fail.
Edward Lemur125d60a2019-09-13 18:25:41 +00001582 self.EnsureAuthenticated(force=options.force)
1583 self.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001584
1585 # Apply watchlists on upload.
1586 change = self.GetChange(base_branch, None)
1587 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1588 files = [f.LocalPath() for f in change.AffectedFiles()]
1589 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001590 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001591
1592 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001593 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001594 # Set the reviewer list now so that presubmit checks can access it.
1595 change_description = ChangeDescription(change.FullDescriptionText())
1596 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001597 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001598 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001599 change)
1600 change.SetDescriptionText(change_description.description)
1601 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001602 may_prompt=not options.force,
1603 verbose=options.verbose,
1604 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001605 if not hook_results.should_continue():
1606 return 1
1607 if not options.reviewers and hook_results.reviewers:
1608 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001609 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001610
Aaron Gable13101a62018-02-09 13:20:41 -08001611 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001612 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001613 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001614 _git_set_branch_config_value('last-upload-hash',
1615 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001616 # Run post upload hooks, if specified.
1617 if settings.GetRunPostUploadHook():
1618 presubmit_support.DoPostUploadExecuter(
1619 change,
1620 self,
1621 settings.GetRoot(),
1622 options.verbose,
1623 sys.stdout)
1624
1625 # Upload all dependencies if specified.
1626 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001627 print()
1628 print('--dependencies has been specified.')
1629 print('All dependent local branches will be re-uploaded.')
1630 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001631 # Remove the dependencies flag from args so that we do not end up in a
1632 # loop.
1633 orig_args.remove('--dependencies')
1634 ret = upload_branch_deps(self, orig_args)
1635 return ret
1636
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001637 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001638 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001639
1640 Issue must have been already uploaded and known.
1641 """
1642 assert new_state in _CQState.ALL_STATES
1643 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001644 try:
Edward Lemur125d60a2019-09-13 18:25:41 +00001645 vote_map = {
1646 _CQState.NONE: 0,
1647 _CQState.DRY_RUN: 1,
1648 _CQState.COMMIT: 2,
1649 }
1650 labels = {'Commit-Queue': vote_map[new_state]}
1651 notify = False if new_state == _CQState.DRY_RUN else None
1652 gerrit_util.SetReview(
1653 self._GetGerritHost(), self._GerritChangeIdentifier(),
1654 labels=labels, notify=notify)
qyearsley1fdfcb62016-10-24 13:22:03 -07001655 return 0
1656 except KeyboardInterrupt:
1657 raise
1658 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001659 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001660 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001661 ' * Your project has no CQ,\n'
1662 ' * You don\'t have permission to change the CQ state,\n'
1663 ' * There\'s a bug in this code (see stack trace below).\n'
1664 'Consider specifying which bots to trigger manually or asking your '
1665 'project owners for permissions or contacting Chrome Infra at:\n'
1666 'https://www.chromium.org/infra\n\n' %
1667 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001668 # Still raise exception so that stack trace is printed.
1669 raise
1670
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001671 def _GetGerritHost(self):
1672 # Lazy load of configs.
1673 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001674 if self._gerrit_host and '.' not in self._gerrit_host:
1675 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1676 # This happens for internal stuff http://crbug.com/614312.
1677 parsed = urlparse.urlparse(self.GetRemoteUrl())
1678 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001679 print('WARNING: using non-https URLs for remote is likely broken\n'
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001680 ' Your current remote is: %s' % self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001681 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1682 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001683 return self._gerrit_host
1684
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001685 def _GetGitHost(self):
1686 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001687 remote_url = self.GetRemoteUrl()
1688 if not remote_url:
1689 return None
1690 return urlparse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001691
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001692 def GetCodereviewServer(self):
1693 if not self._gerrit_server:
1694 # If we're on a branch then get the server potentially associated
1695 # with that branch.
1696 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001697 self._gerrit_server = self._GitGetBranchConfigValue(
1698 self.CodereviewServerConfigKey())
1699 if self._gerrit_server:
1700 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001701 if not self._gerrit_server:
1702 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1703 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001704 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001705 parts[0] = parts[0] + '-review'
1706 self._gerrit_host = '.'.join(parts)
1707 self._gerrit_server = 'https://%s' % self._gerrit_host
1708 return self._gerrit_server
1709
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001710 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001711 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001712 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001713 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001714 logging.warn('can\'t detect Gerrit project.')
1715 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001716 project = urlparse.urlparse(remote_url).path.strip('/')
1717 if project.endswith('.git'):
1718 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001719 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1720 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1721 # gitiles/git-over-https protocol. E.g.,
1722 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1723 # as
1724 # https://chromium.googlesource.com/v8/v8
1725 if project.startswith('a/'):
1726 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001727 return project
1728
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001729 def _GerritChangeIdentifier(self):
1730 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1731
1732 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001733 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001734 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001735 project = self._GetGerritProject()
1736 if project:
1737 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1738 # Fall back on still unique, but less efficient change number.
1739 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001740
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001741 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001742 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001743 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001744
tandrii5d48c322016-08-18 16:19:37 -07001745 @classmethod
1746 def PatchsetConfigKey(cls):
1747 return 'gerritpatchset'
1748
1749 @classmethod
1750 def CodereviewServerConfigKey(cls):
1751 return 'gerritserver'
1752
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001753 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001754 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00001755 if settings.GetGerritSkipEnsureAuthenticated():
1756 # For projects with unusual authentication schemes.
1757 # See http://crbug.com/603378.
1758 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001759
1760 # Check presence of cookies only if using cookies-based auth method.
1761 cookie_auth = gerrit_util.Authenticator.get()
1762 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001763 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001764
Daniel Chengcf6269b2019-05-18 01:02:12 +00001765 if urlparse.urlparse(self.GetRemoteUrl()).scheme != 'https':
1766 print('WARNING: Ignoring branch %s with non-https remote %s' %
Edward Lemur125d60a2019-09-13 18:25:41 +00001767 (self.branch, self.GetRemoteUrl()))
Daniel Chengcf6269b2019-05-18 01:02:12 +00001768 return
1769
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001770 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001771 self.GetCodereviewServer()
1772 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00001773 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001774
1775 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1776 git_auth = cookie_auth.get_auth_header(git_host)
1777 if gerrit_auth and git_auth:
1778 if gerrit_auth == git_auth:
1779 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001780 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
Raul Tambre80ee78e2019-05-06 22:41:05 +00001781 print(
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001782 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001783 ' %s\n'
1784 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001785 ' Consider running the following command:\n'
1786 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001787 ' %s\n'
Raul Tambre80ee78e2019-05-06 22:41:05 +00001788 ' %s' %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001789 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001790 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001791 cookie_auth.get_new_password_message(git_host)))
1792 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001793 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001794 return
1795 else:
1796 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02001797 ([] if gerrit_auth else [self._gerrit_host]) +
1798 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001799 DieWithError('Credentials for the following hosts are required:\n'
1800 ' %s\n'
1801 'These are read from %s (or legacy %s)\n'
1802 '%s' % (
1803 '\n '.join(missing),
1804 cookie_auth.get_gitcookies_path(),
1805 cookie_auth.get_netrc_path(),
1806 cookie_auth.get_new_password_message(git_host)))
1807
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001808 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001809 if not self.GetIssue():
1810 return
1811
1812 # Warm change details cache now to avoid RPCs later, reducing latency for
1813 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001814 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00001815 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001816
1817 status = self._GetChangeDetail()['status']
1818 if status in ('MERGED', 'ABANDONED'):
1819 DieWithError('Change %s has been %s, new uploads are not allowed' %
1820 (self.GetIssueURL(),
1821 'submitted' if status == 'MERGED' else 'abandoned'))
1822
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001823 # TODO(vadimsh): For some reason the chunk of code below was skipped if
1824 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
1825 # Apparently this check is not very important? Otherwise get_auth_email
1826 # could have been added to other implementations of Authenticator.
1827 cookies_auth = gerrit_util.Authenticator.get()
1828 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001829 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001830
1831 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001832 if self.GetIssueOwner() == cookies_user:
1833 return
1834 logging.debug('change %s owner is %s, cookies user is %s',
1835 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001836 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001837 # so ask what Gerrit thinks of this user.
1838 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
1839 if details['email'] == self.GetIssueOwner():
1840 return
1841 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001842 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001843 'as %s.\n'
1844 'Uploading may fail due to lack of permissions.' %
1845 (self.GetIssue(), self.GetIssueOwner(), details['email']))
1846 confirm_or_exit(action='upload')
1847
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001848 def _PostUnsetIssueProperties(self):
1849 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001850 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001851
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001852 def GetGerritObjForPresubmit(self):
1853 return presubmit_support.GerritAccessor(self._GetGerritHost())
1854
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001855 def GetStatus(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001856 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001857 or CQ status, assuming adherence to a common workflow.
1858
1859 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001860 * 'error' - error from review tool (including deleted issues)
1861 * 'unsent' - no reviewers added
1862 * 'waiting' - waiting for review
1863 * 'reply' - waiting for uploader to reply to review
1864 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00001865 * 'dry-run' - dry-running in the CQ
1866 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07001867 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001868 """
1869 if not self.GetIssue():
1870 return None
1871
1872 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001873 data = self._GetChangeDetail([
1874 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08001875 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001876 return 'error'
1877
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00001878 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001879 return 'closed'
1880
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001881 cq_label = data['labels'].get('Commit-Queue', {})
1882 max_cq_vote = 0
1883 for vote in cq_label.get('all', []):
1884 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
1885 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001886 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001887 if max_cq_vote == 1:
1888 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001889
Aaron Gable9ab38c62017-04-06 14:36:33 -07001890 if data['labels'].get('Code-Review', {}).get('approved'):
1891 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001892
1893 if not data.get('reviewers', {}).get('REVIEWER', []):
1894 return 'unsent'
1895
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001896 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07001897 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
1898 last_message_author = messages.pop().get('author', {})
1899 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001900 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
1901 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07001902 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001903 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07001904 if last_message_author.get('_account_id') == owner:
1905 # Most recent message was by owner.
1906 return 'waiting'
1907 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001908 # Some reply from non-owner.
1909 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07001910
1911 # Somehow there are no messages even though there are reviewers.
1912 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001913
1914 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001915 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08001916 patchset = data['revisions'][data['current_revision']]['_number']
1917 self.SetPatchset(patchset)
1918 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001919
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001920 def FetchDescription(self, force=False):
1921 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
1922 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00001923 current_rev = data['current_revision']
Dan Beamcf6df902018-11-08 01:48:37 +00001924 return data['revisions'][current_rev]['commit']['message'].encode(
1925 'utf-8', 'ignore')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001926
dsansomee2d6fd92016-09-08 00:10:47 -07001927 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001928 if gerrit_util.HasPendingChangeEdit(
1929 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07001930 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001931 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07001932 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001933 'unpublished edit. Either publish the edit in the Gerrit web UI '
1934 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07001935
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001936 gerrit_util.DeletePendingChangeEdit(
1937 self._GetGerritHost(), self._GerritChangeIdentifier())
1938 gerrit_util.SetCommitMessage(
1939 self._GetGerritHost(), self._GerritChangeIdentifier(),
1940 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001941
Aaron Gable636b13f2017-07-14 10:42:48 -07001942 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001943 gerrit_util.SetReview(
1944 self._GetGerritHost(), self._GerritChangeIdentifier(),
1945 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001946
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001947 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01001948 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001949 # CURRENT_REVISION is included to get the latest patchset so that
1950 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001951 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001952 options=['MESSAGES', 'DETAILED_ACCOUNTS',
1953 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001954 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001955 self._GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001956 robot_file_comments = gerrit_util.GetChangeRobotComments(
1957 self._GetGerritHost(), self._GerritChangeIdentifier())
1958
1959 # Add the robot comments onto the list of comments, but only
1960 # keep those that are from the latest pachset.
1961 latest_patch_set = self.GetMostRecentPatchset()
1962 for path, robot_comments in robot_file_comments.iteritems():
1963 line_comments = file_comments.setdefault(path, [])
1964 line_comments.extend(
1965 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001966
1967 # Build dictionary of file comments for easy access and sorting later.
1968 # {author+date: {path: {patchset: {line: url+message}}}}
1969 comments = collections.defaultdict(
1970 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
1971 for path, line_comments in file_comments.iteritems():
1972 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001973 tag = comment.get('tag', '')
1974 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001975 continue
1976 key = (comment['author']['email'], comment['updated'])
1977 if comment.get('side', 'REVISION') == 'PARENT':
1978 patchset = 'Base'
1979 else:
1980 patchset = 'PS%d' % comment['patch_set']
1981 line = comment.get('line', 0)
1982 url = ('https://%s/c/%s/%s/%s#%s%s' %
1983 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
1984 'b' if comment.get('side') == 'PARENT' else '',
1985 str(line) if line else ''))
1986 comments[key][path][patchset][line] = (url, comment['message'])
1987
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001988 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001989 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001990 summary = self._BuildCommentSummary(msg, comments, readable)
1991 if summary:
1992 summaries.append(summary)
1993 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001994
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001995 @staticmethod
1996 def _BuildCommentSummary(msg, comments, readable):
1997 key = (msg['author']['email'], msg['date'])
1998 # Don't bother showing autogenerated messages that don't have associated
1999 # file or line comments. this will filter out most autogenerated
2000 # messages, but will keep robot comments like those from Tricium.
2001 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
2002 if is_autogenerated and not comments.get(key):
2003 return None
2004 message = msg['message']
2005 # Gerrit spits out nanoseconds.
2006 assert len(msg['date'].split('.')[-1]) == 9
2007 date = datetime.datetime.strptime(msg['date'][:-3],
2008 '%Y-%m-%d %H:%M:%S.%f')
2009 if key in comments:
2010 message += '\n'
2011 for path, patchsets in sorted(comments.get(key, {}).items()):
2012 if readable:
2013 message += '\n%s' % path
2014 for patchset, lines in sorted(patchsets.items()):
2015 for line, (url, content) in sorted(lines.items()):
2016 if line:
2017 line_str = 'Line %d' % line
2018 path_str = '%s:%d:' % (path, line)
2019 else:
2020 line_str = 'File comment'
2021 path_str = '%s:0:' % path
2022 if readable:
2023 message += '\n %s, %s: %s' % (patchset, line_str, url)
2024 message += '\n %s\n' % content
2025 else:
2026 message += '\n%s ' % path_str
2027 message += '\n%s\n' % content
2028
2029 return _CommentSummary(
2030 date=date,
2031 message=message,
2032 sender=msg['author']['email'],
2033 autogenerated=is_autogenerated,
2034 # These could be inferred from the text messages and correlated with
2035 # Code-Review label maximum, however this is not reliable.
2036 # Leaving as is until the need arises.
2037 approval=False,
2038 disapproval=False,
2039 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002040
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002041 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002042 gerrit_util.AbandonChange(
2043 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002044
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002045 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002046 gerrit_util.SubmitChange(
2047 self._GetGerritHost(), self._GerritChangeIdentifier(),
2048 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002049
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002050 def _GetChangeDetail(self, options=None, no_cache=False):
2051 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002052
2053 If fresh data is needed, set no_cache=True which will clear cache and
2054 thus new data will be fetched from Gerrit.
2055 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002056 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002057 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002058
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002059 # Optimization to avoid multiple RPCs:
2060 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2061 'CURRENT_COMMIT' not in options):
2062 options.append('CURRENT_COMMIT')
2063
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002064 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002065 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002066 options = [o.upper() for o in options]
2067
2068 # Check in cache first unless no_cache is True.
2069 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002070 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002071 else:
2072 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002073 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002074 # Assumption: data fetched before with extra options is suitable
2075 # for return for a smaller set of options.
2076 # For example, if we cached data for
2077 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2078 # and request is for options=[CURRENT_REVISION],
2079 # THEN we can return prior cached data.
2080 if options_set.issubset(cached_options_set):
2081 return data
2082
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002083 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002084 data = gerrit_util.GetChangeDetail(
2085 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002086 except gerrit_util.GerritError as e:
2087 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002088 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002089 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002090
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002091 self._detail_cache.setdefault(cache_key, []).append(
2092 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002093 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002094
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002095 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002096 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002097 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002098 data = gerrit_util.GetChangeCommit(
2099 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002100 except gerrit_util.GerritError as e:
2101 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002102 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002103 raise
agable32978d92016-11-01 12:55:02 -07002104 return data
2105
Karen Qian40c19422019-03-13 21:28:29 +00002106 def _IsCqConfigured(self):
2107 detail = self._GetChangeDetail(['LABELS'])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002108 if u'Commit-Queue' not in detail.get('labels', {}):
Karen Qian40c19422019-03-13 21:28:29 +00002109 return False
2110 # TODO(crbug/753213): Remove temporary hack
2111 if ('https://chromium.googlesource.com/chromium/src' ==
Edward Lemur125d60a2019-09-13 18:25:41 +00002112 self.GetRemoteUrl() and
Karen Qian40c19422019-03-13 21:28:29 +00002113 detail['branch'].startswith('refs/branch-heads/')):
2114 return False
2115 return True
2116
Olivier Robin75ee7252018-04-13 10:02:56 +02002117 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002118 if git_common.is_dirty_git_tree('land'):
2119 return 1
Karen Qian40c19422019-03-13 21:28:29 +00002120
tandriid60367b2016-06-22 05:25:12 -07002121 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00002122 if not force and self._IsCqConfigured():
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002123 confirm_or_exit('\nIt seems this repository has a CQ, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002124 'which can test and land changes for you. '
2125 'Are you sure you wish to bypass it?\n',
2126 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002127 differs = True
tandriic4344b52016-08-29 06:04:54 -07002128 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002129 # Note: git diff outputs nothing if there is no diff.
2130 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002131 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002132 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002133 if detail['current_revision'] == last_upload:
2134 differs = False
2135 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002136 print('WARNING: Local branch contents differ from latest uploaded '
2137 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002138 if differs:
2139 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002140 confirm_or_exit(
2141 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2142 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002143 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002144 elif not bypass_hooks:
2145 hook_results = self.RunHook(
2146 committing=True,
2147 may_prompt=not force,
2148 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002149 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2150 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002151 if not hook_results.should_continue():
2152 return 1
2153
2154 self.SubmitIssue(wait_for_merge=True)
2155 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002156 links = self._GetChangeCommit().get('web_links', [])
2157 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002158 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002159 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002160 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002161 return 0
2162
Edward Lemurf38bc172019-09-03 21:02:13 +00002163 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002164 assert parsed_issue_arg.valid
2165
Edward Lemur125d60a2019-09-13 18:25:41 +00002166 self.issue = parsed_issue_arg.issue
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002167
2168 if parsed_issue_arg.hostname:
2169 self._gerrit_host = parsed_issue_arg.hostname
2170 self._gerrit_server = 'https://%s' % self._gerrit_host
2171
tandriic2405f52016-10-10 08:13:15 -07002172 try:
2173 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002174 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002175 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002176
2177 if not parsed_issue_arg.patchset:
2178 # Use current revision by default.
2179 revision_info = detail['revisions'][detail['current_revision']]
2180 patchset = int(revision_info['_number'])
2181 else:
2182 patchset = parsed_issue_arg.patchset
2183 for revision_info in detail['revisions'].itervalues():
2184 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2185 break
2186 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002187 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002188 (parsed_issue_arg.patchset, self.GetIssue()))
2189
Edward Lemur125d60a2019-09-13 18:25:41 +00002190 remote_url = self.GetRemoteUrl()
Aaron Gable697a91b2018-01-19 15:20:15 -08002191 if remote_url.endswith('.git'):
2192 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002193 remote_url = remote_url.rstrip('/')
2194
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002195 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002196 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002197
2198 if remote_url != fetch_info['url']:
2199 DieWithError('Trying to patch a change from %s but this repo appears '
2200 'to be %s.' % (fetch_info['url'], remote_url))
2201
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002202 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002203
Aaron Gable62619a32017-06-16 08:22:09 -07002204 if force:
2205 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2206 print('Checked out commit for change %i patchset %i locally' %
2207 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002208 elif nocommit:
2209 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2210 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002211 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002212 RunGit(['cherry-pick', 'FETCH_HEAD'])
2213 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002214 (parsed_issue_arg.issue, patchset))
2215 print('Note: this created a local commit which does not have '
2216 'the same hash as the one uploaded for review. This will make '
2217 'uploading changes based on top of this branch difficult.\n'
2218 'If you want to do that, use "git cl patch --force" instead.')
2219
Stefan Zagerd08043c2017-10-12 12:07:02 -07002220 if self.GetBranch():
2221 self.SetIssue(parsed_issue_arg.issue)
2222 self.SetPatchset(patchset)
2223 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2224 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2225 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2226 else:
2227 print('WARNING: You are in detached HEAD state.\n'
2228 'The patch has been applied to your checkout, but you will not be '
2229 'able to upload a new patch set to the gerrit issue.\n'
2230 'Try using the \'-b\' option if you would like to work on a '
2231 'branch and/or upload a new patch set.')
2232
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002233 return 0
2234
tandrii16e0b4e2016-06-07 10:34:28 -07002235 def _GerritCommitMsgHookCheck(self, offer_removal):
2236 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2237 if not os.path.exists(hook):
2238 return
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002239 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
2240 # custom developer-made one.
tandrii16e0b4e2016-06-07 10:34:28 -07002241 data = gclient_utils.FileRead(hook)
2242 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2243 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002244 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002245 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002246 'and may interfere with it in subtle ways.\n'
2247 'We recommend you remove the commit-msg hook.')
2248 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002249 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002250 gclient_utils.rm_file_or_tree(hook)
2251 print('Gerrit commit-msg hook removed.')
2252 else:
2253 print('OK, will keep Gerrit commit-msg hook in place.')
2254
Edward Lemur1b52d872019-05-09 21:12:12 +00002255 def _CleanUpOldTraces(self):
2256 """Keep only the last |MAX_TRACES| traces."""
2257 try:
2258 traces = sorted([
2259 os.path.join(TRACES_DIR, f)
2260 for f in os.listdir(TRACES_DIR)
2261 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2262 and not f.startswith('tmp'))
2263 ])
2264 traces_to_delete = traces[:-MAX_TRACES]
2265 for trace in traces_to_delete:
Daniel Chengcf6269b2019-05-18 01:02:12 +00002266 os.remove(trace)
Edward Lemur1b52d872019-05-09 21:12:12 +00002267 except OSError:
2268 print('WARNING: Failed to remove old git traces from\n'
2269 ' %s'
2270 'Consider removing them manually.' % TRACES_DIR)
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002271
Edward Lemur5737f022019-05-17 01:24:00 +00002272 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
Edward Lemur1b52d872019-05-09 21:12:12 +00002273 """Zip and write the git push traces stored in traces_dir."""
2274 gclient_utils.safe_makedirs(TRACES_DIR)
Edward Lemur1b52d872019-05-09 21:12:12 +00002275 traces_zip = trace_name + '-traces'
2276 traces_readme = trace_name + '-README'
Michael Mosse7f0b4c2019-05-08 04:36:24 +00002277 # Create a temporary dir to store git config and gitcookies in. It will be
2278 # compressed and stored next to the traces.
2279 git_info_dir = tempfile.mkdtemp()
Edward Lemur1b52d872019-05-09 21:12:12 +00002280 git_info_zip = trace_name + '-git-info'
2281
Edward Lemur5737f022019-05-17 01:24:00 +00002282 git_push_metadata['now'] = datetime_now().strftime('%c')
Eric Boren67c48202019-05-30 16:52:51 +00002283 if sys.stdin.encoding and sys.stdin.encoding != 'utf-8':
sangwoo.ko7a614332019-05-22 02:46:19 +00002284 git_push_metadata['now'] = git_push_metadata['now'].decode(
2285 sys.stdin.encoding)
2286
Edward Lemur1b52d872019-05-09 21:12:12 +00002287 git_push_metadata['trace_name'] = trace_name
2288 gclient_utils.FileWrite(
2289 traces_readme, TRACES_README_FORMAT % git_push_metadata)
2290
2291 # Keep only the first 6 characters of the git hashes on the packet
2292 # trace. This greatly decreases size after compression.
2293 packet_traces = os.path.join(traces_dir, 'trace-packet')
2294 if os.path.isfile(packet_traces):
2295 contents = gclient_utils.FileRead(packet_traces)
2296 gclient_utils.FileWrite(
2297 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2298 shutil.make_archive(traces_zip, 'zip', traces_dir)
2299
2300 # Collect and compress the git config and gitcookies.
2301 git_config = RunGit(['config', '-l'])
2302 gclient_utils.FileWrite(
2303 os.path.join(git_info_dir, 'git-config'),
2304 git_config)
2305
2306 cookie_auth = gerrit_util.Authenticator.get()
2307 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2308 gitcookies_path = cookie_auth.get_gitcookies_path()
2309 if os.path.isfile(gitcookies_path):
2310 gitcookies = gclient_utils.FileRead(gitcookies_path)
2311 gclient_utils.FileWrite(
2312 os.path.join(git_info_dir, 'gitcookies'),
2313 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2314 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2315
Edward Lemur1b52d872019-05-09 21:12:12 +00002316 gclient_utils.rmtree(git_info_dir)
2317
2318 def _RunGitPushWithTraces(
2319 self, change_desc, refspec, refspec_opts, git_push_metadata):
2320 """Run git push and collect the traces resulting from the execution."""
2321 # Create a temporary directory to store traces in. Traces will be compressed
2322 # and stored in a 'traces' dir inside depot_tools.
2323 traces_dir = tempfile.mkdtemp()
Edward Lemur5737f022019-05-17 01:24:00 +00002324 trace_name = os.path.join(
2325 TRACES_DIR, datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
Edward Lemur0f58ae42019-04-30 17:24:12 +00002326
2327 env = os.environ.copy()
2328 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2329 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Jonathan Nieder9779b142019-05-29 23:19:29 +00002330 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002331 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2332 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2333 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2334
2335 try:
2336 push_returncode = 0
Edward Lemur1b52d872019-05-09 21:12:12 +00002337 remote_url = self.GetRemoteUrl()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002338 before_push = time_time()
2339 push_stdout = gclient_utils.CheckCallAndFilter(
Edward Lemur1b52d872019-05-09 21:12:12 +00002340 ['git', 'push', remote_url, refspec],
Edward Lemur0f58ae42019-04-30 17:24:12 +00002341 env=env,
2342 print_stdout=True,
2343 # Flush after every line: useful for seeing progress when running as
2344 # recipe.
2345 filter_fn=lambda _: sys.stdout.flush())
2346 except subprocess2.CalledProcessError as e:
2347 push_returncode = e.returncode
2348 DieWithError('Failed to create a change. Please examine output above '
2349 'for the reason of the failure.\n'
2350 'Hint: run command below to diagnose common Git/Gerrit '
2351 'credential problems:\n'
Edward Lemur5737f022019-05-17 01:24:00 +00002352 ' git cl creds-check\n'
2353 '\n'
2354 'If git-cl is not working correctly, file a bug under the '
2355 'Infra>SDK component including the files below.\n'
2356 'Review the files before upload, since they might contain '
2357 'sensitive information.\n'
2358 'Set the Restrict-View-Google label so that they are not '
2359 'publicly accessible.\n'
2360 + TRACES_MESSAGE % {'trace_name': trace_name},
Edward Lemur0f58ae42019-04-30 17:24:12 +00002361 change_desc)
2362 finally:
2363 execution_time = time_time() - before_push
2364 metrics.collector.add_repeated('sub_commands', {
2365 'command': 'git push',
2366 'execution_time': execution_time,
2367 'exit_code': push_returncode,
2368 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2369 })
2370
Edward Lemur1b52d872019-05-09 21:12:12 +00002371 git_push_metadata['execution_time'] = execution_time
2372 git_push_metadata['exit_code'] = push_returncode
Edward Lemur5737f022019-05-17 01:24:00 +00002373 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002374
Edward Lemur1b52d872019-05-09 21:12:12 +00002375 self._CleanUpOldTraces()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002376 gclient_utils.rmtree(traces_dir)
2377
2378 return push_stdout
2379
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002380 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002381 """Upload the current branch to Gerrit."""
Mike Frysingera989d552019-08-14 20:51:23 +00002382 if options.squash is None:
tandriia60502f2016-06-20 02:01:53 -07002383 # Load default for user, repo, squash=true, in this order.
2384 options.squash = settings.GetSquashGerritUploads()
tandrii26f3e4e2016-06-10 08:37:04 -07002385
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002386 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002387 branch = GetTargetRef(remote, remote_branch, options.target_branch)
Aaron Gableb56ad332017-01-06 15:24:31 -08002388 # This may be None; default fallback value is determined in logic below.
2389 title = options.title
2390
Dominic Battre7d1c4842017-10-27 09:17:28 +02002391 # Extract bug number from branch name.
2392 bug = options.bug
2393 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2394 if not bug and match:
2395 bug = match.group(1)
2396
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002397 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002398 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002399 if self.GetIssue():
2400 # Try to get the message from a previous upload.
2401 message = self.GetDescription()
2402 if not message:
2403 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002404 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002405 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002406 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002407 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002408 # When uploading a subsequent patchset, -m|--message is taken
2409 # as the patchset title if --title was not provided.
2410 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002411 else:
2412 default_title = RunGit(
2413 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002414 if options.force:
2415 title = default_title
2416 else:
2417 title = ask_for_data(
2418 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002419 change_id = self._GetChangeDetail()['change_id']
2420 while True:
2421 footer_change_ids = git_footers.get_footer_change_id(message)
2422 if footer_change_ids == [change_id]:
2423 break
2424 if not footer_change_ids:
2425 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002426 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002427 continue
2428 # There is already a valid footer but with different or several ids.
2429 # Doing this automatically is non-trivial as we don't want to lose
2430 # existing other footers, yet we want to append just 1 desired
2431 # Change-Id. Thus, just create a new footer, but let user verify the
2432 # new description.
2433 message = '%s\n\nChange-Id: %s' % (message, change_id)
Anthony Polito8b955342019-09-24 19:01:36 +00002434 change_desc = ChangeDescription(message, bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002435 if not options.force:
Anthony Polito8b955342019-09-24 19:01:36 +00002436 print(
2437 'WARNING: change %s has Change-Id footer(s):\n'
2438 ' %s\n'
2439 'but change has Change-Id %s, according to Gerrit.\n'
2440 'Please, check the proposed correction to the description, '
2441 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2442 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2443 change_id))
2444 confirm_or_exit(action='edit')
2445 change_desc.prompt()
2446
2447 message = change_desc.description
2448 if not message:
2449 DieWithError("Description is empty. Aborting...")
2450
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002451 # Continue the while loop.
2452 # Sanity check of this code - we should end up with proper message
2453 # footer.
2454 assert [change_id] == git_footers.get_footer_change_id(message)
Anthony Polito8b955342019-09-24 19:01:36 +00002455 change_desc = ChangeDescription(message, bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002456 else: # if not self.GetIssue()
2457 if options.message:
2458 message = options.message
2459 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002460 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002461 if options.title:
2462 message = options.title + '\n\n' + message
Anthony Polito8b955342019-09-24 19:01:36 +00002463 change_desc = ChangeDescription(message, bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002464 if not options.force:
Anthony Polito8b955342019-09-24 19:01:36 +00002465 change_desc.prompt()
2466
Aaron Gableb56ad332017-01-06 15:24:31 -08002467 # On first upload, patchset title is always this string, while
2468 # --title flag gets converted to first line of message.
2469 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002470 if not change_desc.description:
2471 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002472 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002473 if len(change_ids) > 1:
2474 DieWithError('too many Change-Id footers, at most 1 allowed.')
2475 if not change_ids:
2476 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002477 change_desc.set_description(git_footers.add_footer_change_id(
2478 change_desc.description,
2479 GenerateGerritChangeId(change_desc.description)))
2480 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002481 assert len(change_ids) == 1
2482 change_id = change_ids[0]
2483
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002484 if options.reviewers or options.tbrs or options.add_owners_to:
2485 change_desc.update_reviewers(options.reviewers, options.tbrs,
2486 options.add_owners_to, change)
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002487 if options.preserve_tryjobs:
2488 change_desc.set_preserve_tryjobs()
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002489
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002490 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002491 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2492 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002493 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002494 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2495 desc_tempfile.write(change_desc.description)
2496 desc_tempfile.close()
2497 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2498 '-F', desc_tempfile.name]).strip()
2499 os.remove(desc_tempfile.name)
Anthony Polito8b955342019-09-24 19:01:36 +00002500 else: # if not options.squash
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002501 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002502 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002503 if not change_desc.description:
2504 DieWithError("Description is empty. Aborting...")
2505
2506 if not git_footers.get_footer_change_id(change_desc.description):
2507 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002508 change_desc.set_description(
2509 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002510 if options.reviewers or options.tbrs or options.add_owners_to:
2511 change_desc.update_reviewers(options.reviewers, options.tbrs,
2512 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002513 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002514 # For no-squash mode, we assume the remote called "origin" is the one we
2515 # want. It is not worthwhile to support different workflows for
2516 # no-squash mode.
2517 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002518 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2519
2520 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002521 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002522 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2523 ref_to_push)]).splitlines()
2524 if len(commits) > 1:
2525 print('WARNING: This will upload %d commits. Run the following command '
2526 'to see which commits will be uploaded: ' % len(commits))
2527 print('git log %s..%s' % (parent, ref_to_push))
2528 print('You can also use `git squash-branch` to squash these into a '
2529 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002530 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002531
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002532 if options.reviewers or options.tbrs or options.add_owners_to:
2533 change_desc.update_reviewers(options.reviewers, options.tbrs,
2534 options.add_owners_to, change)
2535
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002536 reviewers = sorted(change_desc.get_reviewers())
Edward Lemur4508b422019-10-03 21:56:35 +00002537 cc = []
2538 # Add CCs from WATCHLISTS and rietveld.cc git config unless this is
2539 # the initial upload, the CL is private, or auto-CCing has ben disabled.
2540 if not (self.GetIssue() or options.private or options.no_autocc):
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002541 cc = self.GetCCList().split(',')
Edward Lemur4508b422019-10-03 21:56:35 +00002542 # Add cc's from the --cc flag.
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002543 if options.cc:
2544 cc.extend(options.cc)
2545 cc = filter(None, [email.strip() for email in cc])
2546 if change_desc.get_cced():
2547 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002548 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2549 valid_accounts = set(reviewers + cc)
2550 # TODO(crbug/877717): relax this for all hosts.
2551 else:
2552 valid_accounts = gerrit_util.ValidAccounts(
2553 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002554 logging.info('accounts %s are recognized, %s invalid',
2555 sorted(valid_accounts),
2556 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002557
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002558 # Extra options that can be specified at push time. Doc:
2559 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002560 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002561
Aaron Gable844cf292017-06-28 11:32:59 -07002562 # By default, new changes are started in WIP mode, and subsequent patchsets
2563 # don't send email. At any time, passing --send-mail will mark the change
2564 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002565 if options.send_mail:
2566 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002567 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002568 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002569 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002570 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002571 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002572
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002573 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002574 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002575
Aaron Gable9b713dd2016-12-14 16:04:21 -08002576 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002577 # Punctuation and whitespace in |title| must be percent-encoded.
2578 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002579
agablec6787972016-09-09 16:13:34 -07002580 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002581 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002582
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002583 for r in sorted(reviewers):
2584 if r in valid_accounts:
2585 refspec_opts.append('r=%s' % r)
2586 reviewers.remove(r)
2587 else:
2588 # TODO(tandrii): this should probably be a hard failure.
2589 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2590 % r)
2591 for c in sorted(cc):
2592 # refspec option will be rejected if cc doesn't correspond to an
2593 # account, even though REST call to add such arbitrary cc may succeed.
2594 if c in valid_accounts:
2595 refspec_opts.append('cc=%s' % c)
2596 cc.remove(c)
2597
rmistry9eadede2016-09-19 11:22:43 -07002598 if options.topic:
2599 # Documentation on Gerrit topics is here:
2600 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002601 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002602
Edward Lemur687ca902018-12-05 02:30:30 +00002603 if options.enable_auto_submit:
2604 refspec_opts.append('l=Auto-Submit+1')
2605 if options.use_commit_queue:
2606 refspec_opts.append('l=Commit-Queue+2')
2607 elif options.cq_dry_run:
2608 refspec_opts.append('l=Commit-Queue+1')
2609
2610 if change_desc.get_reviewers(tbr_only=True):
2611 score = gerrit_util.GetCodeReviewTbrScore(
2612 self._GetGerritHost(),
2613 self._GetGerritProject())
2614 refspec_opts.append('l=Code-Review+%s' % score)
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002615
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002616 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002617 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002618 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002619 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002620 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2621
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002622 refspec_suffix = ''
2623 if refspec_opts:
2624 refspec_suffix = '%' + ','.join(refspec_opts)
2625 assert ' ' not in refspec_suffix, (
2626 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2627 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2628
Edward Lemur1b52d872019-05-09 21:12:12 +00002629 git_push_metadata = {
2630 'gerrit_host': self._GetGerritHost(),
2631 'title': title or '<untitled>',
2632 'change_id': change_id,
2633 'description': change_desc.description,
2634 }
2635 push_stdout = self._RunGitPushWithTraces(
2636 change_desc, refspec, refspec_opts, git_push_metadata)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002637
2638 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002639 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002640 change_numbers = [m.group(1)
2641 for m in map(regex.match, push_stdout.splitlines())
2642 if m]
2643 if len(change_numbers) != 1:
2644 DieWithError(
2645 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002646 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002647 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002648 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002649
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002650 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002651 # GetIssue() is not set in case of non-squash uploads according to tests.
2652 # TODO(agable): non-squash uploads in git cl should be removed.
2653 gerrit_util.AddReviewers(
2654 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002655 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002656 reviewers, cc,
2657 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002658
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002659 return 0
2660
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002661 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2662 change_desc):
2663 """Computes parent of the generated commit to be uploaded to Gerrit.
2664
2665 Returns revision or a ref name.
2666 """
2667 if custom_cl_base:
2668 # Try to avoid creating additional unintended CLs when uploading, unless
2669 # user wants to take this risk.
2670 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2671 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2672 local_ref_of_target_remote])
2673 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002674 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002675 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2676 'If you proceed with upload, more than 1 CL may be created by '
2677 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2678 'If you are certain that specified base `%s` has already been '
2679 'uploaded to Gerrit as another CL, you may proceed.\n' %
2680 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2681 if not force:
2682 confirm_or_exit(
2683 'Do you take responsibility for cleaning up potential mess '
2684 'resulting from proceeding with upload?',
2685 action='upload')
2686 return custom_cl_base
2687
Aaron Gablef97e33d2017-03-30 15:44:27 -07002688 if remote != '.':
2689 return self.GetCommonAncestorWithUpstream()
2690
2691 # If our upstream branch is local, we base our squashed commit on its
2692 # squashed version.
2693 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2694
Aaron Gablef97e33d2017-03-30 15:44:27 -07002695 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002696 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002697
2698 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002699 # TODO(tandrii): consider checking parent change in Gerrit and using its
2700 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2701 # the tree hash of the parent branch. The upside is less likely bogus
2702 # requests to reupload parent change just because it's uploadhash is
2703 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07002704 parent = RunGit(['config',
2705 'branch.%s.gerritsquashhash' % upstream_branch_name],
2706 error_ok=True).strip()
2707 # Verify that the upstream branch has been uploaded too, otherwise
2708 # Gerrit will create additional CLs when uploading.
2709 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2710 RunGitSilent(['rev-parse', parent + ':'])):
2711 DieWithError(
2712 '\nUpload upstream branch %s first.\n'
2713 'It is likely that this branch has been rebased since its last '
2714 'upload, so you just need to upload it again.\n'
2715 '(If you uploaded it with --no-squash, then branch dependencies '
2716 'are not supported, and you should reupload with --squash.)'
2717 % upstream_branch_name,
2718 change_desc)
2719 return parent
2720
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002721 def _AddChangeIdToCommitMessage(self, options, args):
2722 """Re-commits using the current message, assumes the commit hook is in
2723 place.
2724 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002725 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002726 git_command = ['commit', '--amend', '-m', log_desc]
2727 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002728 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002729 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002730 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002731 return new_log_desc
2732 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002733 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002734
tandriie113dfd2016-10-11 10:20:12 -07002735 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002736 try:
2737 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002738 except GerritChangeNotExists:
2739 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002740
2741 if data['status'] in ('ABANDONED', 'MERGED'):
2742 return 'CL %s is closed' % self.GetIssue()
2743
Edward Lemurf0faf482019-09-25 20:40:17 +00002744 # TODO(1004447): Remove on Oct 9th, 2019.
2745 def GetLegacyProperties(self, patchset=None):
2746 host = self.GetCodereviewServer()
2747 issue = self.GetIssue()
2748 patchset = int(patchset or self.GetPatchset())
2749 data = self._GetChangeDetail(['ALL_REVISIONS'])
2750
2751 assert host and issue and patchset, 'CL must be uploaded first'
2752
2753 revision_data = None # Pylint wants it to be defined.
2754 for revision_data in data['revisions'].itervalues():
2755 if int(revision_data['_number']) == patchset:
2756 break
2757 else:
2758 raise Exception('Patchset %d is not known in Gerrit change %d' %
2759 (patchset, issue))
2760
2761 return {
2762 'patch_issue': issue,
2763 'patch_set': patchset,
2764 'patch_project': data['project'],
2765 'patch_storage': 'gerrit',
2766 'patch_ref': revision_data['fetch']['http']['ref'],
2767 'patch_repository_url': revision_data['fetch']['http']['url'],
2768 'patch_gerrit_url': host,
2769 }
2770
2771
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002772 def GetGerritChange(self, patchset=None):
2773 """Returns a buildbucket.v2.GerritChange message for the current issue."""
2774 host = urlparse.urlparse(self.GetCodereviewServer()).hostname
2775 issue = self.GetIssue()
Edward Lemur2c210a42019-09-16 23:58:35 +00002776 patchset = int(patchset or self.GetPatchset())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002777 data = self._GetChangeDetail(['ALL_REVISIONS'])
2778
2779 assert host and issue and patchset, 'CL must be uploaded first'
2780
2781 has_patchset = any(
2782 int(revision_data['_number']) == patchset
2783 for revision_data in data['revisions'].itervalues())
2784 if not has_patchset:
Aaron Gablea45ee112016-11-22 15:14:38 -08002785 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002786 (patchset, self.GetIssue()))
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002787
tandrii8c5a3532016-11-04 07:52:02 -07002788 return {
Edward Lemurd4d1ba42019-09-20 21:46:37 +00002789 'host': host,
2790 'change': issue,
2791 'project': data['project'],
2792 'patchset': patchset,
tandrii8c5a3532016-11-04 07:52:02 -07002793 }
tandriie113dfd2016-10-11 10:20:12 -07002794
tandriide281ae2016-10-12 06:02:30 -07002795 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002796 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002797
Edward Lemur707d70b2018-02-07 00:50:14 +01002798 def GetReviewers(self):
2799 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00002800 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01002801
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002802
2803_CODEREVIEW_IMPLEMENTATIONS = {
Edward Lemur125d60a2019-09-13 18:25:41 +00002804 'gerrit': Changelist,
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002805}
2806
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002807
iannuccie53c9352016-08-17 14:40:40 -07002808def _add_codereview_issue_select_options(parser, extra=""):
2809 _add_codereview_select_options(parser)
2810
2811 text = ('Operate on this issue number instead of the current branch\'s '
2812 'implicit issue.')
2813 if extra:
2814 text += ' '+extra
2815 parser.add_option('-i', '--issue', type=int, help=text)
2816
2817
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002818def _add_codereview_select_options(parser):
Edward Lemurf38bc172019-09-03 21:02:13 +00002819 """Appends --gerrit option to force specific codereview."""
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002820 parser.codereview_group = optparse.OptionGroup(
Edward Lemurf38bc172019-09-03 21:02:13 +00002821 parser, 'DEPRECATED! Codereview override options')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002822 parser.add_option_group(parser.codereview_group)
2823 parser.codereview_group.add_option(
2824 '--gerrit', action='store_true',
Edward Lemurf38bc172019-09-03 21:02:13 +00002825 help='Deprecated. Noop. Do not use.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002826
2827
tandriif9aefb72016-07-01 09:06:51 -07002828def _get_bug_line_values(default_project, bugs):
2829 """Given default_project and comma separated list of bugs, yields bug line
2830 values.
2831
2832 Each bug can be either:
2833 * a number, which is combined with default_project
2834 * string, which is left as is.
2835
2836 This function may produce more than one line, because bugdroid expects one
2837 project per line.
2838
2839 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2840 ['v8:123', 'chromium:789']
2841 """
2842 default_bugs = []
2843 others = []
2844 for bug in bugs.split(','):
2845 bug = bug.strip()
2846 if bug:
2847 try:
2848 default_bugs.append(int(bug))
2849 except ValueError:
2850 others.append(bug)
2851
2852 if default_bugs:
2853 default_bugs = ','.join(map(str, default_bugs))
2854 if default_project:
2855 yield '%s:%s' % (default_project, default_bugs)
2856 else:
2857 yield default_bugs
2858 for other in sorted(others):
2859 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2860 yield other
2861
2862
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002863class ChangeDescription(object):
2864 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002865 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002866 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07002867 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002868 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002869 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
2870 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
2871 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
2872 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002873
Anthony Polito8b955342019-09-24 19:01:36 +00002874 def __init__(self, description, bug=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00002875 self._description_lines = (description or '').strip().splitlines()
Anthony Polito8b955342019-09-24 19:01:36 +00002876 if bug:
2877 regexp = re.compile(self.BUG_LINE)
2878 prefix = settings.GetBugPrefix()
2879 if not any((regexp.match(line) for line in self._description_lines)):
2880 values = list(_get_bug_line_values(prefix, bug))
2881 self.append_footer('Bug: %s' % ', '.join(values))
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002882
agable@chromium.org42c20792013-09-12 17:34:49 +00002883 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08002884 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00002885 return '\n'.join(self._description_lines)
2886
2887 def set_description(self, desc):
2888 if isinstance(desc, basestring):
2889 lines = desc.splitlines()
2890 else:
2891 lines = [line.rstrip() for line in desc]
2892 while lines and not lines[0]:
2893 lines.pop(0)
2894 while lines and not lines[-1]:
2895 lines.pop(-1)
2896 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002897
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002898 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
2899 """Rewrites the R=/TBR= line(s) as a single line each.
2900
2901 Args:
2902 reviewers (list(str)) - list of additional emails to use for reviewers.
2903 tbrs (list(str)) - list of additional emails to use for TBRs.
2904 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
2905 the change that are missing OWNER coverage. If this is not None, you
2906 must also pass a value for `change`.
2907 change (Change) - The Change that should be used for OWNERS lookups.
2908 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002909 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002910 assert isinstance(tbrs, list), tbrs
2911
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002912 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07002913 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002914
2915 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002916 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002917
2918 reviewers = set(reviewers)
2919 tbrs = set(tbrs)
2920 LOOKUP = {
2921 'TBR': tbrs,
2922 'R': reviewers,
2923 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002924
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002925 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00002926 regexp = re.compile(self.R_LINE)
2927 matches = [regexp.match(line) for line in self._description_lines]
2928 new_desc = [l for i, l in enumerate(self._description_lines)
2929 if not matches[i]]
2930 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002931
agable@chromium.org42c20792013-09-12 17:34:49 +00002932 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002933
2934 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00002935 for match in matches:
2936 if not match:
2937 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002938 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
2939
2940 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002941 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00002942 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02002943 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002944 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07002945 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002946 LOOKUP[add_owners_to].update(
2947 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002948
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002949 # If any folks ended up in both groups, remove them from tbrs.
2950 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002951
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002952 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
2953 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00002954
2955 # Put the new lines in the description where the old first R= line was.
2956 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2957 if 0 <= line_loc < len(self._description_lines):
2958 if new_tbr_line:
2959 self._description_lines.insert(line_loc, new_tbr_line)
2960 if new_r_line:
2961 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002962 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002963 if new_r_line:
2964 self.append_footer(new_r_line)
2965 if new_tbr_line:
2966 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002967
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002968 def set_preserve_tryjobs(self):
2969 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
2970 footers = git_footers.parse_footers(self.description)
2971 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
2972 if v.lower() == 'true':
2973 return
2974 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
2975
Anthony Polito8b955342019-09-24 19:01:36 +00002976 def prompt(self):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002977 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002978 self.set_description([
2979 '# Enter a description of the change.',
2980 '# This will be displayed on the codereview site.',
2981 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002982 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002983 '--------------------',
2984 ] + self._description_lines)
agable@chromium.org42c20792013-09-12 17:34:49 +00002985 regexp = re.compile(self.BUG_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00002986 prefix = settings.GetBugPrefix()
agable@chromium.org42c20792013-09-12 17:34:49 +00002987 if not any((regexp.match(line) for line in self._description_lines)):
Anthony Polito8b955342019-09-24 19:01:36 +00002988 self.append_footer('Bug: %s' % prefix)
tandriif9aefb72016-07-01 09:06:51 -07002989
agable@chromium.org42c20792013-09-12 17:34:49 +00002990 content = gclient_utils.RunEditor(self.description, True,
Anthony Polito8b955342019-09-24 19:01:36 +00002991 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002992 if not content:
2993 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002994 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002995
Bruce Dawson2377b012018-01-11 16:46:49 -08002996 # Strip off comments and default inserted "Bug:" line.
2997 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00002998 (line.startswith('#') or
2999 line.rstrip() == "Bug:" or
3000 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00003001 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003002 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003003 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003004
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003005 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003006 """Adds a footer line to the description.
3007
3008 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3009 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3010 that Gerrit footers are always at the end.
3011 """
3012 parsed_footer_line = git_footers.parse_footer(line)
3013 if parsed_footer_line:
3014 # Line is a gerrit footer in the form: Footer-Key: any value.
3015 # Thus, must be appended observing Gerrit footer rules.
3016 self.set_description(
3017 git_footers.add_footer(self.description,
3018 key=parsed_footer_line[0],
3019 value=parsed_footer_line[1]))
3020 return
3021
3022 if not self._description_lines:
3023 self._description_lines.append(line)
3024 return
3025
3026 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3027 if gerrit_footers:
3028 # git_footers.split_footers ensures that there is an empty line before
3029 # actual (gerrit) footers, if any. We have to keep it that way.
3030 assert top_lines and top_lines[-1] == ''
3031 top_lines, separator = top_lines[:-1], top_lines[-1:]
3032 else:
3033 separator = [] # No need for separator if there are no gerrit_footers.
3034
3035 prev_line = top_lines[-1] if top_lines else ''
3036 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3037 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3038 top_lines.append('')
3039 top_lines.append(line)
3040 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003041
tandrii99a72f22016-08-17 14:33:24 -07003042 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003043 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003044 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003045 reviewers = [match.group(2).strip()
3046 for match in matches
3047 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003048 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003049
bradnelsond975b302016-10-23 12:20:23 -07003050 def get_cced(self):
3051 """Retrieves the list of reviewers."""
3052 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3053 cced = [match.group(2).strip() for match in matches if match]
3054 return cleanup_list(cced)
3055
Nodir Turakulov23b82142017-11-16 11:04:25 -08003056 def get_hash_tags(self):
3057 """Extracts and sanitizes a list of Gerrit hashtags."""
3058 subject = (self._description_lines or ('',))[0]
3059 subject = re.sub(
3060 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3061
3062 tags = []
3063 start = 0
3064 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3065 while True:
3066 m = bracket_exp.match(subject, start)
3067 if not m:
3068 break
3069 tags.append(self.sanitize_hash_tag(m.group(1)))
3070 start = m.end()
3071
3072 if not tags:
3073 # Try "Tag: " prefix.
3074 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3075 if m:
3076 tags.append(self.sanitize_hash_tag(m.group(1)))
3077 return tags
3078
3079 @classmethod
3080 def sanitize_hash_tag(cls, tag):
3081 """Returns a sanitized Gerrit hash tag.
3082
3083 A sanitized hashtag can be used as a git push refspec parameter value.
3084 """
3085 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3086
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003087 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3088 """Updates this commit description given the parent.
3089
3090 This is essentially what Gnumbd used to do.
3091 Consult https://goo.gl/WMmpDe for more details.
3092 """
3093 assert parent_msg # No, orphan branch creation isn't supported.
3094 assert parent_hash
3095 assert dest_ref
3096 parent_footer_map = git_footers.parse_footers(parent_msg)
3097 # This will also happily parse svn-position, which GnumbD is no longer
3098 # supporting. While we'd generate correct footers, the verifier plugin
3099 # installed in Gerrit will block such commit (ie git push below will fail).
3100 parent_position = git_footers.get_position(parent_footer_map)
3101
3102 # Cherry-picks may have last line obscuring their prior footers,
3103 # from git_footers perspective. This is also what Gnumbd did.
3104 cp_line = None
3105 if (self._description_lines and
3106 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3107 cp_line = self._description_lines.pop()
3108
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003109 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003110
3111 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3112 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003113 for i, line in enumerate(footer_lines):
3114 k, v = git_footers.parse_footer(line) or (None, None)
3115 if k and k.startswith('Cr-'):
3116 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003117
3118 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003119 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003120 if parent_position[0] == dest_ref:
3121 # Same branch as parent.
3122 number = int(parent_position[1]) + 1
3123 else:
3124 number = 1 # New branch, and extra lineage.
3125 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3126 int(parent_position[1])))
3127
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003128 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3129 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003130
3131 self._description_lines = top_lines
3132 if cp_line:
3133 self._description_lines.append(cp_line)
3134 if self._description_lines[-1] != '':
3135 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003136 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003137
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003138
Aaron Gablea1bab272017-04-11 16:38:18 -07003139def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003140 """Retrieves the reviewers that approved a CL from the issue properties with
3141 messages.
3142
3143 Note that the list may contain reviewers that are not committer, thus are not
3144 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003145
3146 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003147 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003148 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003149 return sorted(
3150 set(
3151 message['sender']
3152 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003153 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003154 )
3155 )
3156
3157
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003158def FindCodereviewSettingsFile(filename='codereview.settings'):
3159 """Finds the given file starting in the cwd and going up.
3160
3161 Only looks up to the top of the repository unless an
3162 'inherit-review-settings-ok' file exists in the root of the repository.
3163 """
3164 inherit_ok_file = 'inherit-review-settings-ok'
3165 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003166 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003167 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3168 root = '/'
3169 while True:
3170 if filename in os.listdir(cwd):
3171 if os.path.isfile(os.path.join(cwd, filename)):
3172 return open(os.path.join(cwd, filename))
3173 if cwd == root:
3174 break
3175 cwd = os.path.dirname(cwd)
3176
3177
3178def LoadCodereviewSettingsFromFile(fileobj):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003179 """Parses a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003180 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003181
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003182 def SetProperty(name, setting, unset_error_ok=False):
3183 fullname = 'rietveld.' + name
3184 if setting in keyvals:
3185 RunGit(['config', fullname, keyvals[setting]])
3186 else:
3187 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3188
tandrii48df5812016-10-17 03:55:37 -07003189 if not keyvals.get('GERRIT_HOST', False):
3190 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003191 # Only server setting is required. Other settings can be absent.
3192 # In that case, we ignore errors raised during option deletion attempt.
3193 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3194 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3195 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003196 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003197 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3198 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003199 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3200 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003201
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003202 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003203 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003204
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003205 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003206 RunGit(['config', 'gerrit.squash-uploads',
3207 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003208
tandrii@chromium.org28253532016-04-14 13:46:56 +00003209 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003210 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003211 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3212
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003213 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003214 # should be of the form
3215 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3216 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003217 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3218 keyvals['ORIGIN_URL_CONFIG']])
3219
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003220
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003221def urlretrieve(source, destination):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003222 """Downloads a network object to a local file, like urllib.urlretrieve.
3223
3224 This is necessary because urllib is broken for SSL connections via a proxy.
3225 """
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003226 with open(destination, 'w') as f:
3227 f.write(urllib2.urlopen(source).read())
3228
3229
ukai@chromium.org712d6102013-11-27 00:52:58 +00003230def hasSheBang(fname):
3231 """Checks fname is a #! script."""
3232 with open(fname) as f:
3233 return f.read(2).startswith('#!')
3234
3235
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003236# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3237def DownloadHooks(*args, **kwargs):
3238 pass
3239
3240
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003241def DownloadGerritHook(force):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003242 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003243
3244 Args:
3245 force: True to update hooks. False to install hooks if not present.
3246 """
3247 if not settings.GetIsGerrit():
3248 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003249 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003250 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3251 if not os.access(dst, os.X_OK):
3252 if os.path.exists(dst):
3253 if not force:
3254 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003255 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003256 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003257 if not hasSheBang(dst):
3258 DieWithError('Not a script: %s\n'
3259 'You need to download from\n%s\n'
3260 'into .git/hooks/commit-msg and '
3261 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003262 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3263 except Exception:
3264 if os.path.exists(dst):
3265 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003266 DieWithError('\nFailed to download hooks.\n'
3267 'You need to download from\n%s\n'
3268 'into .git/hooks/commit-msg and '
3269 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003270
3271
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003272class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003273 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003274
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003275 _GOOGLESOURCE = 'googlesource.com'
3276
3277 def __init__(self):
3278 # Cached list of [host, identity, source], where source is either
3279 # .gitcookies or .netrc.
3280 self._all_hosts = None
3281
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003282 def ensure_configured_gitcookies(self):
3283 """Runs checks and suggests fixes to make git use .gitcookies from default
3284 path."""
3285 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3286 configured_path = RunGitSilent(
3287 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003288 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003289 if configured_path:
3290 self._ensure_default_gitcookies_path(configured_path, default)
3291 else:
3292 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003293
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003294 @staticmethod
3295 def _ensure_default_gitcookies_path(configured_path, default_path):
3296 assert configured_path
3297 if configured_path == default_path:
3298 print('git is already configured to use your .gitcookies from %s' %
3299 configured_path)
3300 return
3301
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003302 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003303 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3304 (configured_path, default_path))
3305
3306 if not os.path.exists(configured_path):
3307 print('However, your configured .gitcookies file is missing.')
3308 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3309 action='reconfigure')
3310 RunGit(['config', '--global', 'http.cookiefile', default_path])
3311 return
3312
3313 if os.path.exists(default_path):
3314 print('WARNING: default .gitcookies file already exists %s' %
3315 default_path)
3316 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3317 default_path)
3318
3319 confirm_or_exit('Move existing .gitcookies to default location?',
3320 action='move')
3321 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003322 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003323 print('Moved and reconfigured git to use .gitcookies from %s' %
3324 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003325
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003326 @staticmethod
3327 def _configure_gitcookies_path(default_path):
3328 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3329 if os.path.exists(netrc_path):
3330 print('You seem to be using outdated .netrc for git credentials: %s' %
3331 netrc_path)
3332 print('This tool will guide you through setting up recommended '
3333 '.gitcookies store for git credentials.\n'
3334 '\n'
3335 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3336 ' git config --global --unset http.cookiefile\n'
3337 ' mv %s %s.backup\n\n' % (default_path, default_path))
3338 confirm_or_exit(action='setup .gitcookies')
3339 RunGit(['config', '--global', 'http.cookiefile', default_path])
3340 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003341
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003342 def get_hosts_with_creds(self, include_netrc=False):
3343 if self._all_hosts is None:
3344 a = gerrit_util.CookiesAuthenticator()
3345 self._all_hosts = [
3346 (h, u, s)
3347 for h, u, s in itertools.chain(
3348 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3349 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3350 )
3351 if h.endswith(self._GOOGLESOURCE)
3352 ]
3353
3354 if include_netrc:
3355 return self._all_hosts
3356 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3357
3358 def print_current_creds(self, include_netrc=False):
3359 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3360 if not hosts:
3361 print('No Git/Gerrit credentials found')
3362 return
3363 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3364 header = [('Host', 'User', 'Which file'),
3365 ['=' * l for l in lengths]]
3366 for row in (header + hosts):
3367 print('\t'.join((('%%+%ds' % l) % s)
3368 for l, s in zip(lengths, row)))
3369
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003370 @staticmethod
3371 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003372 """Parses identity "git-<username>.domain" into <username> and domain."""
3373 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003374 # distinguishable from sub-domains. But we do know typical domains:
3375 if identity.endswith('.chromium.org'):
3376 domain = 'chromium.org'
3377 username = identity[:-len('.chromium.org')]
3378 else:
3379 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003380 if username.startswith('git-'):
3381 username = username[len('git-'):]
3382 return username, domain
3383
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003384 def _canonical_git_googlesource_host(self, host):
3385 """Normalizes Gerrit hosts (with '-review') to Git host."""
3386 assert host.endswith(self._GOOGLESOURCE)
3387 # Prefix doesn't include '.' at the end.
3388 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3389 if prefix.endswith('-review'):
3390 prefix = prefix[:-len('-review')]
3391 return prefix + '.' + self._GOOGLESOURCE
3392
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003393 def _canonical_gerrit_googlesource_host(self, host):
3394 git_host = self._canonical_git_googlesource_host(host)
3395 prefix = git_host.split('.', 1)[0]
3396 return prefix + '-review.' + self._GOOGLESOURCE
3397
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003398 def _get_counterpart_host(self, host):
3399 assert host.endswith(self._GOOGLESOURCE)
3400 git = self._canonical_git_googlesource_host(host)
3401 gerrit = self._canonical_gerrit_googlesource_host(git)
3402 return git if gerrit == host else gerrit
3403
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003404 def has_generic_host(self):
3405 """Returns whether generic .googlesource.com has been configured.
3406
3407 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3408 """
3409 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3410 if host == '.' + self._GOOGLESOURCE:
3411 return True
3412 return False
3413
3414 def _get_git_gerrit_identity_pairs(self):
3415 """Returns map from canonic host to pair of identities (Git, Gerrit).
3416
3417 One of identities might be None, meaning not configured.
3418 """
3419 host_to_identity_pairs = {}
3420 for host, identity, _ in self.get_hosts_with_creds():
3421 canonical = self._canonical_git_googlesource_host(host)
3422 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3423 idx = 0 if canonical == host else 1
3424 pair[idx] = identity
3425 return host_to_identity_pairs
3426
3427 def get_partially_configured_hosts(self):
3428 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003429 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3430 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3431 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003432
3433 def get_conflicting_hosts(self):
3434 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003435 host
3436 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003437 if None not in (i1, i2) and i1 != i2)
3438
3439 def get_duplicated_hosts(self):
3440 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3441 return set(host for host, count in counters.iteritems() if count > 1)
3442
3443 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3444 'chromium.googlesource.com': 'chromium.org',
3445 'chrome-internal.googlesource.com': 'google.com',
3446 }
3447
3448 def get_hosts_with_wrong_identities(self):
3449 """Finds hosts which **likely** reference wrong identities.
3450
3451 Note: skips hosts which have conflicting identities for Git and Gerrit.
3452 """
3453 hosts = set()
3454 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3455 pair = self._get_git_gerrit_identity_pairs().get(host)
3456 if pair and pair[0] == pair[1]:
3457 _, domain = self._parse_identity(pair[0])
3458 if domain != expected:
3459 hosts.add(host)
3460 return hosts
3461
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003462 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003463 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003464 hosts = sorted(hosts)
3465 assert hosts
3466 if extra_column_func is None:
3467 extras = [''] * len(hosts)
3468 else:
3469 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003470 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3471 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003472 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003473 lines.append(tmpl % he)
3474 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003475
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003476 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003477 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003478 yield ('.googlesource.com wildcard record detected',
3479 ['Chrome Infrastructure team recommends to list full host names '
3480 'explicitly.'],
3481 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003482
3483 dups = self.get_duplicated_hosts()
3484 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003485 yield ('The following hosts were defined twice',
3486 self._format_hosts(dups),
3487 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003488
3489 partial = self.get_partially_configured_hosts()
3490 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003491 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3492 'These hosts are missing',
3493 self._format_hosts(partial, lambda host: 'but %s defined' %
3494 self._get_counterpart_host(host)),
3495 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003496
3497 conflicting = self.get_conflicting_hosts()
3498 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003499 yield ('The following Git hosts have differing credentials from their '
3500 'Gerrit counterparts',
3501 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3502 tuple(self._get_git_gerrit_identity_pairs()[host])),
3503 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003504
3505 wrong = self.get_hosts_with_wrong_identities()
3506 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003507 yield ('These hosts likely use wrong identity',
3508 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3509 (self._get_git_gerrit_identity_pairs()[host][0],
3510 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3511 wrong)
3512
3513 def find_and_report_problems(self):
3514 """Returns True if there was at least one problem, else False."""
3515 found = False
3516 bad_hosts = set()
3517 for title, sublines, hosts in self._find_problems():
3518 if not found:
3519 found = True
3520 print('\n\n.gitcookies problem report:\n')
3521 bad_hosts.update(hosts or [])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003522 print(' %s%s' % (title, (':' if sublines else '')))
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003523 if sublines:
3524 print()
3525 print(' %s' % '\n '.join(sublines))
3526 print()
3527
3528 if bad_hosts:
3529 assert found
3530 print(' You can manually remove corresponding lines in your %s file and '
3531 'visit the following URLs with correct account to generate '
3532 'correct credential lines:\n' %
3533 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3534 print(' %s' % '\n '.join(sorted(set(
3535 gerrit_util.CookiesAuthenticator().get_new_password_url(
3536 self._canonical_git_googlesource_host(host))
3537 for host in bad_hosts
3538 ))))
3539 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003540
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003541
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003542@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003543def CMDcreds_check(parser, args):
3544 """Checks credentials and suggests changes."""
3545 _, _ = parser.parse_args(args)
3546
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003547 # Code below checks .gitcookies. Abort if using something else.
3548 authn = gerrit_util.Authenticator.get()
3549 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3550 if isinstance(authn, gerrit_util.GceAuthenticator):
3551 DieWithError(
3552 'This command is not designed for GCE, are you on a bot?\n'
3553 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3554 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003555 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003556 'This command is not designed for bot environment. It checks '
3557 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003558
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003559 checker = _GitCookiesChecker()
3560 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003561
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003562 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003563 checker.print_current_creds(include_netrc=True)
3564
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003565 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003566 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003567 return 0
3568 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003569
3570
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003571@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003572def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003573 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003574 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3575 branch = ShortBranchName(branchref)
3576 _, args = parser.parse_args(args)
3577 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003578 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003579 return RunGit(['config', 'branch.%s.base-url' % branch],
3580 error_ok=False).strip()
3581 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003582 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003583 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3584 error_ok=False).strip()
3585
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003586
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003587def color_for_status(status):
3588 """Maps a Changelist status to color, for CMDstatus and other tools."""
3589 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003590 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003591 'waiting': Fore.BLUE,
3592 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003593 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003594 'lgtm': Fore.GREEN,
3595 'commit': Fore.MAGENTA,
3596 'closed': Fore.CYAN,
3597 'error': Fore.WHITE,
3598 }.get(status, Fore.WHITE)
3599
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003600
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003601def get_cl_statuses(changes, fine_grained, max_processes=None):
3602 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003603
3604 If fine_grained is true, this will fetch CL statuses from the server.
3605 Otherwise, simply indicate if there's a matching url for the given branches.
3606
3607 If max_processes is specified, it is used as the maximum number of processes
3608 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3609 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003610
3611 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003612 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003613 if not changes:
3614 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003615
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003616 if not fine_grained:
3617 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003618 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003619 for cl in changes:
3620 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003621 return
3622
3623 # First, sort out authentication issues.
3624 logging.debug('ensuring credentials exist')
3625 for cl in changes:
3626 cl.EnsureAuthenticated(force=False, refresh=True)
3627
3628 def fetch(cl):
3629 try:
3630 return (cl, cl.GetStatus())
3631 except:
3632 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003633 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003634 raise
3635
3636 threads_count = len(changes)
3637 if max_processes:
3638 threads_count = max(1, min(threads_count, max_processes))
3639 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3640
3641 pool = ThreadPool(threads_count)
3642 fetched_cls = set()
3643 try:
3644 it = pool.imap_unordered(fetch, changes).__iter__()
3645 while True:
3646 try:
3647 cl, status = it.next(timeout=5)
3648 except multiprocessing.TimeoutError:
3649 break
3650 fetched_cls.add(cl)
3651 yield cl, status
3652 finally:
3653 pool.close()
3654
3655 # Add any branches that failed to fetch.
3656 for cl in set(changes) - fetched_cls:
3657 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003658
rmistry@google.com2dd99862015-06-22 12:22:18 +00003659
3660def upload_branch_deps(cl, args):
3661 """Uploads CLs of local branches that are dependents of the current branch.
3662
3663 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003664
3665 test1 -> test2.1 -> test3.1
3666 -> test3.2
3667 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003668
3669 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3670 run on the dependent branches in this order:
3671 test2.1, test3.1, test3.2, test2.2, test3.3
3672
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003673 Note: This function does not rebase your local dependent branches. Use it
3674 when you make a change to the parent branch that will not conflict
3675 with its dependent branches, and you would like their dependencies
3676 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003677 """
3678 if git_common.is_dirty_git_tree('upload-branch-deps'):
3679 return 1
3680
3681 root_branch = cl.GetBranch()
3682 if root_branch is None:
3683 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3684 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003685 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003686 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3687 'patchset dependencies without an uploaded CL.')
3688
3689 branches = RunGit(['for-each-ref',
3690 '--format=%(refname:short) %(upstream:short)',
3691 'refs/heads'])
3692 if not branches:
3693 print('No local branches found.')
3694 return 0
3695
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003696 # Create a dictionary of all local branches to the branches that are
3697 # dependent on it.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003698 tracked_to_dependents = collections.defaultdict(list)
3699 for b in branches.splitlines():
3700 tokens = b.split()
3701 if len(tokens) == 2:
3702 branch_name, tracked = tokens
3703 tracked_to_dependents[tracked].append(branch_name)
3704
vapiera7fbd5a2016-06-16 09:17:49 -07003705 print()
3706 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003707 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003708
rmistry@google.com2dd99862015-06-22 12:22:18 +00003709 def traverse_dependents_preorder(branch, padding=''):
3710 dependents_to_process = tracked_to_dependents.get(branch, [])
3711 padding += ' '
3712 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003713 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003714 dependents.append(dependent)
3715 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003716
rmistry@google.com2dd99862015-06-22 12:22:18 +00003717 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003718 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003719
3720 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003721 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003722 return 0
3723
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003724 confirm_or_exit('This command will checkout all dependent branches and run '
3725 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003726
rmistry@google.com2dd99862015-06-22 12:22:18 +00003727 # Record all dependents that failed to upload.
3728 failures = {}
3729 # Go through all dependents, checkout the branch and upload.
3730 try:
3731 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003732 print()
3733 print('--------------------------------------')
3734 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003735 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003736 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003737 try:
3738 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003739 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003740 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003741 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003742 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003743 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003744 finally:
3745 # Swap back to the original root branch.
3746 RunGit(['checkout', '-q', root_branch])
3747
vapiera7fbd5a2016-06-16 09:17:49 -07003748 print()
3749 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003750 for dependent_branch in dependents:
3751 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003752 print(' %s : %s' % (dependent_branch, upload_status))
3753 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003754
3755 return 0
3756
3757
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003758@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003759def CMDarchive(parser, args):
3760 """Archives and deletes branches associated with closed changelists."""
3761 parser.add_option(
3762 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003763 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003764 parser.add_option(
3765 '-f', '--force', action='store_true',
3766 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003767 parser.add_option(
3768 '-d', '--dry-run', action='store_true',
3769 help='Skip the branch tagging and removal steps.')
3770 parser.add_option(
3771 '-t', '--notags', action='store_true',
3772 help='Do not tag archived branches. '
3773 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003774
kmarshall3bff56b2016-06-06 18:31:47 -07003775 options, args = parser.parse_args(args)
3776 if args:
3777 parser.error('Unsupported args: %s' % ' '.join(args))
kmarshall3bff56b2016-06-06 18:31:47 -07003778
3779 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3780 if not branches:
3781 return 0
3782
vapiera7fbd5a2016-06-16 09:17:49 -07003783 print('Finding all branches associated with closed issues...')
Edward Lemur934836a2019-09-09 20:16:54 +00003784 changes = [Changelist(branchref=b)
3785 for b in branches.splitlines()]
kmarshall3bff56b2016-06-06 18:31:47 -07003786 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3787 statuses = get_cl_statuses(changes,
3788 fine_grained=True,
3789 max_processes=options.maxjobs)
3790 proposal = [(cl.GetBranch(),
3791 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3792 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00003793 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07003794 proposal.sort()
3795
3796 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003797 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003798 return 0
3799
3800 current_branch = GetCurrentBranch()
3801
vapiera7fbd5a2016-06-16 09:17:49 -07003802 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003803 if options.notags:
3804 for next_item in proposal:
3805 print(' ' + next_item[0])
3806 else:
3807 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3808 for next_item in proposal:
3809 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003810
kmarshall9249e012016-08-23 12:02:16 -07003811 # Quit now on precondition failure or if instructed by the user, either
3812 # via an interactive prompt or by command line flags.
3813 if options.dry_run:
3814 print('\nNo changes were made (dry run).\n')
3815 return 0
3816 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003817 print('You are currently on a branch \'%s\' which is associated with a '
3818 'closed codereview issue, so archive cannot proceed. Please '
3819 'checkout another branch and run this command again.' %
3820 current_branch)
3821 return 1
kmarshall9249e012016-08-23 12:02:16 -07003822 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003823 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3824 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003825 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003826 return 1
3827
3828 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003829 if not options.notags:
3830 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003831 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003832
vapiera7fbd5a2016-06-16 09:17:49 -07003833 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003834
3835 return 0
3836
3837
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003838@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003839def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003840 """Show status of changelists.
3841
3842 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003843 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07003844 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003845 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07003846 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00003847 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003848 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07003849 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003850
3851 Also see 'git cl comments'.
3852 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00003853 parser.add_option(
3854 '--no-branch-color',
3855 action='store_true',
3856 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003857 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003858 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003859 parser.add_option('-f', '--fast', action='store_true',
3860 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003861 parser.add_option(
3862 '-j', '--maxjobs', action='store', type=int,
3863 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003864
iannuccie53c9352016-08-17 14:40:40 -07003865 _add_codereview_issue_select_options(
3866 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003867 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003868 if args:
3869 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003870
iannuccie53c9352016-08-17 14:40:40 -07003871 if options.issue is not None and not options.field:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00003872 parser.error('--field must be specified with --issue.')
iannucci3c972b92016-08-17 13:24:10 -07003873
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003874 if options.field:
Edward Lemur934836a2019-09-09 20:16:54 +00003875 cl = Changelist(issue=options.issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003876 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003877 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003878 elif options.field == 'id':
3879 issueid = cl.GetIssue()
3880 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003881 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003882 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08003883 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003884 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003885 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003886 elif options.field == 'status':
3887 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003888 elif options.field == 'url':
3889 url = cl.GetIssueURL()
3890 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003891 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003892 return 0
3893
3894 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3895 if not branches:
3896 print('No local branch found.')
3897 return 0
3898
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003899 changes = [
Edward Lemur934836a2019-09-09 20:16:54 +00003900 Changelist(branchref=b)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003901 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003902 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003903 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003904 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003905 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003906
Daniel McArdlea23bf592019-02-12 00:25:12 +00003907 current_branch = GetCurrentBranch()
3908
3909 def FormatBranchName(branch, colorize=False):
3910 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
3911 an asterisk when it is the current branch."""
3912
3913 asterisk = ""
3914 color = Fore.RESET
3915 if branch == current_branch:
3916 asterisk = "* "
3917 color = Fore.GREEN
3918 branch_name = ShortBranchName(branch)
3919
3920 if colorize:
3921 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00003922 return asterisk + branch_name
3923
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003924 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00003925
3926 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003927 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3928 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003929 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003930 c, status = output.next()
3931 branch_statuses[c.GetBranch()] = status
3932 status = branch_statuses.pop(branch)
3933 url = cl.GetIssueURL()
3934 if url and (not status or status == 'error'):
3935 # The issue probably doesn't exist anymore.
3936 url += ' (broken)'
3937
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003938 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003939 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003940 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003941 color = ''
3942 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003943 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00003944
Alan Cuttera3be9a52019-03-04 18:50:33 +00003945 branch_display = FormatBranchName(branch)
3946 padding = ' ' * (alignment - len(branch_display))
3947 if not options.no_branch_color:
3948 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00003949
Alan Cuttera3be9a52019-03-04 18:50:33 +00003950 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
3951 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003952
vapiera7fbd5a2016-06-16 09:17:49 -07003953 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00003954 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003955 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00003956 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003957 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003958 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003959 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003960 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003961 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003962 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003963 print('Issue description:')
3964 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003965 return 0
3966
3967
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003968def colorize_CMDstatus_doc():
3969 """To be called once in main() to add colors to git cl status help."""
3970 colors = [i for i in dir(Fore) if i[0].isupper()]
3971
3972 def colorize_line(line):
3973 for color in colors:
3974 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003975 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003976 indent = len(line) - len(line.lstrip(' ')) + 1
3977 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3978 return line
3979
3980 lines = CMDstatus.__doc__.splitlines()
3981 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3982
3983
phajdan.jre328cf92016-08-22 04:12:17 -07003984def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07003985 if path == '-':
3986 json.dump(contents, sys.stdout)
3987 else:
3988 with open(path, 'w') as f:
3989 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07003990
3991
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003992@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003993@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003994def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003995 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003996
3997 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003998 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003999 parser.add_option('-r', '--reverse', action='store_true',
4000 help='Lookup the branch(es) for the specified issues. If '
4001 'no issues are specified, all branches with mapped '
4002 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004003 parser.add_option('--json',
4004 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004005 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004006 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004007
dnj@chromium.org406c4402015-03-03 17:22:28 +00004008 if options.reverse:
4009 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004010 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004011 # Reverse issue lookup.
4012 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004013
4014 git_config = {}
4015 for config in RunGit(['config', '--get-regexp',
4016 r'branch\..*issue']).splitlines():
4017 name, _space, val = config.partition(' ')
4018 git_config[name] = val
4019
dnj@chromium.org406c4402015-03-03 17:22:28 +00004020 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004021 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4022 config_key = _git_branch_config_key(ShortBranchName(branch),
4023 cls.IssueConfigKey())
4024 issue = git_config.get(config_key)
4025 if issue:
4026 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004027 if not args:
4028 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004029 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004030 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00004031 try:
4032 issue_num = int(issue)
4033 except ValueError:
4034 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004035 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00004036 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07004037 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00004038 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004039 if options.json:
4040 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004041 return 0
4042
4043 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00004044 issue = ParseIssueNumberArgument(args[0])
Aaron Gable78753da2017-06-15 10:35:49 -07004045 if not issue.valid:
4046 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4047 'or no argument to list it.\n'
4048 'Maybe you want to run git cl status?')
Edward Lemurf38bc172019-09-03 21:02:13 +00004049 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07004050 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004051 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00004052 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07004053 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4054 if options.json:
4055 write_json(options.json, {
4056 'issue': cl.GetIssue(),
4057 'issue_url': cl.GetIssueURL(),
4058 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004059 return 0
4060
4061
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004062@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004063def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004064 """Shows or posts review comments for any changelist."""
4065 parser.add_option('-a', '--add-comment', dest='comment',
4066 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004067 parser.add_option('-p', '--publish', action='store_true',
4068 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004069 parser.add_option('-i', '--issue', dest='issue',
Edward Lemurf38bc172019-09-03 21:02:13 +00004070 help='review issue id (defaults to current issue).')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004071 parser.add_option('-m', '--machine-readable', dest='readable',
4072 action='store_false', default=True,
4073 help='output comments in a format compatible with '
4074 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004075 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004076 help='File to write JSON summary to, or "-" for stdout')
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004077 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004078 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004079
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004080 issue = None
4081 if options.issue:
4082 try:
4083 issue = int(options.issue)
4084 except ValueError:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004085 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004086
Edward Lemur934836a2019-09-09 20:16:54 +00004087 cl = Changelist(issue=issue)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004088
4089 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004090 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004091 return 0
4092
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004093 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4094 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004095 for comment in summary:
4096 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004097 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004098 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004099 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004100 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004101 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00004102 elif comment.autogenerated:
4103 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004104 else:
4105 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004106 print('\n%s%s %s%s\n%s' % (
4107 color,
4108 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4109 comment.sender,
4110 Fore.RESET,
4111 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4112
smut@google.comc85ac942015-09-15 16:34:43 +00004113 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004114 def pre_serialize(c):
4115 dct = c.__dict__.copy()
4116 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4117 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004118 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004119 return 0
4120
4121
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004122@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004123@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004124def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004125 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004126 parser.add_option('-d', '--display', action='store_true',
4127 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004128 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004129 help='New description to set for this issue (- for stdin, '
4130 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004131 parser.add_option('-f', '--force', action='store_true',
4132 help='Delete any unpublished Gerrit edits for this issue '
4133 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004134
4135 _add_codereview_select_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004136 options, args = parser.parse_args(args)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004137
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004138 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004139 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00004140 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004141 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004142 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004143
Edward Lemur934836a2019-09-09 20:16:54 +00004144 kwargs = {}
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004145 if target_issue_arg:
4146 kwargs['issue'] = target_issue_arg.issue
4147 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07004148
4149 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004150 if not cl.GetIssue():
4151 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004152
Edward Lemur678a6842019-10-03 22:25:05 +00004153 if args and not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00004154 logging.info('canonical issue/change URL: %s\n', cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004155
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004156 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004157
smut@google.com34fb6b12015-07-13 20:03:26 +00004158 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004159 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004160 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004161
4162 if options.new_description:
4163 text = options.new_description
4164 if text == '-':
4165 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004166 elif text == '+':
4167 base_branch = cl.GetCommonAncestorWithUpstream()
4168 change = cl.GetChange(base_branch, None, local_description=True)
4169 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004170
4171 description.set_description(text)
4172 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00004173 description.prompt()
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004174 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004175 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004176 return 0
4177
4178
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004179@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004180def CMDlint(parser, args):
4181 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004182 parser.add_option('--filter', action='append', metavar='-x,+y',
4183 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004184 options, args = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004185
4186 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004187 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004188 try:
4189 import cpplint
4190 import cpplint_chromium
4191 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004192 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004193 return 1
4194
4195 # Change the current working directory before calling lint so that it
4196 # shows the correct base.
4197 previous_cwd = os.getcwd()
4198 os.chdir(settings.GetRoot())
4199 try:
Edward Lemur934836a2019-09-09 20:16:54 +00004200 cl = Changelist()
thestig@chromium.org44202a22014-03-11 19:22:18 +00004201 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4202 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004203 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004204 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004205 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004206
4207 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004208 command = args + files
4209 if options.filter:
4210 command = ['--filter=' + ','.join(options.filter)] + command
4211 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004212
4213 white_regex = re.compile(settings.GetLintRegex())
4214 black_regex = re.compile(settings.GetLintIgnoreRegex())
4215 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4216 for filename in filenames:
4217 if white_regex.match(filename):
4218 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004219 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004220 else:
4221 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4222 extra_check_functions)
4223 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004224 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004225 finally:
4226 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004227 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004228 if cpplint._cpplint_state.error_count != 0:
4229 return 1
4230 return 0
4231
4232
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004233@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004234def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004235 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004236 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004237 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004238 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004239 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004240 parser.add_option('--all', action='store_true',
4241 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004242 parser.add_option('--parallel', action='store_true',
4243 help='Run all tests specified by input_api.RunTests in all '
4244 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004245 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004246
sbc@chromium.org71437c02015-04-09 19:29:40 +00004247 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004248 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004249 return 1
4250
Edward Lemur934836a2019-09-09 20:16:54 +00004251 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004252 if args:
4253 base_branch = args[0]
4254 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004255 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004256 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004257
Aaron Gable8076c282017-11-29 14:39:41 -08004258 if options.all:
4259 base_change = cl.GetChange(base_branch, None)
4260 files = [('M', f) for f in base_change.AllFiles()]
4261 change = presubmit_support.GitChange(
4262 base_change.Name(),
4263 base_change.FullDescriptionText(),
4264 base_change.RepositoryRoot(),
4265 files,
4266 base_change.issue,
4267 base_change.patchset,
4268 base_change.author_email,
4269 base_change._upstream)
4270 else:
4271 change = cl.GetChange(base_branch, None)
4272
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004273 cl.RunHook(
4274 committing=not options.upload,
4275 may_prompt=False,
4276 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004277 change=change,
4278 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004279 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004280
4281
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004282def GenerateGerritChangeId(message):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004283 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004284
4285 Works the same way as
4286 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4287 but can be called on demand on all platforms.
4288
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004289 The basic idea is to generate git hash of a state of the tree, original
4290 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004291 """
4292 lines = []
4293 tree_hash = RunGitSilent(['write-tree'])
4294 lines.append('tree %s' % tree_hash.strip())
4295 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4296 if code == 0:
4297 lines.append('parent %s' % parent.strip())
4298 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4299 lines.append('author %s' % author.strip())
4300 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4301 lines.append('committer %s' % committer.strip())
4302 lines.append('')
4303 # Note: Gerrit's commit-hook actually cleans message of some lines and
4304 # whitespace. This code is not doing this, but it clearly won't decrease
4305 # entropy.
4306 lines.append(message)
4307 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00004308 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004309 return 'I%s' % change_hash.strip()
4310
4311
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004312def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004313 """Computes the remote branch ref to use for the CL.
4314
4315 Args:
4316 remote (str): The git remote for the CL.
4317 remote_branch (str): The git remote branch for the CL.
4318 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004319 """
4320 if not (remote and remote_branch):
4321 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004322
wittman@chromium.org455dc922015-01-26 20:15:50 +00004323 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004324 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004325 # refs, which are then translated into the remote full symbolic refs
4326 # below.
4327 if '/' not in target_branch:
4328 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4329 else:
4330 prefix_replacements = (
4331 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4332 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4333 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4334 )
4335 match = None
4336 for regex, replacement in prefix_replacements:
4337 match = re.search(regex, target_branch)
4338 if match:
4339 remote_branch = target_branch.replace(match.group(0), replacement)
4340 break
4341 if not match:
4342 # This is a branch path but not one we recognize; use as-is.
4343 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004344 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4345 # Handle the refs that need to land in different refs.
4346 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004347
wittman@chromium.org455dc922015-01-26 20:15:50 +00004348 # Create the true path to the remote branch.
4349 # Does the following translation:
4350 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4351 # * refs/remotes/origin/master -> refs/heads/master
4352 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4353 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4354 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4355 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4356 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4357 'refs/heads/')
4358 elif remote_branch.startswith('refs/remotes/branch-heads'):
4359 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004360
wittman@chromium.org455dc922015-01-26 20:15:50 +00004361 return remote_branch
4362
4363
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004364def cleanup_list(l):
4365 """Fixes a list so that comma separated items are put as individual items.
4366
4367 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4368 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4369 """
4370 items = sum((i.split(',') for i in l), [])
4371 stripped_items = (i.strip() for i in items)
4372 return sorted(filter(None, stripped_items))
4373
4374
Aaron Gable4db38df2017-11-03 14:59:07 -07004375@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004376@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004377def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004378 """Uploads the current changelist to codereview.
4379
4380 Can skip dependency patchset uploads for a branch by running:
4381 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004382 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00004383 git config --unset branch.branch_name.skip-deps-uploads
4384 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004385
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004386 If the name of the checked out branch starts with "bug-" or "fix-" followed
4387 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02004388 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004389
4390 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004391 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004392 [git-cl] add support for hashtags
4393 Foo bar: implement foo
4394 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004395 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004396 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4397 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004398 parser.add_option('--bypass-watchlists', action='store_true',
4399 dest='bypass_watchlists',
4400 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004401 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004402 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004403 parser.add_option('--message', '-m', dest='message',
4404 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004405 parser.add_option('-b', '--bug',
4406 help='pre-populate the bug number(s) for this issue. '
4407 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004408 parser.add_option('--message-file', dest='message_file',
4409 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004410 parser.add_option('--title', '-t', dest='title',
4411 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004412 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004413 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004414 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004415 parser.add_option('--tbrs',
4416 action='append', default=[],
4417 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004418 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004419 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004420 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004421 parser.add_option('--hashtag', dest='hashtags',
4422 action='append', default=[],
4423 help=('Gerrit hashtag for new CL; '
4424 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004425 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004426 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004427 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004428 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004429 metavar='TARGET',
4430 help='Apply CL to remote ref TARGET. ' +
4431 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004432 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004433 help='Squash multiple commits into one')
Mike Frysingera989d552019-08-14 20:51:23 +00004434 parser.add_option('--no-squash', action='store_false', dest='squash',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004435 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004436 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004437 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004438 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4439 const='TBR', help='add a set of OWNERS to TBR')
4440 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4441 const='R', help='add a set of OWNERS to R')
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004442 parser.add_option('-c', '--use-commit-queue', action='store_true',
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004443 default=False,
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004444 help='tell the CQ to commit this patchset; '
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004445 'implies --send-mail')
4446 parser.add_option('-d', '--cq-dry-run',
4447 action='store_true', default=False,
rmistry@google.comef966222015-04-07 11:15:01 +00004448 help='Send the patchset to do a CQ dry run right after '
4449 'upload.')
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00004450 parser.add_option('--preserve-tryjobs', action='store_true',
4451 help='instruct the CQ to let tryjobs running even after '
4452 'new patchsets are uploaded instead of canceling '
4453 'prior patchset\' tryjobs')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004454 parser.add_option('--dependencies', action='store_true',
4455 help='Uploads CLs of all the local branches that depend on '
4456 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004457 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4458 help='Sends your change to the CQ after an approval. Only '
4459 'works on repos that have the Auto-Submit label '
4460 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004461 parser.add_option('--parallel', action='store_true',
4462 help='Run all tests specified by input_api.RunTests in all '
4463 'PRESUBMIT files in parallel.')
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004464 parser.add_option('--no-autocc', action='store_true',
4465 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004466 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004467 help='Set the review private. This implies --no-autocc.')
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004468 parser.add_option('-R', '--retry-failed', action='store_true',
4469 help='Retry failed tryjobs from old patchset immediately '
4470 'after uploading new patchset. Cannot be used with '
4471 '--use-commit-queue or --cq-dry-run.')
4472 parser.add_option('--buildbucket-host', default='cr-buildbucket.appspot.com',
4473 help='Host of buildbucket. The default host is %default.')
4474 auth.add_auth_options(parser)
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004475
rmistry@google.com2dd99862015-06-22 12:22:18 +00004476 orig_args = args
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004477 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004478 (options, args) = parser.parse_args(args)
4479
sbc@chromium.org71437c02015-04-09 19:29:40 +00004480 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004481 return 1
4482
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004483 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004484 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004485 options.cc = cleanup_list(options.cc)
4486
tandriib80458a2016-06-23 12:20:07 -07004487 if options.message_file:
4488 if options.message:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004489 parser.error('Only one of --message and --message-file allowed.')
tandriib80458a2016-06-23 12:20:07 -07004490 options.message = gclient_utils.FileRead(options.message_file)
4491 options.message_file = None
4492
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004493 if ([options.cq_dry_run,
4494 options.use_commit_queue,
4495 options.retry_failed].count(True) > 1):
4496 parser.error('Only one of --use-commit-queue, --cq-dry-run, or '
4497 '--retry-failed is allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07004498
Aaron Gableedbc4132017-09-11 13:22:28 -07004499 if options.use_commit_queue:
4500 options.send_mail = True
4501
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004502 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4503 settings.GetIsGerrit()
4504
Edward Lemur934836a2019-09-09 20:16:54 +00004505 cl = Changelist()
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004506 if options.retry_failed and not cl.GetIssue():
4507 print('No previous patchsets, so --retry-failed has no effect.')
4508 options.retry_failed = False
4509 # cl.GetMostRecentPatchset uses cached information, and can return the last
4510 # patchset before upload. Calling it here makes it clear that it's the
4511 # last patchset before upload. Note that GetMostRecentPatchset will fail
4512 # if no CL has been uploaded yet.
4513 if options.retry_failed:
4514 patchset = cl.GetMostRecentPatchset()
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004515
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004516 ret = cl.CMDUpload(options, args, orig_args)
4517
4518 if options.retry_failed:
4519 if ret != 0:
4520 print('Upload failed, so --retry-failed has no effect.')
4521 return ret
4522 auth_config = auth.extract_auth_config_from_options(options)
4523 builds = fetch_try_jobs(
4524 auth_config, cl, options.buildbucket_host, patchset)
4525 buckets = _filter_failed(builds)
4526 if len(buckets) == 0:
4527 print('No failed tryjobs, so --retry-failed has no effect.')
4528 return ret
4529 _trigger_try_jobs(auth_config, cl, buckets, options, patchset + 1)
4530
4531 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00004532
4533
Francois Dorayd42c6812017-05-30 15:10:20 -04004534@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004535@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004536def CMDsplit(parser, args):
4537 """Splits a branch into smaller branches and uploads CLs.
4538
4539 Creates a branch and uploads a CL for each group of files modified in the
4540 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004541 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004542 the shared OWNERS file.
4543 """
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004544 parser.add_option('-d', '--description', dest='description_file',
4545 help='A text file containing a CL description in which '
4546 '$directory will be replaced by each CL\'s directory.')
4547 parser.add_option('-c', '--comment', dest='comment_file',
4548 help='A text file containing a CL comment.')
4549 parser.add_option('-n', '--dry-run', dest='dry_run', action='store_true',
Chris Watkinsba28e462017-12-13 11:22:17 +11004550 default=False,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004551 help='List the files and reviewers for each CL that would '
4552 'be created, but don\'t create branches or CLs.')
4553 parser.add_option('--cq-dry-run', action='store_true',
4554 help='If set, will do a cq dry run for each uploaded CL. '
4555 'Please be careful when doing this; more than ~10 CLs '
4556 'has the potential to overload our build '
4557 'infrastructure. Try to upload these not during high '
4558 'load times (usually 11-3 Mountain View time). Email '
4559 'infra-dev@chromium.org with any questions.')
Takuto Ikuta51eca592019-02-14 19:40:52 +00004560 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4561 default=True,
4562 help='Sends your change to the CQ after an approval. Only '
4563 'works on repos that have the Auto-Submit label '
4564 'enabled')
Francois Dorayd42c6812017-05-30 15:10:20 -04004565 options, _ = parser.parse_args(args)
4566
4567 if not options.description_file:
4568 parser.error('No --description flag specified.')
4569
4570 def WrappedCMDupload(args):
4571 return CMDupload(OptionParser(), args)
4572
4573 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004574 Changelist, WrappedCMDupload, options.dry_run,
Takuto Ikuta51eca592019-02-14 19:40:52 +00004575 options.cq_dry_run, options.enable_auto_submit)
Francois Dorayd42c6812017-05-30 15:10:20 -04004576
4577
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004578@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004579@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004580def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004581 """DEPRECATED: Used to commit the current changelist via git-svn."""
4582 message = ('git-cl no longer supports committing to SVN repositories via '
4583 'git-svn. You probably want to use `git cl land` instead.')
4584 print(message)
4585 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004586
4587
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004588@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004589@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004590def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004591 """Commits the current changelist via git.
4592
4593 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4594 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004595 """
4596 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4597 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004598 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004599 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004600 parser.add_option('--parallel', action='store_true',
4601 help='Run all tests specified by input_api.RunTests in all '
4602 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004603 (options, args) = parser.parse_args(args)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004604
Edward Lemur934836a2019-09-09 20:16:54 +00004605 cl = Changelist()
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004606
Robert Iannucci2e73d432018-03-14 01:10:47 -07004607 if not cl.GetIssue():
4608 DieWithError('You must upload the change first to Gerrit.\n'
4609 ' If you would rather have `git cl land` upload '
4610 'automatically for you, see http://crbug.com/642759')
Edward Lemur125d60a2019-09-13 18:25:41 +00004611 return cl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004612 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004613
4614
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004615@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004616@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004617def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004618 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004619 parser.add_option('-b', dest='newbranch',
4620 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004621 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004622 help='overwrite state on the current or chosen branch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004623 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
Edward Lemurf38bc172019-09-03 21:02:13 +00004624 help='don\'t commit after patch applies.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004625
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004626 group = optparse.OptionGroup(
4627 parser,
4628 'Options for continuing work on the current issue uploaded from a '
4629 'different clone (e.g. different machine). Must be used independently '
4630 'from the other options. No issue number should be specified, and the '
4631 'branch must have an issue number associated with it')
4632 group.add_option('--reapply', action='store_true', dest='reapply',
4633 help='Reset the branch and reapply the issue.\n'
4634 'CAUTION: This will undo any local changes in this '
4635 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004636
4637 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004638 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004639 parser.add_option_group(group)
4640
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004641 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004642 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004643
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004644 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004645 if options.newbranch:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004646 parser.error('--reapply works on the current branch only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004647 if len(args) > 0:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004648 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004649
Edward Lemur934836a2019-09-09 20:16:54 +00004650 cl = Changelist()
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004651 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004652 parser.error('Current branch must have an associated issue.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004653
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004654 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004655 if upstream is None:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004656 parser.error('No upstream branch specified. Cannot reset branch.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004657
4658 RunGit(['reset', '--hard', upstream])
4659 if options.pull:
4660 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004661
Edward Lemur678a6842019-10-03 22:25:05 +00004662 target_issue_arg = ParseIssueNumberArgument(cl.GetIssue())
4663 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit, False)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004664
4665 if len(args) != 1 or not args[0]:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004666 parser.error('Must specify issue number or URL.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004667
Edward Lemurf38bc172019-09-03 21:02:13 +00004668 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004669 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004670 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004671
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004672 # We don't want uncommitted changes mixed up with the patch.
4673 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004674 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004675
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004676 if options.newbranch:
4677 if options.force:
4678 RunGit(['branch', '-D', options.newbranch],
4679 stderr=subprocess2.PIPE, error_ok=True)
4680 RunGit(['new-branch', options.newbranch])
4681
Edward Lemur678a6842019-10-03 22:25:05 +00004682 cl = Changelist(
4683 codereview_host=target_issue_arg.hostname, issue=target_issue_arg.issue)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004684
Edward Lemur678a6842019-10-03 22:25:05 +00004685 if not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00004686 print('canonical issue/change URL: %s\n' % cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004687
Edward Lemurf38bc172019-09-03 21:02:13 +00004688 return cl.CMDPatchWithParsedIssue(
4689 target_issue_arg, options.nocommit, options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004690
4691
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004692def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004693 """Fetches the tree status and returns either 'open', 'closed',
4694 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004695 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004696 if url:
4697 status = urllib2.urlopen(url).read().lower()
4698 if status.find('closed') != -1 or status == '0':
4699 return 'closed'
4700 elif status.find('open') != -1 or status == '1':
4701 return 'open'
4702 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004703 return 'unset'
4704
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004705
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004706def GetTreeStatusReason():
4707 """Fetches the tree status from a json url and returns the message
4708 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004709 url = settings.GetTreeStatusUrl()
4710 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004711 connection = urllib2.urlopen(json_url)
4712 status = json.loads(connection.read())
4713 connection.close()
4714 return status['message']
4715
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004716
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004717@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004718def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004719 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004720 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004721 status = GetTreeStatus()
4722 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004723 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004724 return 2
4725
vapiera7fbd5a2016-06-16 09:17:49 -07004726 print('The tree is %s' % status)
4727 print()
4728 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004729 if status != 'open':
4730 return 1
4731 return 0
4732
4733
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004734@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00004735def CMDtry(parser, args):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004736 """Triggers tryjobs using either Buildbucket or CQ dry run."""
4737 group = optparse.OptionGroup(parser, 'Tryjob options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004738 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004739 '-b', '--bot', action='append',
4740 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4741 'times to specify multiple builders. ex: '
4742 '"-b win_rel -b win_layout". See '
4743 'the try server waterfall for the builders name and the tests '
4744 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004745 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004746 '-B', '--bucket', default='',
4747 help=('Buildbucket bucket to send the try requests.'))
4748 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004749 '-r', '--revision',
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004750 help='Revision to use for the tryjob; default: the revision will '
tandriif7b29d42016-10-07 08:45:41 -07004751 'be determined by the try recipe that builder runs, which usually '
4752 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004753 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004754 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004755 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004756 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004757 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004758 '--category', default='git_cl_try', help='Specify custom build category.')
4759 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004760 '--project',
4761 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004762 'in recipe to determine to which repository or directory to '
4763 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004764 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004765 '-p', '--property', dest='properties', action='append', default=[],
4766 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004767 'key2=value2 etc. The value will be treated as '
4768 'json if decodable, or as string otherwise. '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004769 'NOTE: using this may make your tryjob not usable for CQ, '
4770 'which will then schedule another tryjob with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004771 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004772 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4773 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004774 parser.add_option_group(group)
Quinten Yearsley983111f2019-09-26 17:18:48 +00004775 parser.add_option(
4776 '-R', '--retry-failed', action='store_true', default=False,
4777 help='Retry failed jobs from the latest set of tryjobs. '
4778 'Not allowed with --bucket and --bot options.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004779 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09004780 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004781 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004782 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004783
machenbach@chromium.org45453142015-09-15 08:45:22 +00004784 # Make sure that all properties are prop=value pairs.
4785 bad_params = [x for x in options.properties if '=' not in x]
4786 if bad_params:
4787 parser.error('Got properties with missing "=": %s' % bad_params)
4788
maruel@chromium.org15192402012-09-06 12:38:29 +00004789 if args:
4790 parser.error('Unknown arguments: %s' % args)
4791
Edward Lemur934836a2019-09-09 20:16:54 +00004792 cl = Changelist(issue=options.issue)
maruel@chromium.org15192402012-09-06 12:38:29 +00004793 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004794 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004795
Edward Lemurf38bc172019-09-03 21:02:13 +00004796 # HACK: warm up Gerrit change detail cache to save on RPCs.
Edward Lemur125d60a2019-09-13 18:25:41 +00004797 cl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004798
tandriie113dfd2016-10-11 10:20:12 -07004799 error_message = cl.CannotTriggerTryJobReason()
4800 if error_message:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004801 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004802
Quinten Yearsley983111f2019-09-26 17:18:48 +00004803 if options.retry_failed:
4804 if options.bot or options.bucket:
4805 print('ERROR: The option --retry-failed is not compatible with '
4806 '-B, -b, --bucket, or --bot.', file=sys.stderr)
4807 return 1
4808 print('Searching for failed tryjobs...')
4809 builds, patchset = _fetch_latest_builds(
4810 auth_config, cl, options.buildbucket_host)
4811 if options.verbose:
4812 print('Got %d builds in patchset #%d' % (len(builds), patchset))
4813 buckets = _filter_failed(builds)
4814 if not buckets:
4815 print('There are no failed jobs in the latest set of jobs '
4816 '(patchset #%d), doing nothing.' % patchset)
4817 return 0
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004818 num_builders = sum(map(len, buckets.itervalues()))
Quinten Yearsley983111f2019-09-26 17:18:48 +00004819 if num_builders > 10:
4820 confirm_or_exit('There are %d builders with failed builds.'
4821 % num_builders, action='continue')
4822 else:
4823 buckets = _get_bucket_map(cl, options, parser)
4824 if buckets and any(b.startswith('master.') for b in buckets):
4825 print('ERROR: Buildbot masters are not supported.')
4826 return 1
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004827
qyearsleydd49f942016-10-28 11:57:22 -07004828 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4829 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004830 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004831 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07004832 print('git cl try with no bots now defaults to CQ dry run.')
4833 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
4834 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00004835
borenet6c0efe62016-10-19 08:13:29 -07004836 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004837 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004838 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004839 'of bot requires an initial job from a parent (usually a builder). '
4840 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004841 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004842 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004843
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004844 patchset = cl.GetMostRecentPatchset()
Edward Lemur2c210a42019-09-16 23:58:35 +00004845 try:
4846 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
4847 except BuildbucketResponseException as ex:
4848 print('ERROR: %s' % ex)
4849 return 1
4850 return 0
maruel@chromium.org15192402012-09-06 12:38:29 +00004851
4852
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004853@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004854def CMDtry_results(parser, args):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00004855 """Prints info about results for tryjobs associated with the current CL."""
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004856 group = optparse.OptionGroup(parser, 'Tryjob results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004857 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004858 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004859 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004860 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004861 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004862 '--color', action='store_true', default=setup_color.IS_TTY,
4863 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004864 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004865 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4866 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004867 group.add_option(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004868 '--json', help=('Path of JSON output file to write tryjob results to,'
Stefan Zager1306bd02017-06-22 19:26:46 -07004869 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004870 parser.add_option_group(group)
4871 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07004872 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004873 options, args = parser.parse_args(args)
4874 if args:
4875 parser.error('Unrecognized args: %s' % ' '.join(args))
4876
4877 auth_config = auth.extract_auth_config_from_options(options)
Edward Lemur934836a2019-09-09 20:16:54 +00004878 cl = Changelist(issue=options.issue)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004879 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004880 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004881
tandrii221ab252016-10-06 08:12:04 -07004882 patchset = options.patchset
4883 if not patchset:
4884 patchset = cl.GetMostRecentPatchset()
4885 if not patchset:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004886 parser.error('Code review host doesn\'t know about issue %s. '
tandrii221ab252016-10-06 08:12:04 -07004887 'No access to issue or wrong issue number?\n'
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004888 'Either upload first, or pass --patchset explicitly.' %
tandrii221ab252016-10-06 08:12:04 -07004889 cl.GetIssue())
4890
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004891 try:
tandrii221ab252016-10-06 08:12:04 -07004892 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004893 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004894 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004895 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004896 if options.json:
4897 write_try_results_json(options.json, jobs)
4898 else:
4899 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004900 return 0
4901
4902
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004903@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004904@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004905def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004906 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004907 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004908 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004909 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004910
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004911 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004912 if args:
4913 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004914 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004915 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004916 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004917 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004918
4919 # Clear configured merge-base, if there is one.
4920 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004921 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004922 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004923 return 0
4924
4925
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004926@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00004927def CMDweb(parser, args):
4928 """Opens the current CL in the web browser."""
4929 _, args = parser.parse_args(args)
4930 if args:
4931 parser.error('Unrecognized args: %s' % ' '.join(args))
4932
4933 issue_url = Changelist().GetIssueURL()
4934 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004935 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004936 return 1
4937
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004938 # Redirect I/O before invoking browser to hide its output. For example, this
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004939 # allows us to hide the "Created new window in existing browser session."
4940 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004941 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004942 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004943 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004944 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004945 os.open(os.devnull, os.O_RDWR)
4946 try:
4947 webbrowser.open(issue_url)
4948 finally:
4949 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004950 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004951 return 0
4952
4953
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004954@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004955def CMDset_commit(parser, args):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004956 """Sets the commit bit to trigger the CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004957 parser.add_option('-d', '--dry-run', action='store_true',
4958 help='trigger in dry run mode')
4959 parser.add_option('-c', '--clear', action='store_true',
4960 help='stop CQ run, if any')
iannuccie53c9352016-08-17 14:40:40 -07004961 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004962 options, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004963 if args:
4964 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004965 if options.dry_run and options.clear:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004966 parser.error('Only one of --dry-run and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004967
Edward Lemur934836a2019-09-09 20:16:54 +00004968 cl = Changelist(issue=options.issue)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004969 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004970 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004971 elif options.dry_run:
4972 state = _CQState.DRY_RUN
4973 else:
4974 state = _CQState.COMMIT
4975 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004976 parser.error('Must upload the issue first.')
tandrii9de9ec62016-07-13 03:01:59 -07004977 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004978 return 0
4979
4980
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004981@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00004982def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004983 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004984 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004985 options, args = parser.parse_args(args)
groby@chromium.org411034a2013-02-26 15:12:01 +00004986 if args:
4987 parser.error('Unrecognized args: %s' % ' '.join(args))
Edward Lemur934836a2019-09-09 20:16:54 +00004988 cl = Changelist(issue=options.issue)
groby@chromium.org411034a2013-02-26 15:12:01 +00004989 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07004990 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004991 DieWithError('ERROR: No issue to close.')
groby@chromium.org411034a2013-02-26 15:12:01 +00004992 cl.CloseIssue()
4993 return 0
4994
4995
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004996@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004997def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004998 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004999 parser.add_option(
5000 '--stat',
5001 action='store_true',
5002 dest='stat',
5003 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005004 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005005 if args:
5006 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005007
Edward Lemur934836a2019-09-09 20:16:54 +00005008 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005009 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005010 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005011 if not issue:
5012 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005013
Aaron Gablea718c3e2017-08-28 17:47:28 -07005014 base = cl._GitGetBranchConfigValue('last-upload-hash')
5015 if not base:
5016 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5017 if not base:
5018 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5019 revision_info = detail['revisions'][detail['current_revision']]
5020 fetch_info = revision_info['fetch']['http']
5021 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5022 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005023
Aaron Gablea718c3e2017-08-28 17:47:28 -07005024 cmd = ['git', 'diff']
5025 if options.stat:
5026 cmd.append('--stat')
5027 cmd.append(base)
5028 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005029
5030 return 0
5031
5032
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005033@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005034def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005035 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005036 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005037 '--ignore-current',
5038 action='store_true',
5039 help='Ignore the CL\'s current reviewers and start from scratch.')
5040 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005041 '--ignore-self',
5042 action='store_true',
5043 help='Do not consider CL\'s author as an owners.')
5044 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005045 '--no-color',
5046 action='store_true',
5047 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005048 parser.add_option(
5049 '--batch',
5050 action='store_true',
5051 help='Do not run interactively, just suggest some')
Yang Guo6e269a02019-06-26 11:17:02 +00005052 # TODO: Consider moving this to another command, since other
5053 # git-cl owners commands deal with owners for a given CL.
5054 parser.add_option(
5055 '--show-all',
5056 action='store_true',
5057 help='Show all owners for a particular file')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005058 options, args = parser.parse_args(args)
5059
5060 author = RunGit(['config', 'user.email']).strip() or None
5061
Edward Lemur934836a2019-09-09 20:16:54 +00005062 cl = Changelist()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005063
Yang Guo6e269a02019-06-26 11:17:02 +00005064 if options.show_all:
5065 for arg in args:
5066 base_branch = cl.GetCommonAncestorWithUpstream()
5067 change = cl.GetChange(base_branch, None)
5068 database = owners.Database(change.RepositoryRoot(), file, os.path)
5069 database.load_data_needed_for([arg])
5070 print('Owners for %s:' % arg)
5071 for owner in sorted(database.all_possible_owners([arg], None)):
5072 print(' - %s' % owner)
5073 return 0
5074
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005075 if args:
5076 if len(args) > 1:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005077 parser.error('Unknown args.')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005078 base_branch = args[0]
5079 else:
5080 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005081 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005082
5083 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005084 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5085
5086 if options.batch:
5087 db = owners.Database(change.RepositoryRoot(), file, os.path)
5088 print('\n'.join(db.reviewers_for(affected_files, author)))
5089 return 0
5090
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005091 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005092 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005093 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005094 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005095 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005096 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005097 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005098 override_files=change.OriginalOwnersFiles(),
5099 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005100
5101
Aiden Bennerc08566e2018-10-03 17:52:42 +00005102def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005103 """Generates a diff command."""
5104 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005105 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5106
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005107 if allow_prefix:
5108 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5109 # case that diff.noprefix is set in the user's git config.
5110 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5111 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005112 diff_cmd += ['--no-prefix']
5113
5114 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005115
5116 if args:
5117 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005118 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005119 diff_cmd.append(arg)
5120 else:
5121 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005122
5123 return diff_cmd
5124
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005125
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005126def MatchingFileType(file_name, extensions):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005127 """Returns True if the file name ends with one of the given extensions."""
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005128 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005129
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005130
enne@chromium.org555cfe42014-01-29 18:21:39 +00005131@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005132@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005133def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005134 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005135 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005136 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005137 parser.add_option('--full', action='store_true',
5138 help='Reformat the full content of all touched files')
5139 parser.add_option('--dry-run', action='store_true',
5140 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005141 parser.add_option(
5142 '--python',
5143 action='store_true',
5144 default=None,
5145 help='Enables python formatting on all python files.')
5146 parser.add_option(
5147 '--no-python',
5148 action='store_true',
5149 dest='python',
5150 help='Disables python formatting on all python files. '
5151 'Takes precedence over --python. '
5152 'If neither --python or --no-python are set, python '
5153 'files that have a .style.yapf file in an ancestor '
5154 'directory will be formatted.')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005155 parser.add_option('--js', action='store_true',
5156 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005157 parser.add_option('--diff', action='store_true',
5158 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005159 parser.add_option('--presubmit', action='store_true',
5160 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005161 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005162
Daniel Chengc55eecf2016-12-30 03:11:02 -08005163 # Normalize any remaining args against the current path, so paths relative to
5164 # the current directory are still resolved as expected.
5165 args = [os.path.join(os.getcwd(), arg) for arg in args]
5166
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005167 # git diff generates paths against the root of the repository. Change
5168 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005169 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005170 if rel_base_path:
5171 os.chdir(rel_base_path)
5172
digit@chromium.org29e47272013-05-17 17:01:46 +00005173 # Grab the merge-base commit, i.e. the upstream commit of the current
5174 # branch when it was created or the last time it was rebased. This is
5175 # to cover the case where the user may have called "git fetch origin",
5176 # moving the origin branch to a newer commit, but hasn't rebased yet.
5177 upstream_commit = None
5178 cl = Changelist()
5179 upstream_branch = cl.GetUpstreamBranch()
5180 if upstream_branch:
5181 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5182 upstream_commit = upstream_commit.strip()
5183
5184 if not upstream_commit:
5185 DieWithError('Could not find base commit for this branch. '
5186 'Are you in detached state?')
5187
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005188 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5189 diff_output = RunGit(changed_files_cmd)
5190 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005191 # Filter out files deleted by this CL
5192 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005193
Christopher Lamc5ba6922017-01-24 11:19:14 +11005194 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005195 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005196
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005197 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5198 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5199 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005200 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005201
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005202 top_dir = os.path.normpath(
5203 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5204
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005205 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5206 # formatted. This is used to block during the presubmit.
5207 return_value = 0
5208
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005209 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005210 # Locate the clang-format binary in the checkout
5211 try:
5212 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005213 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005214 DieWithError(e)
5215
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005216 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005217 cmd = [clang_format_tool]
5218 if not opts.dry_run and not opts.diff:
5219 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005220 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005221 if opts.diff:
5222 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005223 else:
5224 env = os.environ.copy()
5225 env['PATH'] = str(os.path.dirname(clang_format_tool))
5226 try:
5227 script = clang_format.FindClangFormatScriptInChromiumTree(
5228 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005229 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005230 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005231
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005232 cmd = [sys.executable, script, '-p0']
5233 if not opts.dry_run and not opts.diff:
5234 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005235
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005236 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5237 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005238
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005239 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5240 if opts.diff:
5241 sys.stdout.write(stdout)
5242 if opts.dry_run and len(stdout) > 0:
5243 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005244
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005245 # Similar code to above, but using yapf on .py files rather than clang-format
5246 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005247 py_explicitly_disabled = opts.python is not None and not opts.python
5248 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005249 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5250 yapf_tool = os.path.join(depot_tools_path, 'yapf')
5251 if sys.platform.startswith('win'):
5252 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005253
Aiden Bennerc08566e2018-10-03 17:52:42 +00005254 # If we couldn't find a yapf file we'll default to the chromium style
5255 # specified in depot_tools.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005256 chromium_default_yapf_style = os.path.join(depot_tools_path,
5257 YAPF_CONFIG_FILENAME)
Aiden Bennerc08566e2018-10-03 17:52:42 +00005258 # Used for caching.
5259 yapf_configs = {}
5260 for f in python_diff_files:
5261 # Find the yapf style config for the current file, defaults to depot
5262 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005263 _FindYapfConfigFile(f, yapf_configs, top_dir)
5264
5265 # Turn on python formatting by default if a yapf config is specified.
5266 # This breaks in the case of this repo though since the specified
5267 # style file is also the global default.
5268 if opts.python is None:
5269 filtered_py_files = []
5270 for f in python_diff_files:
5271 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5272 filtered_py_files.append(f)
5273 else:
5274 filtered_py_files = python_diff_files
5275
5276 # Note: yapf still seems to fix indentation of the entire file
5277 # even if line ranges are specified.
5278 # See https://github.com/google/yapf/issues/499
5279 if not opts.full and filtered_py_files:
5280 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5281
5282 for f in filtered_py_files:
5283 yapf_config = _FindYapfConfigFile(f, yapf_configs, top_dir)
5284 if yapf_config is None:
5285 yapf_config = chromium_default_yapf_style
Aiden Bennerc08566e2018-10-03 17:52:42 +00005286
5287 cmd = [yapf_tool, '--style', yapf_config, f]
5288
5289 has_formattable_lines = False
5290 if not opts.full:
5291 # Only run yapf over changed line ranges.
5292 for diff_start, diff_len in py_line_diffs[f]:
5293 diff_end = diff_start + diff_len - 1
5294 # Yapf errors out if diff_end < diff_start but this
5295 # is a valid line range diff for a removal.
5296 if diff_end >= diff_start:
5297 has_formattable_lines = True
5298 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5299 # If all line diffs were removals we have nothing to format.
5300 if not has_formattable_lines:
5301 continue
5302
5303 if opts.diff or opts.dry_run:
5304 cmd += ['--diff']
5305 # Will return non-zero exit code if non-empty diff.
5306 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5307 if opts.diff:
5308 sys.stdout.write(stdout)
5309 elif len(stdout) > 0:
5310 return_value = 2
5311 else:
5312 cmd += ['-i']
5313 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005314
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005315 # Dart's formatter does not have the nice property of only operating on
5316 # modified chunks, so hard code full.
5317 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005318 try:
5319 command = [dart_format.FindDartFmtToolInChromiumTree()]
5320 if not opts.dry_run and not opts.diff:
5321 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005322 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005323
ppi@chromium.org6593d932016-03-03 15:41:15 +00005324 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005325 if opts.dry_run and stdout:
5326 return_value = 2
5327 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005328 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5329 'found in this checkout. Files in other languages are still '
5330 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005331
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005332 # Format GN build files. Always run on full build files for canonical form.
5333 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005334 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005335 if opts.dry_run or opts.diff:
5336 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005337 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005338 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5339 shell=sys.platform == 'win32',
5340 cwd=top_dir)
5341 if opts.dry_run and gn_ret == 2:
5342 return_value = 2 # Not formatted.
5343 elif opts.diff and gn_ret == 2:
5344 # TODO this should compute and print the actual diff.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005345 print('This change has GN build file diff for ' + gn_diff_file)
brettw4b8ed592016-08-05 16:19:12 -07005346 elif gn_ret != 0:
5347 # For non-dry run cases (and non-2 return values for dry-run), a
5348 # nonzero error code indicates a failure, probably because the file
5349 # doesn't parse.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005350 DieWithError('gn format failed on ' + gn_diff_file +
5351 '\nTry running `gn format` on this file manually.')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005352
Ilya Shermane081cbe2017-08-15 17:51:04 -07005353 # Skip the metrics formatting from the global presubmit hook. These files have
5354 # a separate presubmit hook that issues an error if the files need formatting,
5355 # whereas the top-level presubmit script merely issues a warning. Formatting
5356 # these files is somewhat slow, so it's important not to duplicate the work.
5357 if not opts.presubmit:
5358 for xml_dir in GetDirtyMetricsDirs(diff_files):
5359 tool_dir = os.path.join(top_dir, xml_dir)
5360 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5361 if opts.dry_run or opts.diff:
5362 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005363 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005364 if opts.diff:
5365 sys.stdout.write(stdout)
5366 if opts.dry_run and stdout:
5367 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005368
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005369 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005370
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005371
Steven Holte2e664bf2017-04-21 13:10:47 -07005372def GetDirtyMetricsDirs(diff_files):
5373 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5374 metrics_xml_dirs = [
5375 os.path.join('tools', 'metrics', 'actions'),
5376 os.path.join('tools', 'metrics', 'histograms'),
5377 os.path.join('tools', 'metrics', 'rappor'),
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005378 os.path.join('tools', 'metrics', 'ukm'),
5379 ]
Steven Holte2e664bf2017-04-21 13:10:47 -07005380 for xml_dir in metrics_xml_dirs:
5381 if any(file.startswith(xml_dir) for file in xml_diff_files):
5382 yield xml_dir
5383
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005384
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005385@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005386@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005387def CMDcheckout(parser, args):
Edward Lemurf38bc172019-09-03 21:02:13 +00005388 """Checks out a branch associated with a given Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005389 _, args = parser.parse_args(args)
5390
5391 if len(args) != 1:
5392 parser.print_help()
5393 return 1
5394
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005395 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005396 if not issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005397 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005398
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005399 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005400
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005401 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005402 output = RunGit(['config', '--local', '--get-regexp',
5403 r'branch\..*\.%s' % issueprefix],
5404 error_ok=True)
5405 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005406 if issue == target_issue:
5407 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005408
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005409 branches = []
5410 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005411 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005412 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005413 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005414 return 1
5415 if len(branches) == 1:
5416 RunGit(['checkout', branches[0]])
5417 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005418 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005419 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005420 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005421 which = raw_input('Choose by index: ')
5422 try:
5423 RunGit(['checkout', branches[int(which)]])
5424 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005425 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005426 return 1
5427
5428 return 0
5429
5430
maruel@chromium.org29404b52014-09-08 22:58:00 +00005431def CMDlol(parser, args):
5432 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005433 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005434 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5435 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5436 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005437 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005438 return 0
5439
5440
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005441class OptionParser(optparse.OptionParser):
5442 """Creates the option parse and add --verbose support."""
5443 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005444 optparse.OptionParser.__init__(
5445 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005446 self.add_option(
5447 '-v', '--verbose', action='count', default=0,
5448 help='Use 2 times for more debugging info')
5449
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005450 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005451 try:
5452 return self._parse_args(args)
5453 finally:
5454 # Regardless of success or failure of args parsing, we want to report
5455 # metrics, but only after logging has been initialized (if parsing
5456 # succeeded).
5457 global settings
5458 settings = Settings()
5459
5460 if not metrics.DISABLE_METRICS_COLLECTION:
5461 # GetViewVCUrl ultimately calls logging method.
5462 project_url = settings.GetViewVCUrl().strip('/+')
5463 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5464 metrics.collector.add('project_urls', [project_url])
5465
5466 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005467 # Create an optparse.Values object that will store only the actual passed
5468 # options, without the defaults.
5469 actual_options = optparse.Values()
5470 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5471 # Create an optparse.Values object with the default options.
5472 options = optparse.Values(self.get_default_values().__dict__)
5473 # Update it with the options passed by the user.
5474 options._update_careful(actual_options.__dict__)
5475 # Store the options passed by the user in an _actual_options attribute.
5476 # We store only the keys, and not the values, since the values can contain
5477 # arbitrary information, which might be PII.
5478 metrics.collector.add('arguments', actual_options.__dict__.keys())
5479
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005480 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005481 logging.basicConfig(
5482 level=levels[min(options.verbose, len(levels) - 1)],
5483 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5484 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005485
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005486 return options, args
5487
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005488
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005489def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005490 if sys.hexversion < 0x02060000:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005491 print('\nYour Python version %s is unsupported, please upgrade.\n' %
vapiera7fbd5a2016-06-16 09:17:49 -07005492 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005493 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005494
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005495 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005496 dispatcher = subcommand.CommandDispatcher(__name__)
5497 try:
5498 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005499 except auth.AuthenticationError as e:
5500 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005501 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005502 if e.code != 500:
5503 raise
5504 DieWithError(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005505 ('App Engine is misbehaving and returned HTTP %d, again. Keep faith '
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005506 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005507 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005508
5509
5510if __name__ == '__main__':
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005511 # These affect sys.stdout, so do it outside of main() to simplify mocks in
5512 # the unit tests.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005513 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005514 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005515 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005516 sys.exit(main(sys.argv[1:]))