blob: dae123dec7df67922a2dc9f263eb5e1434551c99 [file] [log] [blame]
Josip Sokcevic4de5dea2022-03-23 21:15:14 +00001#!/usr/bin/env vpython3
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
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010014import datetime
Brian Sheedyb4307d52019-12-02 19:18:17 +000015import fnmatch
Edward Lemur202c5592019-10-21 22:44:52 +000016import httplib2
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010017import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000018import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000020import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import optparse
22import os
23import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010024import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000025import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000026import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070027import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000029import time
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000030import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000031import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000032import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000034from third_party import colorama
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000035import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000036import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000037import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000038import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000039import gerrit_util
iannucci@chromium.org9e849272014-04-04 00:31:55 +000040import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000041import git_footers
Edward Lemur85153282020-02-14 22:06:29 +000042import git_new_branch
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000043import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000044import metrics_utils
Edward Lesmeseeca9c62020-11-20 00:00:17 +000045import owners_client
iannucci@chromium.org9e849272014-04-04 00:31:55 +000046import owners_finder
Lei Zhangb8c62cf2020-07-15 20:09:37 +000047import presubmit_canned_checks
Josip Sokcevic7958e302023-03-01 23:02:21 +000048import presubmit_support
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +000049import rustfmt
Josip Sokcevic7958e302023-03-01 23:02:21 +000050import scm
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000051import setup_color
Francois Dorayd42c6812017-05-30 15:10:20 -040052import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000053import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000054import subprocess2
Olivier Robin0a6b5442022-04-07 07:25:04 +000055import swift_format
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import watchlists
57
Josip Sokcevic7958e302023-03-01 23:02:21 +000058from lib import utils
Edward Lemur79d4f992019-11-11 23:49:02 +000059from third_party import six
60from six.moves import urllib
61
62
63if sys.version_info.major == 3:
64 basestring = (str,) # pylint: disable=redefined-builtin
65
Edward Lemurb9830242019-10-30 22:19:20 +000066
tandrii7400cf02016-06-21 08:48:07 -070067__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000068
Edward Lemur0f58ae42019-04-30 17:24:12 +000069# Traces for git push will be stored in a traces directory inside the
70# depot_tools checkout.
71DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
72TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
Edward Lemur227d5102020-02-25 23:45:35 +000073PRESUBMIT_SUPPORT = os.path.join(DEPOT_TOOLS, 'presubmit_support.py')
Edward Lemur0f58ae42019-04-30 17:24:12 +000074
75# When collecting traces, Git hashes will be reduced to 6 characters to reduce
76# the size after compression.
77GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
78# Used to redact the cookies from the gitcookies file.
79GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
80
Edward Lemurd4d1ba42019-09-20 21:46:37 +000081MAX_ATTEMPTS = 3
82
Edward Lemur1b52d872019-05-09 21:12:12 +000083# The maximum number of traces we will keep. Multiplied by 3 since we store
84# 3 files per trace.
85MAX_TRACES = 3 * 10
Edward Lemur5737f022019-05-17 01:24:00 +000086# Message to be displayed to the user to inform where to find the traces for a
87# git-cl upload execution.
Edward Lemur0f58ae42019-04-30 17:24:12 +000088TRACES_MESSAGE = (
Edward Lemur1b52d872019-05-09 21:12:12 +000089'\n'
Edward Lemur5737f022019-05-17 01:24:00 +000090'The traces of this git-cl execution have been recorded at:\n'
Edward Lemur1b52d872019-05-09 21:12:12 +000091' %(trace_name)s-traces.zip\n'
Edward Lemur5737f022019-05-17 01:24:00 +000092'Copies of your gitcookies file and git config have been recorded at:\n'
93' %(trace_name)s-git-info.zip\n')
Edward Lemur1b52d872019-05-09 21:12:12 +000094# Format of the message to be stored as part of the traces to give developers a
95# better context when they go through traces.
96TRACES_README_FORMAT = (
97'Date: %(now)s\n'
98'\n'
99'Change: https://%(gerrit_host)s/q/%(change_id)s\n'
100'Title: %(title)s\n'
101'\n'
102'%(description)s\n'
103'\n'
104'Execution time: %(execution_time)s\n'
105'Exit code: %(exit_code)s\n') + TRACES_MESSAGE
Edward Lemur0f58ae42019-04-30 17:24:12 +0000106
Aaron Gable1bc7bfe2016-12-19 10:08:14 -0800107POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
Henrique Ferreiroff249622019-11-28 23:19:29 +0000108DESCRIPTION_BACKUP_FILE = '.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +0000109REFS_THAT_ALIAS_TO_OTHER_REFS = {
Josip Sokcevic7e133ff2021-07-13 17:44:53 +0000110 'refs/remotes/origin/lkgr': 'refs/remotes/origin/main',
111 'refs/remotes/origin/lkcr': 'refs/remotes/origin/main',
rmistry@google.comc68112d2015-03-03 12:48:06 +0000112}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
Josip Sokcevicc39ab992020-09-24 20:09:15 +0000114DEFAULT_OLD_BRANCH = 'refs/remotes/origin/master'
115DEFAULT_NEW_BRANCH = 'refs/remotes/origin/main'
116
Joanna Wanga8db0cb2023-01-24 15:43:17 +0000117DEFAULT_BUILDBUCKET_HOST = 'cr-buildbucket.appspot.com'
118
thestig@chromium.org44202a22014-03-11 19:22:18 +0000119# Valid extensions for files we want to lint.
120DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
121DEFAULT_LINT_IGNORE_REGEX = r"$^"
122
Aiden Bennerc08566e2018-10-03 17:52:42 +0000123# File name for yapf style config files.
124YAPF_CONFIG_FILENAME = '.style.yapf'
125
Edward Lesmes50da7702020-03-30 19:23:43 +0000126# The issue, patchset and codereview server are stored on git config for each
127# branch under branch.<branch-name>.<config-key>.
128ISSUE_CONFIG_KEY = 'gerritissue'
129PATCHSET_CONFIG_KEY = 'gerritpatchset'
130CODEREVIEW_SERVER_CONFIG_KEY = 'gerritserver'
Gavin Makbe2e9262022-11-08 23:41:55 +0000131# When using squash workflow, _CMDUploadChange doesn't simply push the commit(s)
132# you make to Gerrit. Instead, it creates a new commit object that contains all
133# changes you've made, diffed against a parent/merge base.
134# This is the hash of the new squashed commit and you can find this on Gerrit.
135GERRIT_SQUASH_HASH_CONFIG_KEY = 'gerritsquashhash'
136# This is the latest uploaded local commit hash.
137LAST_UPLOAD_HASH_CONFIG_KEY = 'last-upload-hash'
Edward Lesmes50da7702020-03-30 19:23:43 +0000138
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000139# Shortcut since it quickly becomes repetitive.
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000140Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000141
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000142# Initialized in main()
143settings = None
144
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100145# Used by tests/git_cl_test.py to add extra logging.
146# Inside the weirdly failing test, add this:
147# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700148# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100149_IS_BEING_TESTED = False
150
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000151_GOOGLESOURCE = 'googlesource.com'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000152
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000153_KNOWN_GERRIT_TO_SHORT_URLS = {
154 'https://chrome-internal-review.googlesource.com': 'https://crrev.com/i',
155 'https://chromium-review.googlesource.com': 'https://crrev.com/c',
156}
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000157assert len(_KNOWN_GERRIT_TO_SHORT_URLS) == len(
158 set(_KNOWN_GERRIT_TO_SHORT_URLS.values())), 'must have unique values'
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000159
160
Joanna Wang18de1f62023-01-21 01:24:24 +0000161# Maximum number of branches in a stack that can be traversed and uploaded
162# at once. Picked arbitrarily.
163_MAX_STACKED_BRANCHES_UPLOAD = 20
164
165
Joanna Wang5051ffe2023-03-01 22:24:07 +0000166# Repo prefixes that are enrolled in the stacked changes dogfood.
167DOGFOOD_STACKED_CHANGES_REPOS = [
168 'chromium.googlesource.com/infra/',
169 'chrome-internal.googlesource.com/infra/'
170]
171
172
Josip Sokcevicf736cab2020-10-20 23:41:38 +0000173class GitPushError(Exception):
174 pass
175
176
Christopher Lamf732cd52017-01-24 12:40:11 +1100177def DieWithError(message, change_desc=None):
178 if change_desc:
179 SaveDescriptionBackup(change_desc)
Josip Sokcevic953278a2020-02-28 19:46:36 +0000180 print('\n ** Content of CL description **\n' +
181 '='*72 + '\n' +
182 change_desc.description + '\n' +
183 '='*72 + '\n')
Christopher Lamf732cd52017-01-24 12:40:11 +1100184
vapiera7fbd5a2016-06-16 09:17:49 -0700185 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000186 sys.exit(1)
187
188
Christopher Lamf732cd52017-01-24 12:40:11 +1100189def SaveDescriptionBackup(change_desc):
Henrique Ferreiro5ae48172019-11-29 16:14:42 +0000190 backup_path = os.path.join(DEPOT_TOOLS, DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000191 print('\nsaving CL description to %s\n' % backup_path)
sokcevic07152802021-08-18 00:06:34 +0000192 with open(backup_path, 'wb') as backup_file:
193 backup_file.write(change_desc.description.encode('utf-8'))
Christopher Lamf732cd52017-01-24 12:40:11 +1100194
195
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000196def GetNoGitPagerEnv():
197 env = os.environ.copy()
198 # 'cat' is a magical git string that disables pagers on all platforms.
199 env['GIT_PAGER'] = 'cat'
200 return env
201
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000202
bsep@chromium.org627d9002016-04-29 00:00:52 +0000203def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000204 try:
Edward Lemur79d4f992019-11-11 23:49:02 +0000205 stdout = subprocess2.check_output(args, shell=shell, **kwargs)
206 return stdout.decode('utf-8', 'replace')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000207 except subprocess2.CalledProcessError as e:
208 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000209 if not error_ok:
Alan Cutter594fd332020-07-21 23:55:27 +0000210 message = error_message or e.stdout.decode('utf-8', 'replace') or ''
211 DieWithError('Command "%s" failed.\n%s' % (' '.join(args), message))
Josip Sokcevic673e8ed2021-10-27 23:46:18 +0000212 out = e.stdout.decode('utf-8', 'replace')
213 if e.stderr:
214 out += e.stderr.decode('utf-8', 'replace')
215 return out
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000216
217
218def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000219 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000220 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000221
222
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000223def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000224 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700225 if suppress_stderr:
Edward Lesmescf06cad2020-12-14 22:03:23 +0000226 stderr = subprocess2.DEVNULL
tandrii5d48c322016-08-18 16:19:37 -0700227 else:
228 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000229 try:
tandrii5d48c322016-08-18 16:19:37 -0700230 (out, _), code = subprocess2.communicate(['git'] + args,
231 env=GetNoGitPagerEnv(),
232 stdout=subprocess2.PIPE,
233 stderr=stderr)
Edward Lemur79d4f992019-11-11 23:49:02 +0000234 return code, out.decode('utf-8', 'replace')
tandrii5d48c322016-08-18 16:19:37 -0700235 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900236 logging.debug('Failed running %s', ['git'] + args)
Edward Lemur79d4f992019-11-11 23:49:02 +0000237 return e.returncode, e.stdout.decode('utf-8', 'replace')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000238
239
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000240def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000241 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000242 return RunGitWithCode(args, suppress_stderr=True)[1]
243
244
tandrii2a16b952016-10-19 07:09:44 -0700245def time_sleep(seconds):
246 # Use this so that it can be mocked in tests without interfering with python
247 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700248 return time.sleep(seconds)
249
250
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000251def time_time():
252 # Use this so that it can be mocked in tests without interfering with python
253 # system machinery.
254 return time.time()
255
256
Edward Lemur1b52d872019-05-09 21:12:12 +0000257def datetime_now():
258 # Use this so that it can be mocked in tests without interfering with python
259 # system machinery.
260 return datetime.datetime.now()
261
262
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100263def confirm_or_exit(prefix='', action='confirm'):
264 """Asks user to press enter to continue or press Ctrl+C to abort."""
265 if not prefix or prefix.endswith('\n'):
266 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100267 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100268 mid = ' Press'
269 elif prefix.endswith(' '):
270 mid = 'press'
271 else:
272 mid = ' press'
Edward Lesmesae3586b2020-03-23 21:21:14 +0000273 gclient_utils.AskForData(
274 '%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100275
276
277def ask_for_explicit_yes(prompt):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000278 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
Edward Lesmesae3586b2020-03-23 21:21:14 +0000279 result = gclient_utils.AskForData(prompt + ' [Yes/No]: ').lower()
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100280 while True:
281 if 'yes'.startswith(result):
282 return True
283 if 'no'.startswith(result):
284 return False
Edward Lesmesae3586b2020-03-23 21:21:14 +0000285 result = gclient_utils.AskForData('Please, type yes or no: ').lower()
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100286
287
machenbach@chromium.org45453142015-09-15 08:45:22 +0000288def _get_properties_from_options(options):
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000289 prop_list = getattr(options, 'properties', [])
290 properties = dict(x.split('=', 1) for x in prop_list)
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000291 for key, val in properties.items():
machenbach@chromium.org45453142015-09-15 08:45:22 +0000292 try:
293 properties[key] = json.loads(val)
294 except ValueError:
295 pass # If a value couldn't be evaluated, treat it as a string.
296 return properties
297
298
Edward Lemur4c707a22019-09-24 21:13:43 +0000299def _call_buildbucket(http, buildbucket_host, method, request):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000300 """Calls a buildbucket v2 method and returns the parsed json response."""
301 headers = {
302 'Accept': 'application/json',
303 'Content-Type': 'application/json',
304 }
305 request = json.dumps(request)
306 url = 'https://%s/prpc/buildbucket.v2.Builds/%s' % (buildbucket_host, method)
307
308 logging.info('POST %s with %s' % (url, request))
309
310 attempts = 1
311 time_to_sleep = 1
312 while True:
313 response, content = http.request(url, 'POST', body=request, headers=headers)
314 if response.status == 200:
315 return json.loads(content[4:])
316 if attempts >= MAX_ATTEMPTS or 400 <= response.status < 500:
317 msg = '%s error when calling POST %s with %s: %s' % (
318 response.status, url, request, content)
319 raise BuildbucketResponseException(msg)
320 logging.debug(
321 '%s error when calling POST %s with %s. '
322 'Sleeping for %d seconds and retrying...' % (
323 response.status, url, request, time_to_sleep))
324 time.sleep(time_to_sleep)
325 time_to_sleep *= 2
326 attempts += 1
327
328 assert False, 'unreachable'
329
330
Edward Lemur6215c792019-10-03 21:59:05 +0000331def _parse_bucket(raw_bucket):
332 legacy = True
333 project = bucket = None
334 if '/' in raw_bucket:
335 legacy = False
336 project, bucket = raw_bucket.split('/', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000337 # Assume luci.<project>.<bucket>.
Edward Lemur6215c792019-10-03 21:59:05 +0000338 elif raw_bucket.startswith('luci.'):
339 project, bucket = raw_bucket[len('luci.'):].split('.', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000340 # Otherwise, assume prefix is also the project name.
Edward Lemur6215c792019-10-03 21:59:05 +0000341 elif '.' in raw_bucket:
342 project = raw_bucket.split('.')[0]
343 bucket = raw_bucket
344 # Legacy buckets.
Edward Lemur45768512020-03-02 19:03:14 +0000345 if legacy and project and bucket:
Edward Lemur6215c792019-10-03 21:59:05 +0000346 print('WARNING Please use %s/%s to specify the bucket.' % (project, bucket))
347 return project, bucket
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000348
349
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000350def _canonical_git_googlesource_host(host):
351 """Normalizes Gerrit hosts (with '-review') to Git host."""
352 assert host.endswith(_GOOGLESOURCE)
353 # Prefix doesn't include '.' at the end.
354 prefix = host[:-(1 + len(_GOOGLESOURCE))]
355 if prefix.endswith('-review'):
356 prefix = prefix[:-len('-review')]
357 return prefix + '.' + _GOOGLESOURCE
358
359
360def _canonical_gerrit_googlesource_host(host):
361 git_host = _canonical_git_googlesource_host(host)
362 prefix = git_host.split('.', 1)[0]
363 return prefix + '-review.' + _GOOGLESOURCE
364
365
366def _get_counterpart_host(host):
367 assert host.endswith(_GOOGLESOURCE)
368 git = _canonical_git_googlesource_host(host)
369 gerrit = _canonical_gerrit_googlesource_host(git)
370 return git if gerrit == host else gerrit
371
372
Quinten Yearsley777660f2020-03-04 23:37:06 +0000373def _trigger_tryjobs(changelist, jobs, options, patchset):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000374 """Sends a request to Buildbucket to trigger tryjobs for a changelist.
qyearsley1fdfcb62016-10-24 13:22:03 -0700375
376 Args:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000377 changelist: Changelist that the tryjobs are associated with.
Edward Lemur45768512020-03-02 19:03:14 +0000378 jobs: A list of (project, bucket, builder).
qyearsley1fdfcb62016-10-24 13:22:03 -0700379 options: Command-line options.
380 """
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000381 print('Scheduling jobs on:')
Edward Lemur45768512020-03-02 19:03:14 +0000382 for project, bucket, builder in jobs:
383 print(' %s/%s: %s' % (project, bucket, builder))
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000384 print('To see results here, run: git cl try-results')
385 print('To see results in browser, run: git cl web')
tandriide281ae2016-10-12 06:02:30 -0700386
Quinten Yearsley777660f2020-03-04 23:37:06 +0000387 requests = _make_tryjob_schedule_requests(changelist, jobs, options, patchset)
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000388 if not requests:
389 return
390
Edward Lemur5b929a42019-10-21 17:57:39 +0000391 http = auth.Authenticator().authorize(httplib2.Http())
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000392 http.force_exception_to_status_code = True
393
394 batch_request = {'requests': requests}
Joanna Wanga8db0cb2023-01-24 15:43:17 +0000395 batch_response = _call_buildbucket(http, DEFAULT_BUILDBUCKET_HOST, 'Batch',
396 batch_request)
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000397
398 errors = [
399 ' ' + response['error']['message']
400 for response in batch_response.get('responses', [])
401 if 'error' in response
402 ]
403 if errors:
404 raise BuildbucketResponseException(
405 'Failed to schedule builds for some bots:\n%s' % '\n'.join(errors))
406
407
Quinten Yearsley777660f2020-03-04 23:37:06 +0000408def _make_tryjob_schedule_requests(changelist, jobs, options, patchset):
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000409 """Constructs requests for Buildbucket to trigger tryjobs."""
Edward Lemurf0faf482019-09-25 20:40:17 +0000410 gerrit_changes = [changelist.GetGerritChange(patchset)]
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000411 shared_properties = {
412 'category': options.ensure_value('category', 'git_cl_try')
413 }
414 if options.ensure_value('clobber', False):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000415 shared_properties['clobber'] = True
416 shared_properties.update(_get_properties_from_options(options) or {})
417
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000418 shared_tags = [{'key': 'user_agent', 'value': 'git_cl_try'}]
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000419 if options.ensure_value('retry_failed', False):
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000420 shared_tags.append({'key': 'retry_failed',
421 'value': '1'})
422
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000423 requests = []
Edward Lemur45768512020-03-02 19:03:14 +0000424 for (project, bucket, builder) in jobs:
425 properties = shared_properties.copy()
426 if 'presubmit' in builder.lower():
427 properties['dry_run'] = 'true'
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000428
Edward Lemur45768512020-03-02 19:03:14 +0000429 requests.append({
430 'scheduleBuild': {
431 'requestId': str(uuid.uuid4()),
432 'builder': {
433 'project': getattr(options, 'project', None) or project,
434 'bucket': bucket,
435 'builder': builder,
436 },
437 'gerritChanges': gerrit_changes,
438 'properties': properties,
439 'tags': [
440 {'key': 'builder', 'value': builder},
441 ] + shared_tags,
442 }
443 })
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000444
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000445 if options.ensure_value('revision', None):
Josip Sokcevic9011a5b2021-02-12 18:59:44 +0000446 remote, remote_branch = changelist.GetRemoteBranch()
Edward Lemur45768512020-03-02 19:03:14 +0000447 requests[-1]['scheduleBuild']['gitilesCommit'] = {
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000448 'host': _canonical_git_googlesource_host(gerrit_changes[0]['host']),
Edward Lemur45768512020-03-02 19:03:14 +0000449 'project': gerrit_changes[0]['project'],
Josip Sokcevic9011a5b2021-02-12 18:59:44 +0000450 'id': options.revision,
451 'ref': GetTargetRef(remote, remote_branch, None)
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000452 }
Anthony Polito1a5fe232020-01-24 23:17:52 +0000453
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000454 return requests
kjellander@chromium.org44424542015-06-02 18:35:29 +0000455
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000456
Quinten Yearsley777660f2020-03-04 23:37:06 +0000457def _fetch_tryjobs(changelist, buildbucket_host, patchset=None):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000458 """Fetches tryjobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000459
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000460 Returns list of buildbucket.v2.Build with the try jobs for the changelist.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000461 """
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000462 fields = ['id', 'builder', 'status', 'createTime', 'tags']
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000463 request = {
464 'predicate': {
465 'gerritChanges': [changelist.GetGerritChange(patchset)],
466 },
467 'fields': ','.join('builds.*.' + field for field in fields),
468 }
tandrii221ab252016-10-06 08:12:04 -0700469
Edward Lemur5b929a42019-10-21 17:57:39 +0000470 authenticator = auth.Authenticator()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000471 if authenticator.has_cached_credentials():
472 http = authenticator.authorize(httplib2.Http())
473 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700474 print('Warning: Some results might be missing because %s' %
475 # Get the message on how to login.
Andrii Shyshkalov2517afd2021-01-19 17:07:43 +0000476 (str(auth.LoginRequiredError()),))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000477 http = httplib2.Http()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000478 http.force_exception_to_status_code = True
479
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000480 response = _call_buildbucket(http, buildbucket_host, 'SearchBuilds', request)
481 return response.get('builds', [])
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000482
Edward Lemur45768512020-03-02 19:03:14 +0000483
Edward Lemur5b929a42019-10-21 17:57:39 +0000484def _fetch_latest_builds(changelist, buildbucket_host, latest_patchset=None):
Quinten Yearsley983111f2019-09-26 17:18:48 +0000485 """Fetches builds from the latest patchset that has builds (within
486 the last few patchsets).
487
488 Args:
Quinten Yearsley983111f2019-09-26 17:18:48 +0000489 changelist (Changelist): The CL to fetch builds for
490 buildbucket_host (str): Buildbucket host, e.g. "cr-buildbucket.appspot.com"
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000491 lastest_patchset(int|NoneType): the patchset to start fetching builds from.
492 If None (default), starts with the latest available patchset.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000493 Returns:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000494 A tuple (builds, patchset) where builds is a list of buildbucket.v2.Build,
495 and patchset is the patchset number where those builds came from.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000496 """
497 assert buildbucket_host
498 assert changelist.GetIssue(), 'CL must be uploaded first'
499 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000500 if latest_patchset is None:
501 assert changelist.GetMostRecentPatchset()
502 ps = changelist.GetMostRecentPatchset()
503 else:
504 assert latest_patchset > 0, latest_patchset
505 ps = latest_patchset
506
Quinten Yearsley983111f2019-09-26 17:18:48 +0000507 min_ps = max(1, ps - 5)
508 while ps >= min_ps:
Quinten Yearsley777660f2020-03-04 23:37:06 +0000509 builds = _fetch_tryjobs(changelist, buildbucket_host, patchset=ps)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000510 if len(builds):
511 return builds, ps
512 ps -= 1
513 return [], 0
514
515
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000516def _filter_failed_for_retry(all_builds):
517 """Returns a list of buckets/builders that are worth retrying.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000518
519 Args:
Quinten Yearsley777660f2020-03-04 23:37:06 +0000520 all_builds (list): Builds, in the format returned by _fetch_tryjobs,
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000521 i.e. a list of buildbucket.v2.Builds which includes status and builder
522 info.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000523
524 Returns:
Edward Lemur45768512020-03-02 19:03:14 +0000525 A dict {(proj, bucket): [builders]}. This is the same format accepted by
Quinten Yearsley777660f2020-03-04 23:37:06 +0000526 _trigger_tryjobs.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000527 """
Edward Lemur45768512020-03-02 19:03:14 +0000528 grouped = {}
529 for build in all_builds:
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000530 builder = build['builder']
Edward Lemur45768512020-03-02 19:03:14 +0000531 key = (builder['project'], builder['bucket'], builder['builder'])
532 grouped.setdefault(key, []).append(build)
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000533
Edward Lemur45768512020-03-02 19:03:14 +0000534 jobs = []
535 for (project, bucket, builder), builds in grouped.items():
536 if 'triggered' in builder:
537 print('WARNING: Not scheduling %s. Triggered bots require an initial job '
538 'from a parent. Please schedule a manual job for the parent '
539 'instead.')
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000540 continue
541 if any(b['status'] in ('STARTED', 'SCHEDULED') for b in builds):
542 # Don't retry if any are running.
543 continue
Edward Lemur45768512020-03-02 19:03:14 +0000544 # If builder had several builds, retry only if the last one failed.
545 # This is a bit different from CQ, which would re-use *any* SUCCESS-full
546 # build, but in case of retrying failed jobs retrying a flaky one makes
547 # sense.
548 builds = sorted(builds, key=lambda b: b['createTime'])
549 if builds[-1]['status'] not in ('FAILURE', 'INFRA_FAILURE'):
550 continue
551 # Don't retry experimental build previously triggered by CQ.
552 if any(t['key'] == 'cq_experimental' and t['value'] == 'true'
553 for t in builds[-1]['tags']):
554 continue
555 jobs.append((project, bucket, builder))
556
557 # Sort the jobs to make testing easier.
558 return sorted(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000559
560
Quinten Yearsley777660f2020-03-04 23:37:06 +0000561def _print_tryjobs(options, builds):
562 """Prints nicely result of _fetch_tryjobs."""
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000563 if not builds:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000564 print('No tryjobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000565 return
566
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000567 longest_builder = max(len(b['builder']['builder']) for b in builds)
568 name_fmt = '{builder:<%d}' % longest_builder
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000569 if options.print_master:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000570 longest_bucket = max(len(b['builder']['bucket']) for b in builds)
571 name_fmt = ('{bucket:>%d} ' % longest_bucket) + name_fmt
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000572
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000573 builds_by_status = {}
574 for b in builds:
575 builds_by_status.setdefault(b['status'], []).append({
576 'id': b['id'],
577 'name': name_fmt.format(
578 builder=b['builder']['builder'], bucket=b['builder']['bucket']),
579 })
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000580
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000581 sort_key = lambda b: (b['name'], b['id'])
582
583 def print_builds(title, builds, fmt=None, color=None):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000584 """Pop matching builds from `builds` dict and print them."""
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000585 if not builds:
586 return
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000587
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000588 fmt = fmt or '{name} https://ci.chromium.org/b/{id}'
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000589 if not options.color or color is None:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000590 colorize = lambda x: x
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000591 else:
592 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
593
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000594 print(colorize(title))
595 for b in sorted(builds, key=sort_key):
596 print(' ', colorize(fmt.format(**b)))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000597
598 total = len(builds)
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000599 print_builds(
600 'Successes:', builds_by_status.pop('SUCCESS', []), color=Fore.GREEN)
601 print_builds(
602 'Infra Failures:', builds_by_status.pop('INFRA_FAILURE', []),
603 color=Fore.MAGENTA)
604 print_builds('Failures:', builds_by_status.pop('FAILURE', []), color=Fore.RED)
605 print_builds('Canceled:', builds_by_status.pop('CANCELED', []), fmt='{name}',
606 color=Fore.MAGENTA)
Andrii Shyshkalov792630c2020-10-19 16:47:44 +0000607 print_builds('Started:', builds_by_status.pop('STARTED', []),
608 color=Fore.YELLOW)
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000609 print_builds(
610 'Scheduled:', builds_by_status.pop('SCHEDULED', []), fmt='{name} id={id}')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000611 # The last section is just in case buildbucket API changes OR there is a bug.
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000612 print_builds(
613 'Other:', sum(builds_by_status.values(), []), fmt='{name} id={id}')
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000614 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000615
616
Aiden Bennerc08566e2018-10-03 17:52:42 +0000617def _ComputeDiffLineRanges(files, upstream_commit):
618 """Gets the changed line ranges for each file since upstream_commit.
619
620 Parses a git diff on provided files and returns a dict that maps a file name
621 to an ordered list of range tuples in the form (start_line, count).
622 Ranges are in the same format as a git diff.
623 """
624 # If files is empty then diff_output will be a full diff.
625 if len(files) == 0:
626 return {}
627
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000628 # Take the git diff and find the line ranges where there are changes.
Jamie Madill3671a6a2019-10-24 15:13:21 +0000629 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000630 diff_output = RunGit(diff_cmd)
631
632 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
633 # 2 capture groups
634 # 0 == fname of diff file
635 # 1 == 'diff_start,diff_count' or 'diff_start'
636 # will match each of
637 # diff --git a/foo.foo b/foo.py
638 # @@ -12,2 +14,3 @@
639 # @@ -12,2 +17 @@
640 # running re.findall on the above string with pattern will give
641 # [('foo.py', ''), ('', '14,3'), ('', '17')]
642
643 curr_file = None
644 line_diffs = {}
645 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
646 if match[0] != '':
647 # Will match the second filename in diff --git a/a.py b/b.py.
648 curr_file = match[0]
649 line_diffs[curr_file] = []
650 else:
651 # Matches +14,3
652 if ',' in match[1]:
653 diff_start, diff_count = match[1].split(',')
654 else:
655 # Single line changes are of the form +12 instead of +12,1.
656 diff_start = match[1]
657 diff_count = 1
658
659 diff_start = int(diff_start)
660 diff_count = int(diff_count)
661
662 # If diff_count == 0 this is a removal we can ignore.
663 line_diffs[curr_file].append((diff_start, diff_count))
664
665 return line_diffs
666
667
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000668def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000669 """Checks if a yapf file is in any parent directory of fpath until top_dir.
670
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000671 Recursively checks parent directories to find yapf file and if no yapf file
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000672 is found returns None. Uses yapf_config_cache as a cache for previously found
673 configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000674 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000675 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000676 # Return result if we've already computed it.
677 if fpath in yapf_config_cache:
678 return yapf_config_cache[fpath]
679
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000680 parent_dir = os.path.dirname(fpath)
681 if os.path.isfile(fpath):
682 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000683 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000684 # Otherwise fpath is a directory
685 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
686 if os.path.isfile(yapf_file):
687 ret = yapf_file
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000688 elif fpath in (top_dir, parent_dir):
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000689 # If we're at the top level directory, or if we're at root
690 # there is no provided style.
691 ret = None
692 else:
693 # Otherwise recurse on the current directory.
694 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000695 yapf_config_cache[fpath] = ret
696 return ret
697
698
Brian Sheedyb4307d52019-12-02 19:18:17 +0000699def _GetYapfIgnorePatterns(top_dir):
700 """Returns all patterns in the .yapfignore file.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000701
702 yapf is supposed to handle the ignoring of files listed in .yapfignore itself,
703 but this functionality appears to break when explicitly passing files to
704 yapf for formatting. According to
Josip Sokcevicc39ab992020-09-24 20:09:15 +0000705 https://github.com/google/yapf/blob/HEAD/README.rst#excluding-files-from-formatting-yapfignore,
Brian Sheedy59b06a82019-10-14 17:03:29 +0000706 the .yapfignore file should be in the directory that yapf is invoked from,
707 which we assume to be the top level directory in this case.
708
709 Args:
710 top_dir: The top level directory for the repository being formatted.
711
712 Returns:
Brian Sheedyb4307d52019-12-02 19:18:17 +0000713 A set of all fnmatch patterns to be ignored.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000714 """
715 yapfignore_file = os.path.join(top_dir, '.yapfignore')
Brian Sheedyb4307d52019-12-02 19:18:17 +0000716 ignore_patterns = set()
Brian Sheedy59b06a82019-10-14 17:03:29 +0000717 if not os.path.exists(yapfignore_file):
Brian Sheedyb4307d52019-12-02 19:18:17 +0000718 return ignore_patterns
Brian Sheedy59b06a82019-10-14 17:03:29 +0000719
Anthony Politoc64e3902021-04-30 21:55:25 +0000720 for line in gclient_utils.FileRead(yapfignore_file).split('\n'):
721 stripped_line = line.strip()
722 # Comments and blank lines should be ignored.
723 if stripped_line.startswith('#') or stripped_line == '':
724 continue
725 ignore_patterns.add(stripped_line)
Brian Sheedyb4307d52019-12-02 19:18:17 +0000726 return ignore_patterns
727
728
729def _FilterYapfIgnoredFiles(filepaths, patterns):
730 """Filters out any filepaths that match any of the given patterns.
731
732 Args:
733 filepaths: An iterable of strings containing filepaths to filter.
734 patterns: An iterable of strings containing fnmatch patterns to filter on.
735
736 Returns:
737 A list of strings containing all the elements of |filepaths| that did not
738 match any of the patterns in |patterns|.
739 """
740 # Not inlined so that tests can use the same implementation.
741 return [f for f in filepaths
742 if not any(fnmatch.fnmatch(f, p) for p in patterns)]
Brian Sheedy59b06a82019-10-14 17:03:29 +0000743
744
Aaron Gable13101a62018-02-09 13:20:41 -0800745def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000746 """Prints statistics about the change to the user."""
747 # --no-ext-diff is broken in some versions of Git, so try to work around
748 # this by overriding the environment (but there is still a problem if the
749 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000750 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000751 if 'GIT_EXTERNAL_DIFF' in env:
752 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000753
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000754 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800755 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
Edward Lemur0db01f02019-11-12 22:01:51 +0000756 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000757
758
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000759class BuildbucketResponseException(Exception):
760 pass
761
762
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000763class Settings(object):
764 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000765 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000766 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000767 self.tree_status_url = None
768 self.viewvc_url = None
769 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000770 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000771 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000772 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000773 self.git_editor = None
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000774 self.format_full_by_default = None
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +0000775 self.is_status_commit_order_by_date = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000776
Edward Lemur26964072020-02-19 19:18:51 +0000777 def _LazyUpdateIfNeeded(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000778 """Updates the settings from a codereview.settings file, if available."""
Edward Lemur26964072020-02-19 19:18:51 +0000779 if self.updated:
780 return
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000781
Edward Lemur26964072020-02-19 19:18:51 +0000782 # The only value that actually changes the behavior is
783 # autoupdate = "false". Everything else means "true".
784 autoupdate = (
785 scm.GIT.GetConfig(self.GetRoot(), 'rietveld.autoupdate', '').lower())
786
787 cr_settings_file = FindCodereviewSettingsFile()
788 if autoupdate != 'false' and cr_settings_file:
789 LoadCodereviewSettingsFromFile(cr_settings_file)
790 cr_settings_file.close()
791
792 self.updated = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000793
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000794 @staticmethod
795 def GetRelativeRoot():
Edward Lesmes50da7702020-03-30 19:23:43 +0000796 return scm.GIT.GetCheckoutRoot('.')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000797
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000798 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000799 if self.root is None:
800 self.root = os.path.abspath(self.GetRelativeRoot())
801 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000802
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000803 def GetTreeStatusUrl(self, error_ok=False):
804 if not self.tree_status_url:
Edward Lemur26964072020-02-19 19:18:51 +0000805 self.tree_status_url = self._GetConfig('rietveld.tree-status-url')
806 if self.tree_status_url is None and not error_ok:
807 DieWithError(
808 'You must configure your tree status URL by running '
809 '"git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000810 return self.tree_status_url
811
812 def GetViewVCUrl(self):
813 if not self.viewvc_url:
Edward Lemur26964072020-02-19 19:18:51 +0000814 self.viewvc_url = self._GetConfig('rietveld.viewvc-url')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000815 return self.viewvc_url
816
rmistry@google.com90752582014-01-14 21:04:50 +0000817 def GetBugPrefix(self):
Edward Lemur26964072020-02-19 19:18:51 +0000818 return self._GetConfig('rietveld.bug-prefix')
rmistry@google.com78948ed2015-07-08 23:09:57 +0000819
rmistry@google.com5626a922015-02-26 14:03:30 +0000820 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000821 run_post_upload_hook = self._GetConfig(
Edward Lemur26964072020-02-19 19:18:51 +0000822 'rietveld.run-post-upload-hook')
rmistry@google.com5626a922015-02-26 14:03:30 +0000823 return run_post_upload_hook == "True"
824
Joanna Wangc8f23e22023-01-19 21:18:10 +0000825 def GetDefaultCCList(self):
826 return self._GetConfig('rietveld.cc')
827
Dirk Pranke6f0df682021-06-25 00:42:33 +0000828 def GetUsePython3(self):
829 return self._GetConfig('rietveld.use-python3')
830
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000831 def GetSquashGerritUploads(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000832 """Returns True if uploads to Gerrit should be squashed by default."""
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000833 if self.squash_gerrit_uploads is None:
Edward Lesmes4de54132020-05-05 19:41:33 +0000834 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
835 if self.squash_gerrit_uploads is None:
Edward Lemur26964072020-02-19 19:18:51 +0000836 # Default is squash now (http://crbug.com/611892#c23).
837 self.squash_gerrit_uploads = self._GetConfig(
838 'gerrit.squash-uploads').lower() != 'false'
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000839 return self.squash_gerrit_uploads
840
Edward Lesmes4de54132020-05-05 19:41:33 +0000841 def GetSquashGerritUploadsOverride(self):
842 """Return True or False if codereview.settings should be overridden.
843
844 Returns None if no override has been defined.
845 """
846 # See also http://crbug.com/611892#c23
847 result = self._GetConfig('gerrit.override-squash-uploads').lower()
848 if result == 'true':
849 return True
850 if result == 'false':
851 return False
852 return None
853
Aleksey Khoroshilov35ef5ad2022-06-03 18:29:25 +0000854 def GetIsGerrit(self):
855 """Return True if gerrit.host is set."""
856 if self.is_gerrit is None:
857 self.is_gerrit = bool(self._GetConfig('gerrit.host', False))
858 return self.is_gerrit
859
tandrii@chromium.org28253532016-04-14 13:46:56 +0000860 def GetGerritSkipEnsureAuthenticated(self):
861 """Return True if EnsureAuthenticated should not be done for Gerrit
862 uploads."""
863 if self.gerrit_skip_ensure_authenticated is None:
Edward Lemur26964072020-02-19 19:18:51 +0000864 self.gerrit_skip_ensure_authenticated = self._GetConfig(
865 'gerrit.skip-ensure-authenticated').lower() == 'true'
tandrii@chromium.org28253532016-04-14 13:46:56 +0000866 return self.gerrit_skip_ensure_authenticated
867
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000868 def GetGitEditor(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000869 """Returns the editor specified in the git config, or None if none is."""
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000870 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000871 # Git requires single quotes for paths with spaces. We need to replace
872 # them with double quotes for Windows to treat such paths as a single
873 # path.
Edward Lemur26964072020-02-19 19:18:51 +0000874 self.git_editor = self._GetConfig('core.editor').replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000875 return self.git_editor or None
876
thestig@chromium.org44202a22014-03-11 19:22:18 +0000877 def GetLintRegex(self):
Edward Lemur26964072020-02-19 19:18:51 +0000878 return self._GetConfig('rietveld.cpplint-regex', DEFAULT_LINT_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000879
880 def GetLintIgnoreRegex(self):
Edward Lemur26964072020-02-19 19:18:51 +0000881 return self._GetConfig(
882 'rietveld.cpplint-ignore-regex', DEFAULT_LINT_IGNORE_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000883
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000884 def GetFormatFullByDefault(self):
885 if self.format_full_by_default is None:
Jamie Madillac6f6232021-07-07 20:54:08 +0000886 self._LazyUpdateIfNeeded()
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000887 result = (
888 RunGit(['config', '--bool', 'rietveld.format-full-by-default'],
889 error_ok=True).strip())
890 self.format_full_by_default = (result == 'true')
891 return self.format_full_by_default
892
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +0000893 def IsStatusCommitOrderByDate(self):
894 if self.is_status_commit_order_by_date is None:
895 result = (RunGit(['config', '--bool', 'cl.date-order'],
896 error_ok=True).strip())
897 self.is_status_commit_order_by_date = (result == 'true')
898 return self.is_status_commit_order_by_date
899
Edward Lemur26964072020-02-19 19:18:51 +0000900 def _GetConfig(self, key, default=''):
901 self._LazyUpdateIfNeeded()
902 return scm.GIT.GetConfig(self.GetRoot(), key, default)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000903
904
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000905class _CQState(object):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +0000906 """Enum for states of CL with respect to CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000907 NONE = 'none'
Greg Gutermanbe5fccd2021-06-14 17:58:20 +0000908 QUICK_RUN = 'quick_run'
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000909 DRY_RUN = 'dry_run'
910 COMMIT = 'commit'
911
Greg Gutermanbe5fccd2021-06-14 17:58:20 +0000912 ALL_STATES = [NONE, QUICK_RUN, DRY_RUN, COMMIT]
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000913
914
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000915class _ParsedIssueNumberArgument(object):
Edward Lemurf38bc172019-09-03 21:02:13 +0000916 def __init__(self, issue=None, patchset=None, hostname=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000917 self.issue = issue
918 self.patchset = patchset
919 self.hostname = hostname
920
921 @property
922 def valid(self):
923 return self.issue is not None
924
925
Edward Lemurf38bc172019-09-03 21:02:13 +0000926def ParseIssueNumberArgument(arg):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000927 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
928 fail_result = _ParsedIssueNumberArgument()
929
Edward Lemur678a6842019-10-03 22:25:05 +0000930 if isinstance(arg, int):
931 return _ParsedIssueNumberArgument(issue=arg)
932 if not isinstance(arg, basestring):
933 return fail_result
934
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000935 if arg.isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +0000936 return _ParsedIssueNumberArgument(issue=int(arg))
Aaron Gableaee6c852017-06-26 12:49:01 -0700937
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000938 url = gclient_utils.UpgradeToHttps(arg)
Alex Turner30ae6372022-01-04 02:32:52 +0000939 if not url.startswith('http'):
940 return fail_result
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000941 for gerrit_url, short_url in _KNOWN_GERRIT_TO_SHORT_URLS.items():
942 if url.startswith(short_url):
943 url = gerrit_url + url[len(short_url):]
944 break
945
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000946 try:
Edward Lemur79d4f992019-11-11 23:49:02 +0000947 parsed_url = urllib.parse.urlparse(url)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000948 except ValueError:
949 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +0200950
Alex Turner30ae6372022-01-04 02:32:52 +0000951 # If "https://" was automatically added, fail if `arg` looks unlikely to be a
952 # URL.
953 if not arg.startswith('http') and '.' not in parsed_url.netloc:
954 return fail_result
955
Edward Lemur678a6842019-10-03 22:25:05 +0000956 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
957 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
958 # Short urls like https://domain/<issue_number> can be used, but don't allow
959 # specifying the patchset (you'd 404), but we allow that here.
960 if parsed_url.path == '/':
961 part = parsed_url.fragment
962 else:
963 part = parsed_url.path
964
965 match = re.match(
966 r'(/c(/.*/\+)?)?/(?P<issue>\d+)(/(?P<patchset>\d+)?/?)?$', part)
967 if not match:
968 return fail_result
969
970 issue = int(match.group('issue'))
971 patchset = match.group('patchset')
972 return _ParsedIssueNumberArgument(
973 issue=issue,
974 patchset=int(patchset) if patchset else None,
975 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000976
977
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000978def _create_description_from_log(args):
979 """Pulls out the commit log to use as a base for the CL description."""
980 log_args = []
Bruce Dawson13acea32022-05-03 22:13:08 +0000981 if len(args) == 1 and args[0] == None:
982 # Handle the case where None is passed as the branch.
983 return ''
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000984 if len(args) == 1 and not args[0].endswith('.'):
985 log_args = [args[0] + '..']
986 elif len(args) == 1 and args[0].endswith('...'):
987 log_args = [args[0][:-1]]
988 elif len(args) == 2:
989 log_args = [args[0] + '..' + args[1]]
990 else:
991 log_args = args[:] # Hope for the best!
Manh Nguyene3644862020-08-05 18:25:46 +0000992 return RunGit(['log', '--pretty=format:%B%n'] + log_args)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000993
994
Aaron Gablea45ee112016-11-22 15:14:38 -0800995class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -0700996 def __init__(self, issue, url):
997 self.issue = issue
998 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -0800999 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001000
1001 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001002 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001003 self.issue, self.url)
1004
1005
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001006_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001007 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001008 # TODO(tandrii): these two aren't known in Gerrit.
1009 'approval', 'disapproval'])
1010
1011
Joanna Wang6215dd02023-02-07 15:58:03 +00001012# TODO(b/265929888): Change `parent` to `pushed_commit_base`.
Joanna Wange8523912023-01-21 02:05:40 +00001013_NewUpload = collections.namedtuple('NewUpload', [
Joanna Wang40497912023-01-24 21:18:16 +00001014 'reviewers', 'ccs', 'commit_to_push', 'new_last_uploaded_commit', 'parent',
Joanna Wang7603f042023-03-01 22:17:36 +00001015 'change_desc', 'prev_patchset'
Joanna Wange8523912023-01-21 02:05:40 +00001016])
1017
1018
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001019class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001020 """Changelist works with one changelist in local branch.
1021
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001022 Notes:
1023 * Not safe for concurrent multi-{thread,process} use.
1024 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001025 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001026 """
1027
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00001028 def __init__(self,
1029 branchref=None,
1030 issue=None,
1031 codereview_host=None,
1032 commit_date=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001033 """Create a new ChangeList instance.
1034
Edward Lemurf38bc172019-09-03 21:02:13 +00001035 **kwargs will be passed directly to Gerrit implementation.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001036 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001037 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001038 global settings
1039 if not settings:
1040 # Happens when git_cl.py is used as a utility library.
1041 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001042
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001043 self.branchref = branchref
1044 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001045 assert branchref.startswith('refs/heads/')
Edward Lemur85153282020-02-14 22:06:29 +00001046 self.branch = scm.GIT.ShortBranchName(self.branchref)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001047 else:
1048 self.branch = None
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00001049 self.commit_date = commit_date
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001050 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001051 self.lookedup_issue = False
1052 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001053 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001054 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001055 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001056 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001057 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001058 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001059 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001060
Edward Lemur125d60a2019-09-13 18:25:41 +00001061 # Lazily cached values.
1062 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1063 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Edward Lesmese1576912021-02-16 21:53:34 +00001064 self._owners_client = None
Edward Lemur125d60a2019-09-13 18:25:41 +00001065 # Map from change number (issue) to its detail cache.
1066 self._detail_cache = {}
1067
1068 if codereview_host is not None:
1069 assert not codereview_host.startswith('https://'), codereview_host
1070 self._gerrit_host = codereview_host
1071 self._gerrit_server = 'https://%s' % codereview_host
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001072
Edward Lesmese1576912021-02-16 21:53:34 +00001073 @property
1074 def owners_client(self):
1075 if self._owners_client is None:
1076 remote, remote_branch = self.GetRemoteBranch()
1077 branch = GetTargetRef(remote, remote_branch, None)
1078 self._owners_client = owners_client.GetCodeOwnersClient(
Edward Lesmese1576912021-02-16 21:53:34 +00001079 host=self.GetGerritHost(),
1080 project=self.GetGerritProject(),
1081 branch=branch)
1082 return self._owners_client
1083
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001084 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001085 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001086
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001087 The return value is a string suitable for passing to git cl with the --cc
1088 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001089 """
1090 if self.cc is None:
Joanna Wangc8f23e22023-01-19 21:18:10 +00001091 base_cc = settings.GetDefaultCCList()
1092 more_cc = ','.join(self.more_cc)
1093 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001094 return self.cc
1095
Daniel Cheng7227d212017-11-17 08:12:37 -08001096 def ExtendCC(self, more_cc):
1097 """Extends the list of users to cc on this CL based on the changed files."""
1098 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00001100 def GetCommitDate(self):
1101 """Returns the commit date as provided in the constructor"""
1102 return self.commit_date
1103
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001104 def GetBranch(self):
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001105 """Returns the short branch name, e.g. 'main'."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106 if not self.branch:
Edward Lemur85153282020-02-14 22:06:29 +00001107 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001108 if not branchref:
1109 return None
1110 self.branchref = branchref
Edward Lemur85153282020-02-14 22:06:29 +00001111 self.branch = scm.GIT.ShortBranchName(self.branchref)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001112 return self.branch
1113
1114 def GetBranchRef(self):
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001115 """Returns the full branch name, e.g. 'refs/heads/main'."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001116 self.GetBranch() # Poke the lazy loader.
1117 return self.branchref
1118
Edward Lemur85153282020-02-14 22:06:29 +00001119 def _GitGetBranchConfigValue(self, key, default=None):
1120 return scm.GIT.GetBranchConfig(
1121 settings.GetRoot(), self.GetBranch(), key, default)
tandrii5d48c322016-08-18 16:19:37 -07001122
Edward Lemur85153282020-02-14 22:06:29 +00001123 def _GitSetBranchConfigValue(self, key, value):
1124 action = 'set %s to %r' % (key, value)
1125 if not value:
1126 action = 'unset %s' % key
1127 assert self.GetBranch(), 'a branch is needed to ' + action
1128 return scm.GIT.SetBranchConfig(
1129 settings.GetRoot(), self.GetBranch(), key, value)
tandrii5d48c322016-08-18 16:19:37 -07001130
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001131 @staticmethod
1132 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001133 """Returns a tuple containing remote and remote ref,
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001134 e.g. 'origin', 'refs/heads/main'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001135 """
Edward Lemur15a9b8c2020-02-13 00:52:30 +00001136 remote, upstream_branch = scm.GIT.FetchUpstreamTuple(
1137 settings.GetRoot(), branch)
1138 if not remote or not upstream_branch:
1139 DieWithError(
1140 'Unable to determine default branch to diff against.\n'
Josip Sokcevicb038f722021-01-06 18:28:11 +00001141 'Verify this branch is set up to track another \n'
1142 '(via the --track argument to "git checkout -b ..."). \n'
1143 'or pass complete "git diff"-style arguments if supported, like\n'
1144 ' git cl upload origin/main\n')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145
1146 return remote, upstream_branch
1147
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001148 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001149 upstream_branch = self.GetUpstreamBranch()
Edward Lesmes50da7702020-03-30 19:23:43 +00001150 if not scm.GIT.IsValidRevision(settings.GetRoot(), upstream_branch):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001151 DieWithError('The upstream for the current branch (%s) does not exist '
1152 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001153 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001154 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001155
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156 def GetUpstreamBranch(self):
1157 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001158 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Raul Tambrefe1dbe12019-05-02 04:43:57 +00001159 if remote != '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001160 upstream_branch = upstream_branch.replace('refs/heads/',
1161 'refs/remotes/%s/' % remote)
1162 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1163 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001164 self.upstream_branch = upstream_branch
1165 return self.upstream_branch
1166
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001167 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001168 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001169 remote, branch = None, self.GetBranch()
1170 seen_branches = set()
1171 while branch not in seen_branches:
1172 seen_branches.add(branch)
1173 remote, branch = self.FetchUpstreamTuple(branch)
Edward Lemur85153282020-02-14 22:06:29 +00001174 branch = scm.GIT.ShortBranchName(branch)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001175 if remote != '.' or branch.startswith('refs/remotes'):
1176 break
1177 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001178 remotes = RunGit(['remote'], error_ok=True).split()
1179 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001180 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001181 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001182 remote = 'origin'
Gavin Make6a62332020-12-04 21:57:10 +00001183 logging.warning('Could not determine which remote this change is '
1184 'associated with, so defaulting to "%s".' %
1185 self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001186 else:
Gavin Make6a62332020-12-04 21:57:10 +00001187 logging.warning('Could not determine which remote this change is '
1188 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001189 branch = 'HEAD'
1190 if branch.startswith('refs/remotes'):
1191 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001192 elif branch.startswith('refs/branch-heads/'):
1193 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001194 else:
1195 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001196 return self._remote
1197
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001198 def GetRemoteUrl(self):
1199 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1200
1201 Returns None if there is no remote.
1202 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001203 is_cached, value = self._cached_remote_url
1204 if is_cached:
1205 return value
1206
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001207 remote, _ = self.GetRemoteBranch()
Edward Lemur26964072020-02-19 19:18:51 +00001208 url = scm.GIT.GetConfig(settings.GetRoot(), 'remote.%s.url' % remote, '')
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001209
Edward Lemur298f2cf2019-02-22 21:40:39 +00001210 # Check if the remote url can be parsed as an URL.
Edward Lemur79d4f992019-11-11 23:49:02 +00001211 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001212 if host:
1213 self._cached_remote_url = (True, url)
1214 return url
1215
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001216 # If it cannot be parsed as an url, assume it is a local directory,
1217 # probably a git cache.
Edward Lemur298f2cf2019-02-22 21:40:39 +00001218 logging.warning('"%s" doesn\'t appear to point to a git host. '
1219 'Interpreting it as a local directory.', url)
1220 if not os.path.isdir(url):
1221 logging.error(
Josip906bfde2020-01-31 22:38:49 +00001222 'Remote "%(remote)s" for branch "%(branch)s" points to "%(url)s", '
1223 'but it doesn\'t exist.',
1224 {'remote': remote, 'branch': self.GetBranch(), 'url': url})
Edward Lemur298f2cf2019-02-22 21:40:39 +00001225 return None
1226
1227 cache_path = url
Edward Lemur26964072020-02-19 19:18:51 +00001228 url = scm.GIT.GetConfig(url, 'remote.%s.url' % remote, '')
Edward Lemur298f2cf2019-02-22 21:40:39 +00001229
Edward Lemur79d4f992019-11-11 23:49:02 +00001230 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001231 if not host:
1232 logging.error(
1233 'Remote "%(remote)s" for branch "%(branch)s" points to '
1234 '"%(cache_path)s", but it is misconfigured.\n'
1235 '"%(cache_path)s" must be a git repo and must have a remote named '
1236 '"%(remote)s" pointing to the git host.', {
1237 'remote': remote,
1238 'cache_path': cache_path,
1239 'branch': self.GetBranch()})
1240 return None
1241
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001242 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001243 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001244
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001245 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001246 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001247 if self.issue is None and not self.lookedup_issue:
Bruce Dawson13acea32022-05-03 22:13:08 +00001248 if self.GetBranch():
1249 self.issue = self._GitGetBranchConfigValue(ISSUE_CONFIG_KEY)
Edward Lemur85153282020-02-14 22:06:29 +00001250 if self.issue is not None:
1251 self.issue = int(self.issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001252 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001253 return self.issue
1254
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00001255 def GetIssueURL(self, short=False):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001256 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001257 issue = self.GetIssue()
1258 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001259 return None
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00001260 server = self.GetCodereviewServer()
1261 if short:
1262 server = _KNOWN_GERRIT_TO_SHORT_URLS.get(server, server)
1263 return '%s/%s' % (server, issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001264
Dirk Pranke6f0df682021-06-25 00:42:33 +00001265 def GetUsePython3(self):
Josip Sokcevic340edc32021-07-08 17:01:46 +00001266 return settings.GetUsePython3()
Dirk Pranke6f0df682021-06-25 00:42:33 +00001267
Edward Lemur6c6827c2020-02-06 21:15:18 +00001268 def FetchDescription(self, pretty=False):
1269 assert self.GetIssue(), 'issue is required to query Gerrit'
1270
Edward Lemur9aa1a962020-02-25 00:58:38 +00001271 if self.description is None:
Edward Lemur6c6827c2020-02-06 21:15:18 +00001272 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
1273 current_rev = data['current_revision']
1274 self.description = data['revisions'][current_rev]['commit']['message']
Edward Lemur6c6827c2020-02-06 21:15:18 +00001275
1276 if not pretty:
1277 return self.description
1278
1279 # Set width to 72 columns + 2 space indent.
1280 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
1281 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1282 lines = self.description.splitlines()
1283 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001284
1285 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001286 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001287 if self.patchset is None and not self.lookedup_patchset:
Bruce Dawson13acea32022-05-03 22:13:08 +00001288 if self.GetBranch():
1289 self.patchset = self._GitGetBranchConfigValue(PATCHSET_CONFIG_KEY)
Edward Lemur85153282020-02-14 22:06:29 +00001290 if self.patchset is not None:
1291 self.patchset = int(self.patchset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001292 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293 return self.patchset
1294
Edward Lemur9aa1a962020-02-25 00:58:38 +00001295 def GetAuthor(self):
1296 return scm.GIT.GetConfig(settings.GetRoot(), 'user.email')
1297
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001299 """Set this branch's patchset. If patchset=0, clears the patchset."""
1300 assert self.GetBranch()
1301 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001302 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001303 else:
1304 self.patchset = int(patchset)
Edward Lesmes50da7702020-03-30 19:23:43 +00001305 self._GitSetBranchConfigValue(PATCHSET_CONFIG_KEY, str(self.patchset))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001306
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001307 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001308 """Set this branch's issue. If issue isn't given, clears the issue."""
1309 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001310 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001311 issue = int(issue)
Edward Lesmes50da7702020-03-30 19:23:43 +00001312 self._GitSetBranchConfigValue(ISSUE_CONFIG_KEY, str(issue))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001313 self.issue = issue
Edward Lemur125d60a2019-09-13 18:25:41 +00001314 codereview_server = self.GetCodereviewServer()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001315 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001316 self._GitSetBranchConfigValue(
Edward Lesmes50da7702020-03-30 19:23:43 +00001317 CODEREVIEW_SERVER_CONFIG_KEY, codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001318 else:
tandrii5d48c322016-08-18 16:19:37 -07001319 # Reset all of these just to be clean.
1320 reset_suffixes = [
Gavin Makbe2e9262022-11-08 23:41:55 +00001321 LAST_UPLOAD_HASH_CONFIG_KEY,
Edward Lesmes50da7702020-03-30 19:23:43 +00001322 ISSUE_CONFIG_KEY,
1323 PATCHSET_CONFIG_KEY,
1324 CODEREVIEW_SERVER_CONFIG_KEY,
Gavin Makbe2e9262022-11-08 23:41:55 +00001325 GERRIT_SQUASH_HASH_CONFIG_KEY,
Edward Lesmes50da7702020-03-30 19:23:43 +00001326 ]
tandrii5d48c322016-08-18 16:19:37 -07001327 for prop in reset_suffixes:
Edward Lemur85153282020-02-14 22:06:29 +00001328 try:
1329 self._GitSetBranchConfigValue(prop, None)
1330 except subprocess2.CalledProcessError:
1331 pass
Aaron Gableca01e2c2017-07-19 11:16:02 -07001332 msg = RunGit(['log', '-1', '--format=%B']).strip()
1333 if msg and git_footers.get_footer_change_id(msg):
1334 print('WARNING: The change patched into this branch has a Change-Id. '
1335 'Removing it.')
1336 RunGit(['commit', '--amend', '-m',
1337 git_footers.remove_footer(msg, 'Change-Id')])
Edward Lemurf38bc172019-09-03 21:02:13 +00001338 self.lookedup_issue = True
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001339 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001340 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001341
Joanna Wangb46232e2023-01-21 01:58:46 +00001342 def GetAffectedFiles(self, upstream, end_commit=None):
1343 # type: (str, Optional[str]) -> Sequence[str]
1344 """Returns the list of affected files for the given commit range."""
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001345 try:
Joanna Wangb46232e2023-01-21 01:58:46 +00001346 return [
1347 f for _, f in scm.GIT.CaptureStatus(
1348 settings.GetRoot(), upstream, end_commit=end_commit)
1349 ]
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001350 except subprocess2.CalledProcessError:
1351 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001352 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001353 'This branch probably doesn\'t exist anymore. To reset the\n'
1354 'tracking branch, please run\n'
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001355 ' git branch --set-upstream-to origin/main %s\n'
1356 'or replace origin/main with the relevant branch') %
Edward Lemur2c62b332020-03-12 22:12:33 +00001357 (upstream, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001358
dsansomee2d6fd92016-09-08 00:10:47 -07001359 def UpdateDescription(self, description, force=False):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001360 assert self.GetIssue(), 'issue is required to update description'
1361
1362 if gerrit_util.HasPendingChangeEdit(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001363 self.GetGerritHost(), self._GerritChangeIdentifier()):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001364 if not force:
1365 confirm_or_exit(
1366 'The description cannot be modified while the issue has a pending '
1367 'unpublished edit. Either publish the edit in the Gerrit web UI '
1368 'or delete it.\n\n', action='delete the unpublished edit')
1369
1370 gerrit_util.DeletePendingChangeEdit(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001371 self.GetGerritHost(), self._GerritChangeIdentifier())
Edward Lemur6c6827c2020-02-06 21:15:18 +00001372 gerrit_util.SetCommitMessage(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001373 self.GetGerritHost(), self._GerritChangeIdentifier(),
Edward Lemur6c6827c2020-02-06 21:15:18 +00001374 description, notify='NONE')
1375
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001376 self.description = description
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001377
Edward Lemur75526302020-02-27 22:31:05 +00001378 def _GetCommonPresubmitArgs(self, verbose, upstream):
Edward Lemur227d5102020-02-25 23:45:35 +00001379 args = [
Edward Lemur227d5102020-02-25 23:45:35 +00001380 '--root', settings.GetRoot(),
1381 '--upstream', upstream,
1382 ]
1383
1384 args.extend(['--verbose'] * verbose)
1385
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001386 remote, remote_branch = self.GetRemoteBranch()
1387 target_ref = GetTargetRef(remote, remote_branch, None)
Aleksey Khoroshilov35ef5ad2022-06-03 18:29:25 +00001388 if settings.GetIsGerrit():
1389 args.extend(['--gerrit_url', self.GetCodereviewServer()])
1390 args.extend(['--gerrit_project', self.GetGerritProject()])
1391 args.extend(['--gerrit_branch', target_ref])
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001392
Edward Lemur99df04e2020-03-05 19:39:43 +00001393 author = self.GetAuthor()
Edward Lemur227d5102020-02-25 23:45:35 +00001394 issue = self.GetIssue()
1395 patchset = self.GetPatchset()
Edward Lemur99df04e2020-03-05 19:39:43 +00001396 if author:
1397 args.extend(['--author', author])
Edward Lemur227d5102020-02-25 23:45:35 +00001398 if issue:
1399 args.extend(['--issue', str(issue)])
1400 if patchset:
1401 args.extend(['--patchset', str(patchset)])
Edward Lemur227d5102020-02-25 23:45:35 +00001402
Edward Lemur75526302020-02-27 22:31:05 +00001403 return args
1404
Josip Sokcevic017544d2022-03-31 23:47:53 +00001405 def RunHook(self,
1406 committing,
1407 may_prompt,
1408 verbose,
1409 parallel,
1410 upstream,
1411 description,
1412 all_files,
1413 files=None,
1414 resultdb=False,
1415 realm=None):
Edward Lemur75526302020-02-27 22:31:05 +00001416 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1417 args = self._GetCommonPresubmitArgs(verbose, upstream)
1418 args.append('--commit' if committing else '--upload')
Edward Lemur227d5102020-02-25 23:45:35 +00001419 if may_prompt:
1420 args.append('--may_prompt')
1421 if parallel:
1422 args.append('--parallel')
1423 if all_files:
1424 args.append('--all_files')
Josip Sokcevic017544d2022-03-31 23:47:53 +00001425 if files:
1426 args.extend(files.split(';'))
1427 args.append('--source_controlled_only')
Bruce Dawson09c0c072022-05-26 20:28:58 +00001428 if files or all_files:
1429 args.append('--no_diffs')
Edward Lemur227d5102020-02-25 23:45:35 +00001430
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001431 if resultdb and not realm:
1432 # TODO (crbug.com/1113463): store realm somewhere and look it up so
1433 # it is not required to pass the realm flag
1434 print('Note: ResultDB reporting will NOT be performed because --realm'
1435 ' was not specified. To enable ResultDB, please run the command'
1436 ' again with the --realm argument to specify the LUCI realm.')
1437
Josip Sokcevice9ece0f2023-03-08 21:31:25 +00001438 return self._RunPresubmit(args,
1439 description,
1440 use_python3=True,
1441 resultdb=resultdb,
1442 realm=realm)
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001443
Joanna Wanga8db0cb2023-01-24 15:43:17 +00001444 def _RunPresubmit(self,
1445 args,
1446 description,
1447 use_python3,
1448 resultdb=None,
1449 realm=None):
1450 # type: (Sequence[str], str, bool, Optional[bool], Optional[str]
1451 # ) -> Mapping[str, Any]
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001452 args = args[:]
1453 vpython = 'vpython3' if use_python3 else 'vpython'
1454
Edward Lemur227d5102020-02-25 23:45:35 +00001455 with gclient_utils.temporary_file() as description_file:
1456 with gclient_utils.temporary_file() as json_output:
Edward Lemur1a83da12020-03-04 21:18:36 +00001457 gclient_utils.FileWrite(description_file, description)
Edward Lemur227d5102020-02-25 23:45:35 +00001458 args.extend(['--json_output', json_output])
1459 args.extend(['--description_file', description_file])
Dirk Pranke6f0df682021-06-25 00:42:33 +00001460 if self.GetUsePython3():
1461 args.append('--use-python3')
Edward Lemur227d5102020-02-25 23:45:35 +00001462 start = time_time()
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001463 cmd = [vpython, PRESUBMIT_SUPPORT] + args
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001464 if resultdb and realm:
1465 cmd = ['rdb', 'stream', '-new', '-realm', realm, '--'] + cmd
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001466
1467 p = subprocess2.Popen(cmd)
Edward Lemur227d5102020-02-25 23:45:35 +00001468 exit_code = p.wait()
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001469
Edward Lemur227d5102020-02-25 23:45:35 +00001470 metrics.collector.add_repeated('sub_commands', {
1471 'command': 'presubmit',
1472 'execution_time': time_time() - start,
1473 'exit_code': exit_code,
1474 })
1475
1476 if exit_code:
1477 sys.exit(exit_code)
1478
1479 json_results = gclient_utils.FileRead(json_output)
1480 return json.loads(json_results)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001481
Brian Sheedy7326ca22022-11-02 18:36:17 +00001482 def RunPostUploadHook(self, verbose, upstream, description, py3_only):
Edward Lemur75526302020-02-27 22:31:05 +00001483 args = self._GetCommonPresubmitArgs(verbose, upstream)
1484 args.append('--post_upload')
1485
1486 with gclient_utils.temporary_file() as description_file:
Edward Lemur1a83da12020-03-04 21:18:36 +00001487 gclient_utils.FileWrite(description_file, description)
Edward Lemur75526302020-02-27 22:31:05 +00001488 args.extend(['--description_file', description_file])
Josip Sokcevice9ece0f2023-03-08 21:31:25 +00001489 subprocess2.Popen(['vpython3', PRESUBMIT_SUPPORT] + args +
1490 ['--use-python3']).wait()
Edward Lemur75526302020-02-27 22:31:05 +00001491
Edward Lemur5a644f82020-03-18 16:44:57 +00001492 def _GetDescriptionForUpload(self, options, git_diff_args, files):
Joanna Wangb46232e2023-01-21 01:58:46 +00001493 # type: (optparse.Values, Sequence[str], Sequence[str]
1494 # ) -> ChangeDescription
1495 """Get description message for upload."""
Edward Lemur5a644f82020-03-18 16:44:57 +00001496 if self.GetIssue():
1497 description = self.FetchDescription()
1498 elif options.message:
1499 description = options.message
1500 else:
1501 description = _create_description_from_log(git_diff_args)
1502 if options.title and options.squash:
Edward Lesmes0dd54822020-03-26 18:24:25 +00001503 description = options.title + '\n\n' + description
Edward Lemur5a644f82020-03-18 16:44:57 +00001504
Edward Lemur5a644f82020-03-18 16:44:57 +00001505 bug = options.bug
1506 fixed = options.fixed
Josip Sokcevic340edc32021-07-08 17:01:46 +00001507 if not self.GetIssue():
1508 # Extract bug number from branch name, but only if issue is being created.
1509 # It must start with bug or fix, followed by _ or - and number.
1510 # Optionally, it may contain _ or - after number with arbitrary text.
1511 # Examples:
1512 # bug-123
1513 # bug_123
1514 # fix-123
1515 # fix-123-some-description
mlcui7a0b4cb2023-01-23 23:14:55 +00001516 branch = self.GetBranch()
1517 if branch is not None:
1518 match = re.match(
1519 r'^(?P<type>bug|fix(?:e[sd])?)[_-]?(?P<bugnum>\d+)([-_]|$)', branch)
1520 if not bug and not fixed and match:
1521 if match.group('type') == 'bug':
1522 bug = match.group('bugnum')
1523 else:
1524 fixed = match.group('bugnum')
Edward Lemur5a644f82020-03-18 16:44:57 +00001525
1526 change_description = ChangeDescription(description, bug, fixed)
1527
Joanna Wang39811b12023-01-20 23:09:48 +00001528 # Fill gaps in OWNERS coverage to reviewers if requested.
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001529 if options.add_owners_to:
Joanna Wang39811b12023-01-20 23:09:48 +00001530 assert options.add_owners_to in ('R'), options.add_owners_to
Edward Lesmese1576912021-02-16 21:53:34 +00001531 status = self.owners_client.GetFilesApprovalStatus(
Joanna Wang39811b12023-01-20 23:09:48 +00001532 files, [], options.reviewers)
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001533 missing_files = [
1534 f for f in files
Edward Lesmese1576912021-02-16 21:53:34 +00001535 if status[f] == self._owners_client.INSUFFICIENT_REVIEWERS
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001536 ]
Edward Lesmese1576912021-02-16 21:53:34 +00001537 owners = self.owners_client.SuggestOwners(
1538 missing_files, exclude=[self.GetAuthor()])
Joanna Wang39811b12023-01-20 23:09:48 +00001539 assert isinstance(options.reviewers, list), options.reviewers
1540 options.reviewers.extend(owners)
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001541
Edward Lemur5a644f82020-03-18 16:44:57 +00001542 # Set the reviewer list now so that presubmit checks can access it.
Joanna Wang39811b12023-01-20 23:09:48 +00001543 if options.reviewers:
1544 change_description.update_reviewers(options.reviewers)
Edward Lemur5a644f82020-03-18 16:44:57 +00001545
1546 return change_description
1547
Joanna Wanga1abbed2023-01-24 01:41:05 +00001548 def _GetTitleForUpload(self, options, multi_change_upload=False):
1549 # type: (optparse.Values, Optional[bool]) -> str
1550
1551 # Getting titles for multipl commits is not supported so we return the
1552 # default.
1553 if not options.squash or multi_change_upload or options.title:
Edward Lemur5a644f82020-03-18 16:44:57 +00001554 return options.title
1555
1556 # On first upload, patchset title is always this string, while options.title
1557 # gets converted to first line of message.
1558 if not self.GetIssue():
1559 return 'Initial upload'
1560
1561 # When uploading subsequent patchsets, options.message is taken as the title
1562 # if options.title is not provided.
Edward Lemur5a644f82020-03-18 16:44:57 +00001563 if options.message:
1564 return options.message.strip()
1565
1566 # Use the subject of the last commit as title by default.
Edward Lesmes50da7702020-03-30 19:23:43 +00001567 title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
Mario Bianuccicebfb4e2020-07-22 23:08:16 +00001568 if options.force or options.skip_title:
Edward Lemur5a644f82020-03-18 16:44:57 +00001569 return title
Edward Lesmesae3586b2020-03-23 21:21:14 +00001570 user_title = gclient_utils.AskForData('Title for patchset [%s]: ' % title)
mlcui3da91712021-05-05 10:00:30 +00001571
1572 # Use the default title if the user confirms the default with a 'y'.
1573 if user_title.lower() == 'y':
1574 return title
Edward Lesmesae3586b2020-03-23 21:21:14 +00001575 return user_title or title
Edward Lemur5a644f82020-03-18 16:44:57 +00001576
Joanna Wang562481d2023-01-26 21:57:14 +00001577 def _GetRefSpecOptions(self,
1578 options,
1579 change_desc,
1580 multi_change_upload=False,
1581 dogfood_path=False):
1582 # type: (optparse.Values, Sequence[Changelist], Optional[bool],
1583 # Optional[bool]) -> Sequence[str]
Joanna Wanga1abbed2023-01-24 01:41:05 +00001584
1585 # Extra options that can be specified at push time. Doc:
1586 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
1587 refspec_opts = []
1588
1589 # By default, new changes are started in WIP mode, and subsequent patchsets
1590 # don't send email. At any time, passing --send-mail or --send-email will
1591 # mark the change ready and send email for that particular patch.
1592 if options.send_mail:
1593 refspec_opts.append('ready')
1594 refspec_opts.append('notify=ALL')
Joanna Wang562481d2023-01-26 21:57:14 +00001595 elif (not self.GetIssue() and options.squash and not dogfood_path):
Joanna Wanga1abbed2023-01-24 01:41:05 +00001596 refspec_opts.append('wip')
1597 else:
1598 refspec_opts.append('notify=NONE')
1599
1600 # TODO(tandrii): options.message should be posted as a comment if
1601 # --send-mail or --send-email is set on non-initial upload as Rietveld used
1602 # to do it.
1603
1604 # Set options.title in case user was prompted in _GetTitleForUpload and
1605 # _CMDUploadChange needs to be called again.
1606 options.title = self._GetTitleForUpload(
1607 options, multi_change_upload=multi_change_upload)
1608
1609 if options.title:
1610 # Punctuation and whitespace in |title| must be percent-encoded.
1611 refspec_opts.append('m=' +
1612 gerrit_util.PercentEncodeForGitRef(options.title))
1613
1614 if options.private:
1615 refspec_opts.append('private')
1616
1617 if options.topic:
1618 # Documentation on Gerrit topics is here:
1619 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
1620 refspec_opts.append('topic=%s' % options.topic)
1621
1622 if options.enable_auto_submit:
1623 refspec_opts.append('l=Auto-Submit+1')
1624 if options.set_bot_commit:
1625 refspec_opts.append('l=Bot-Commit+1')
1626 if options.use_commit_queue:
1627 refspec_opts.append('l=Commit-Queue+2')
1628 elif options.cq_dry_run:
1629 refspec_opts.append('l=Commit-Queue+1')
1630 elif options.cq_quick_run:
1631 refspec_opts.append('l=Commit-Queue+1')
1632 refspec_opts.append('l=Quick-Run+1')
1633
1634 if change_desc.get_reviewers(tbr_only=True):
1635 score = gerrit_util.GetCodeReviewTbrScore(self.GetGerritHost(),
1636 self.GetGerritProject())
1637 refspec_opts.append('l=Code-Review+%s' % score)
1638
Joanna Wang40497912023-01-24 21:18:16 +00001639 # Gerrit sorts hashtags, so order is not important.
1640 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
1641 # We check GetIssue because we only add hashtags from the
1642 # description on the first upload.
Joanna Wang562481d2023-01-26 21:57:14 +00001643 # TODO(b/265929888): When we fully launch the new path:
1644 # 1) remove fetching hashtags from description alltogether
1645 # 2) Or use descrtiption hashtags for:
1646 # `not (self.GetIssue() and multi_change_upload)`
1647 # 3) Or enabled change description tags for multi and single changes
1648 # by adding them post `git push`.
1649 if not (self.GetIssue() and dogfood_path):
Joanna Wang40497912023-01-24 21:18:16 +00001650 hashtags.update(change_desc.get_hash_tags())
1651 refspec_opts.extend(['hashtag=%s' % t for t in hashtags])
Joanna Wang40497912023-01-24 21:18:16 +00001652
1653 # Note: Reviewers, and ccs are handled individually for each
Joanna Wanga1abbed2023-01-24 01:41:05 +00001654 # branch/change.
1655 return refspec_opts
1656
Joanna Wang6215dd02023-02-07 15:58:03 +00001657 def PrepareSquashedCommit(self, options, parent, end_commit=None):
1658 # type: (optparse.Values, str, Optional[str]) -> _NewUpload()
Joanna Wangb88a4342023-01-24 01:28:22 +00001659 """Create a squashed commit to upload."""
Joanna Wangb88a4342023-01-24 01:28:22 +00001660
1661 if end_commit is None:
1662 end_commit = RunGit(['rev-parse', self.branchref]).strip()
1663
1664 reviewers, ccs, change_desc = self._PrepareChange(options, parent,
1665 end_commit)
1666 latest_tree = RunGit(['rev-parse', end_commit + ':']).strip()
1667 with gclient_utils.temporary_file() as desc_tempfile:
1668 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
1669 commit_to_push = RunGit(
1670 ['commit-tree', latest_tree, '-p', parent, '-F',
1671 desc_tempfile]).strip()
1672
Joanna Wang7603f042023-03-01 22:17:36 +00001673 # Gerrit may or may not update fast enough to return the correct patchset
1674 # number after we push. Get the pre-upload patchset and increment later.
1675 prev_patchset = self.GetMostRecentPatchset(update=False) or 0
Joanna Wang40497912023-01-24 21:18:16 +00001676 return _NewUpload(reviewers, ccs, commit_to_push, end_commit, parent,
Joanna Wang7603f042023-03-01 22:17:36 +00001677 change_desc, prev_patchset)
Joanna Wangb88a4342023-01-24 01:28:22 +00001678
Joanna Wang6215dd02023-02-07 15:58:03 +00001679 def PrepareCherryPickSquashedCommit(self, options, parent):
1680 # type: (optparse.Values, str) -> _NewUpload()
Joanna Wange8523912023-01-21 02:05:40 +00001681 """Create a commit cherry-picked on parent to push."""
1682
Joanna Wang6215dd02023-02-07 15:58:03 +00001683 # The `parent` is what we will cherry-pick on top of.
1684 # The `cherry_pick_base` is the beginning range of what
1685 # we are cherry-picking.
1686 cherry_pick_base = self.GetCommonAncestorWithUpstream()
1687 reviewers, ccs, change_desc = self._PrepareChange(options, cherry_pick_base,
Joanna Wange8523912023-01-21 02:05:40 +00001688 self.branchref)
1689
1690 new_upload_hash = RunGit(['rev-parse', self.branchref]).strip()
1691 latest_tree = RunGit(['rev-parse', self.branchref + ':']).strip()
1692 with gclient_utils.temporary_file() as desc_tempfile:
1693 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
Joanna Wang6215dd02023-02-07 15:58:03 +00001694 commit_to_cp = RunGit([
1695 'commit-tree', latest_tree, '-p', cherry_pick_base, '-F',
1696 desc_tempfile
1697 ]).strip()
Joanna Wange8523912023-01-21 02:05:40 +00001698
Joanna Wang6215dd02023-02-07 15:58:03 +00001699 RunGit(['checkout', '-q', parent])
Joanna Wange8523912023-01-21 02:05:40 +00001700 ret, _out = RunGitWithCode(['cherry-pick', commit_to_cp])
1701 if ret:
1702 RunGit(['cherry-pick', '--abort'])
1703 RunGit(['checkout', '-q', self.branch])
1704 DieWithError('Could not cleanly cherry-pick')
1705
Joanna Wang6215dd02023-02-07 15:58:03 +00001706 commit_to_push = RunGit(['rev-parse', 'HEAD']).strip()
Joanna Wange8523912023-01-21 02:05:40 +00001707 RunGit(['checkout', '-q', self.branch])
1708
Joanna Wang7603f042023-03-01 22:17:36 +00001709 # Gerrit may or may not update fast enough to return the correct patchset
1710 # number after we push. Get the pre-upload patchset and increment later.
1711 prev_patchset = self.GetMostRecentPatchset(update=False) or 0
Joanna Wang6215dd02023-02-07 15:58:03 +00001712 return _NewUpload(reviewers, ccs, commit_to_push, new_upload_hash,
Joanna Wang7603f042023-03-01 22:17:36 +00001713 cherry_pick_base, change_desc, prev_patchset)
Joanna Wange8523912023-01-21 02:05:40 +00001714
Joanna Wangb46232e2023-01-21 01:58:46 +00001715 def _PrepareChange(self, options, parent, end_commit):
1716 # type: (optparse.Values, str, str) ->
1717 # Tuple[Sequence[str], Sequence[str], ChangeDescription]
1718 """Prepares the change to be uploaded."""
1719 self.EnsureCanUploadPatchset(options.force)
1720
1721 files = self.GetAffectedFiles(parent, end_commit=end_commit)
1722 change_desc = self._GetDescriptionForUpload(options, [parent, end_commit],
1723 files)
1724
1725 watchlist = watchlists.Watchlists(settings.GetRoot())
1726 self.ExtendCC(watchlist.GetWatchersForPaths(files))
1727 if not options.bypass_hooks:
1728 hook_results = self.RunHook(committing=False,
1729 may_prompt=not options.force,
1730 verbose=options.verbose,
1731 parallel=options.parallel,
1732 upstream=parent,
1733 description=change_desc.description,
1734 all_files=False)
1735 self.ExtendCC(hook_results['more_cc'])
1736
1737 # Update the change description and ensure we have a Change Id.
1738 if self.GetIssue():
1739 if options.edit_description:
1740 change_desc.prompt()
1741 change_detail = self._GetChangeDetail(['CURRENT_REVISION'])
1742 change_id = change_detail['change_id']
1743 change_desc.ensure_change_id(change_id)
1744
Joanna Wangb46232e2023-01-21 01:58:46 +00001745 else: # No change issue. First time uploading
1746 if not options.force and not options.message_file:
1747 change_desc.prompt()
1748
1749 # Check if user added a change_id in the descripiton.
1750 change_ids = git_footers.get_footer_change_id(change_desc.description)
1751 if len(change_ids) == 1:
1752 change_id = change_ids[0]
1753 else:
1754 change_id = GenerateGerritChangeId(change_desc.description)
1755 change_desc.ensure_change_id(change_id)
1756
1757 if options.preserve_tryjobs:
1758 change_desc.set_preserve_tryjobs()
1759
1760 SaveDescriptionBackup(change_desc)
1761
1762 # Add ccs
1763 ccs = []
Joanna Wangc4ac3022023-01-31 21:19:57 +00001764 # Add default, watchlist, presubmit ccs if this is the initial upload
Joanna Wangb46232e2023-01-21 01:58:46 +00001765 # and CL is not private and auto-ccing has not been disabled.
Joanna Wangc4ac3022023-01-31 21:19:57 +00001766 if not options.private and not options.no_autocc and not self.GetIssue():
Joanna Wangb46232e2023-01-21 01:58:46 +00001767 ccs = self.GetCCList().split(',')
1768 if len(ccs) > 100:
1769 lsc = ('https://chromium.googlesource.com/chromium/src/+/HEAD/docs/'
1770 'process/lsc/lsc_workflow.md')
1771 print('WARNING: This will auto-CC %s users.' % len(ccs))
1772 print('LSC may be more appropriate: %s' % lsc)
1773 print('You can also use the --no-autocc flag to disable auto-CC.')
1774 confirm_or_exit(action='continue')
1775
1776 # Add ccs from the --cc flag.
1777 if options.cc:
1778 ccs.extend(options.cc)
1779
1780 ccs = [email.strip() for email in ccs if email.strip()]
1781 if change_desc.get_cced():
1782 ccs.extend(change_desc.get_cced())
1783
1784 return change_desc.get_reviewers(), ccs, change_desc
1785
Joanna Wang40497912023-01-24 21:18:16 +00001786 def PostUploadUpdates(self, options, new_upload, change_number):
1787 # type: (optparse.Values, _NewUpload, change_number) -> None
1788 """Makes necessary post upload changes to the local and remote cl."""
1789 if not self.GetIssue():
1790 self.SetIssue(change_number)
1791
Joanna Wang7603f042023-03-01 22:17:36 +00001792 self.SetPatchset(new_upload.prev_patchset + 1)
1793
Joanna Wang40497912023-01-24 21:18:16 +00001794 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY,
1795 new_upload.commit_to_push)
1796 self._GitSetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY,
1797 new_upload.new_last_uploaded_commit)
1798
1799 if settings.GetRunPostUploadHook():
1800 self.RunPostUploadHook(options.verbose, new_upload.parent,
1801 new_upload.change_desc.description,
1802 options.no_python2_post_upload_hooks)
1803
1804 if new_upload.reviewers or new_upload.ccs:
1805 gerrit_util.AddReviewers(self.GetGerritHost(),
1806 self._GerritChangeIdentifier(),
1807 reviewers=new_upload.reviewers,
1808 ccs=new_upload.ccs,
1809 notify=bool(options.send_mail))
1810
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001811 def CMDUpload(self, options, git_diff_args, orig_args):
1812 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001813 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001814 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001815 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001816 else:
1817 if self.GetBranch() is None:
1818 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1819
1820 # Default to diffing against common ancestor of upstream branch
1821 base_branch = self.GetCommonAncestorWithUpstream()
1822 git_diff_args = [base_branch, 'HEAD']
1823
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001824 # Fast best-effort checks to abort before running potentially expensive
1825 # hooks if uploading is likely to fail anyway. Passing these checks does
1826 # not guarantee that uploading will not fail.
Edward Lemur125d60a2019-09-13 18:25:41 +00001827 self.EnsureAuthenticated(force=options.force)
1828 self.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001829
1830 # Apply watchlists on upload.
Edward Lemur2c62b332020-03-12 22:12:33 +00001831 watchlist = watchlists.Watchlists(settings.GetRoot())
1832 files = self.GetAffectedFiles(base_branch)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001833 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001834 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001835
Edward Lemur5a644f82020-03-18 16:44:57 +00001836 change_desc = self._GetDescriptionForUpload(options, git_diff_args, files)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001837 if not options.bypass_hooks:
Joanna Wanga8db0cb2023-01-24 15:43:17 +00001838 hook_results = self.RunHook(committing=False,
1839 may_prompt=not options.force,
1840 verbose=options.verbose,
1841 parallel=options.parallel,
1842 upstream=base_branch,
1843 description=change_desc.description,
1844 all_files=False)
Edward Lemur227d5102020-02-25 23:45:35 +00001845 self.ExtendCC(hook_results['more_cc'])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001846
Aaron Gable13101a62018-02-09 13:20:41 -08001847 print_stats(git_diff_args)
Edward Lemura12175c2020-03-09 16:58:26 +00001848 ret = self.CMDUploadChange(
Edward Lemur5a644f82020-03-18 16:44:57 +00001849 options, git_diff_args, custom_cl_base, change_desc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001850 if not ret:
mlcui7a0b4cb2023-01-23 23:14:55 +00001851 if self.GetBranch() is not None:
1852 self._GitSetBranchConfigValue(
1853 LAST_UPLOAD_HASH_CONFIG_KEY,
1854 scm.GIT.ResolveCommit(settings.GetRoot(), 'HEAD'))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001855 # Run post upload hooks, if specified.
1856 if settings.GetRunPostUploadHook():
Brian Sheedy7326ca22022-11-02 18:36:17 +00001857 self.RunPostUploadHook(options.verbose, base_branch,
1858 change_desc.description,
1859 options.no_python2_post_upload_hooks)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001860
1861 # Upload all dependencies if specified.
1862 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001863 print()
1864 print('--dependencies has been specified.')
1865 print('All dependent local branches will be re-uploaded.')
1866 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001867 # Remove the dependencies flag from args so that we do not end up in a
1868 # loop.
1869 orig_args.remove('--dependencies')
Jose Lopes3863fc52020-04-07 17:00:25 +00001870 ret = upload_branch_deps(self, orig_args, options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001871 return ret
1872
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001873 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001874 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001875
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00001876 Issue must have been already uploaded and known. Optionally allows for
1877 updating Quick-Run (QR) state.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001878 """
1879 assert new_state in _CQState.ALL_STATES
1880 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001881 try:
Edward Lemur125d60a2019-09-13 18:25:41 +00001882 vote_map = {
1883 _CQState.NONE: 0,
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00001884 _CQState.QUICK_RUN: 1,
Edward Lemur125d60a2019-09-13 18:25:41 +00001885 _CQState.DRY_RUN: 1,
1886 _CQState.COMMIT: 2,
1887 }
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00001888 if new_state == _CQState.QUICK_RUN:
1889 labels = {
1890 'Commit-Queue': vote_map[_CQState.DRY_RUN],
1891 'Quick-Run': vote_map[_CQState.QUICK_RUN],
1892 }
1893 else:
1894 labels = {'Commit-Queue': vote_map[new_state]}
Edward Lemur125d60a2019-09-13 18:25:41 +00001895 notify = False if new_state == _CQState.DRY_RUN else None
1896 gerrit_util.SetReview(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001897 self.GetGerritHost(), self._GerritChangeIdentifier(),
Edward Lemur125d60a2019-09-13 18:25:41 +00001898 labels=labels, notify=notify)
qyearsley1fdfcb62016-10-24 13:22:03 -07001899 return 0
1900 except KeyboardInterrupt:
1901 raise
1902 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001903 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001904 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001905 ' * Your project has no CQ,\n'
1906 ' * You don\'t have permission to change the CQ state,\n'
1907 ' * There\'s a bug in this code (see stack trace below).\n'
1908 'Consider specifying which bots to trigger manually or asking your '
1909 'project owners for permissions or contacting Chrome Infra at:\n'
1910 'https://www.chromium.org/infra\n\n' %
1911 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001912 # Still raise exception so that stack trace is printed.
1913 raise
1914
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001915 def GetGerritHost(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001916 # Lazy load of configs.
1917 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001918 if self._gerrit_host and '.' not in self._gerrit_host:
1919 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1920 # This happens for internal stuff http://crbug.com/614312.
Edward Lemur79d4f992019-11-11 23:49:02 +00001921 parsed = urllib.parse.urlparse(self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001922 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001923 print('WARNING: using non-https URLs for remote is likely broken\n'
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001924 ' Your current remote is: %s' % self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001925 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1926 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001927 return self._gerrit_host
1928
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001929 def _GetGitHost(self):
1930 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001931 remote_url = self.GetRemoteUrl()
1932 if not remote_url:
1933 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001934 return urllib.parse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001935
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001936 def GetCodereviewServer(self):
1937 if not self._gerrit_server:
1938 # If we're on a branch then get the server potentially associated
1939 # with that branch.
Edward Lemur85153282020-02-14 22:06:29 +00001940 if self.GetIssue() and self.GetBranch():
tandrii5d48c322016-08-18 16:19:37 -07001941 self._gerrit_server = self._GitGetBranchConfigValue(
Edward Lesmes50da7702020-03-30 19:23:43 +00001942 CODEREVIEW_SERVER_CONFIG_KEY)
tandrii5d48c322016-08-18 16:19:37 -07001943 if self._gerrit_server:
Edward Lemur79d4f992019-11-11 23:49:02 +00001944 self._gerrit_host = urllib.parse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001945 if not self._gerrit_server:
1946 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1947 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001948 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001949 parts[0] = parts[0] + '-review'
1950 self._gerrit_host = '.'.join(parts)
1951 self._gerrit_server = 'https://%s' % self._gerrit_host
1952 return self._gerrit_server
1953
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001954 def GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001955 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001956 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001957 if remote_url is None:
Josip906bfde2020-01-31 22:38:49 +00001958 logging.warning('can\'t detect Gerrit project.')
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001959 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001960 project = urllib.parse.urlparse(remote_url).path.strip('/')
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001961 if project.endswith('.git'):
1962 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001963 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1964 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1965 # gitiles/git-over-https protocol. E.g.,
1966 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1967 # as
1968 # https://chromium.googlesource.com/v8/v8
1969 if project.startswith('a/'):
1970 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001971 return project
1972
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001973 def _GerritChangeIdentifier(self):
1974 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1975
1976 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001977 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001978 """
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001979 project = self.GetGerritProject()
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001980 if project:
1981 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1982 # Fall back on still unique, but less efficient change number.
1983 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001984
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001985 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001986 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00001987 if settings.GetGerritSkipEnsureAuthenticated():
1988 # For projects with unusual authentication schemes.
1989 # See http://crbug.com/603378.
1990 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001991
1992 # Check presence of cookies only if using cookies-based auth method.
1993 cookie_auth = gerrit_util.Authenticator.get()
1994 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001995 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001996
Florian Mayerae510e82020-01-30 21:04:48 +00001997 remote_url = self.GetRemoteUrl()
1998 if remote_url is None:
Josip906bfde2020-01-31 22:38:49 +00001999 logging.warning('invalid remote')
Florian Mayerae510e82020-01-30 21:04:48 +00002000 return
Joanna Wang46ffd1b2022-09-16 20:44:44 +00002001 if urllib.parse.urlparse(remote_url).scheme not in ['https', 'sso']:
2002 logging.warning(
2003 'Ignoring branch %(branch)s with non-https/sso remote '
2004 '%(remote)s', {
2005 'branch': self.branch,
2006 'remote': self.GetRemoteUrl()
2007 })
Daniel Chengcf6269b2019-05-18 01:02:12 +00002008 return
2009
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002010 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002011 self.GetCodereviewServer()
2012 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00002013 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002014
2015 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2016 git_auth = cookie_auth.get_auth_header(git_host)
2017 if gerrit_auth and git_auth:
2018 if gerrit_auth == git_auth:
2019 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002020 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
Raul Tambre80ee78e2019-05-06 22:41:05 +00002021 print(
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002022 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002023 ' %s\n'
2024 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002025 ' Consider running the following command:\n'
2026 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002027 ' %s\n'
Raul Tambre80ee78e2019-05-06 22:41:05 +00002028 ' %s' %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002029 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002030 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002031 cookie_auth.get_new_password_message(git_host)))
2032 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002033 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002034 return
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002035
2036 missing = (
2037 ([] if gerrit_auth else [self._gerrit_host]) +
2038 ([] if git_auth else [git_host]))
2039 DieWithError('Credentials for the following hosts are required:\n'
2040 ' %s\n'
2041 'These are read from %s (or legacy %s)\n'
2042 '%s' % (
2043 '\n '.join(missing),
2044 cookie_auth.get_gitcookies_path(),
2045 cookie_auth.get_netrc_path(),
2046 cookie_auth.get_new_password_message(git_host)))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002047
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002048 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002049 if not self.GetIssue():
2050 return
2051
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002052 status = self._GetChangeDetail()['status']
Joanna Wang583ca662022-04-27 21:17:17 +00002053 if status == 'ABANDONED':
Bruce Dawsoneb8426e2022-08-05 23:58:15 +00002054 DieWithError(
2055 'Change %s has been abandoned, new uploads are not allowed' %
2056 (self.GetIssueURL()))
Joanna Wang583ca662022-04-27 21:17:17 +00002057 if status == 'MERGED':
2058 answer = gclient_utils.AskForData(
2059 'Change %s has been submitted, new uploads are not allowed. '
2060 'Would you like to start a new change (Y/n)?' % self.GetIssueURL()
2061 ).lower()
2062 if answer not in ('y', ''):
2063 DieWithError('New uploads are not allowed.')
2064 self.SetIssue()
2065 return
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002066
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002067 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2068 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2069 # Apparently this check is not very important? Otherwise get_auth_email
2070 # could have been added to other implementations of Authenticator.
2071 cookies_auth = gerrit_util.Authenticator.get()
2072 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002073 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002074
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002075 cookies_user = cookies_auth.get_auth_email(self.GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002076 if self.GetIssueOwner() == cookies_user:
2077 return
2078 logging.debug('change %s owner is %s, cookies user is %s',
2079 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002080 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002081 # so ask what Gerrit thinks of this user.
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002082 details = gerrit_util.GetAccountDetails(self.GetGerritHost(), 'self')
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002083 if details['email'] == self.GetIssueOwner():
2084 return
2085 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002086 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002087 'as %s.\n'
2088 'Uploading may fail due to lack of permissions.' %
2089 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2090 confirm_or_exit(action='upload')
2091
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002092 def GetStatus(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002093 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002094 or CQ status, assuming adherence to a common workflow.
2095
2096 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002097 * 'error' - error from review tool (including deleted issues)
2098 * 'unsent' - no reviewers added
2099 * 'waiting' - waiting for review
2100 * 'reply' - waiting for uploader to reply to review
2101 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002102 * 'dry-run' - dry-running in the CQ
2103 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07002104 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002105 """
2106 if not self.GetIssue():
2107 return None
2108
2109 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002110 data = self._GetChangeDetail([
2111 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Edward Lemur79d4f992019-11-11 23:49:02 +00002112 except GerritChangeNotExists:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002113 return 'error'
2114
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002115 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002116 return 'closed'
2117
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002118 cq_label = data['labels'].get('Commit-Queue', {})
2119 max_cq_vote = 0
2120 for vote in cq_label.get('all', []):
2121 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
2122 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002123 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002124 if max_cq_vote == 1:
2125 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002126
Aaron Gable9ab38c62017-04-06 14:36:33 -07002127 if data['labels'].get('Code-Review', {}).get('approved'):
2128 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002129
2130 if not data.get('reviewers', {}).get('REVIEWER', []):
2131 return 'unsent'
2132
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002133 owner = data['owner'].get('_account_id')
Edward Lemur79d4f992019-11-11 23:49:02 +00002134 messages = sorted(data.get('messages', []), key=lambda m: m.get('date'))
Andrii Shyshkalov8aa9d622020-03-10 19:15:35 +00002135 while messages:
2136 m = messages.pop()
Andrii Shyshkalov899785a2021-07-09 12:45:37 +00002137 if (m.get('tag', '').startswith('autogenerated:cq') or
2138 m.get('tag', '').startswith('autogenerated:cv')):
2139 # Ignore replies from LUCI CV/CQ.
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002140 continue
Andrii Shyshkalov8aa9d622020-03-10 19:15:35 +00002141 if m.get('author', {}).get('_account_id') == owner:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002142 # Most recent message was by owner.
2143 return 'waiting'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002144
2145 # Some reply from non-owner.
2146 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002147
2148 # Somehow there are no messages even though there are reviewers.
2149 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002150
Gavin Mak4e5e3992022-11-14 22:40:12 +00002151 def GetMostRecentPatchset(self, update=True):
Edward Lemur6c6827c2020-02-06 21:15:18 +00002152 if not self.GetIssue():
2153 return None
2154
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002155 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002156 patchset = data['revisions'][data['current_revision']]['_number']
Gavin Mak4e5e3992022-11-14 22:40:12 +00002157 if update:
2158 self.SetPatchset(patchset)
Aaron Gablee8856ee2017-12-07 12:41:46 -08002159 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002160
Gavin Makf35a9eb2022-11-17 18:34:36 +00002161 def _IsPatchsetRangeSignificant(self, lower, upper):
2162 """Returns True if the inclusive range of patchsets contains any reworks or
2163 rebases."""
2164 if not self.GetIssue():
2165 return False
2166
2167 data = self._GetChangeDetail(['ALL_REVISIONS'])
2168 ps_kind = {}
2169 for rev_info in data.get('revisions', {}).values():
2170 ps_kind[rev_info['_number']] = rev_info.get('kind', '')
2171
2172 for ps in range(lower, upper + 1):
2173 assert ps in ps_kind, 'expected patchset %d in change detail' % ps
2174 if ps_kind[ps] not in ('NO_CHANGE', 'NO_CODE_CHANGE'):
2175 return True
2176 return False
2177
Gavin Make61ccc52020-11-13 00:12:57 +00002178 def GetMostRecentDryRunPatchset(self):
2179 """Get patchsets equivalent to the most recent patchset and return
2180 the patchset with the latest dry run. If none have been dry run, return
2181 the latest patchset."""
2182 if not self.GetIssue():
2183 return None
2184
2185 data = self._GetChangeDetail(['ALL_REVISIONS'])
2186 patchset = data['revisions'][data['current_revision']]['_number']
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002187 dry_run = {int(m['_revision_number'])
2188 for m in data.get('messages', [])
2189 if m.get('tag', '').endswith('dry-run')}
Gavin Make61ccc52020-11-13 00:12:57 +00002190
2191 for revision_info in sorted(data.get('revisions', {}).values(),
2192 key=lambda c: c['_number'], reverse=True):
2193 if revision_info['_number'] in dry_run:
2194 patchset = revision_info['_number']
2195 break
2196 if revision_info.get('kind', '') not in \
2197 ('NO_CHANGE', 'NO_CODE_CHANGE', 'TRIVIAL_REBASE'):
2198 break
2199 self.SetPatchset(patchset)
2200 return patchset
2201
Aaron Gable636b13f2017-07-14 10:42:48 -07002202 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002203 gerrit_util.SetReview(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002204 self.GetGerritHost(), self._GerritChangeIdentifier(),
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002205 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002206
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002207 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002208 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002209 # CURRENT_REVISION is included to get the latest patchset so that
2210 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002211 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002212 options=['MESSAGES', 'DETAILED_ACCOUNTS',
2213 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002214 file_comments = gerrit_util.GetChangeComments(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002215 self.GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002216 robot_file_comments = gerrit_util.GetChangeRobotComments(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002217 self.GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002218
2219 # Add the robot comments onto the list of comments, but only
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +00002220 # keep those that are from the latest patchset.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002221 latest_patch_set = self.GetMostRecentPatchset()
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002222 for path, robot_comments in robot_file_comments.items():
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002223 line_comments = file_comments.setdefault(path, [])
2224 line_comments.extend(
2225 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002226
2227 # Build dictionary of file comments for easy access and sorting later.
2228 # {author+date: {path: {patchset: {line: url+message}}}}
2229 comments = collections.defaultdict(
2230 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
Andrii Shyshkalova3762a92020-11-25 10:20:42 +00002231
2232 server = self.GetCodereviewServer()
2233 if server in _KNOWN_GERRIT_TO_SHORT_URLS:
2234 # /c/ is automatically added by short URL server.
2235 url_prefix = '%s/%s' % (_KNOWN_GERRIT_TO_SHORT_URLS[server],
2236 self.GetIssue())
2237 else:
2238 url_prefix = '%s/c/%s' % (server, self.GetIssue())
2239
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002240 for path, line_comments in file_comments.items():
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002241 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002242 tag = comment.get('tag', '')
2243 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002244 continue
2245 key = (comment['author']['email'], comment['updated'])
2246 if comment.get('side', 'REVISION') == 'PARENT':
2247 patchset = 'Base'
2248 else:
2249 patchset = 'PS%d' % comment['patch_set']
2250 line = comment.get('line', 0)
Andrii Shyshkalova3762a92020-11-25 10:20:42 +00002251 url = ('%s/%s/%s#%s%s' %
2252 (url_prefix, comment['patch_set'], path,
2253 'b' if comment.get('side') == 'PARENT' else '',
2254 str(line) if line else ''))
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002255 comments[key][path][patchset][line] = (url, comment['message'])
2256
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002257 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002258 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002259 summary = self._BuildCommentSummary(msg, comments, readable)
2260 if summary:
2261 summaries.append(summary)
2262 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002263
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002264 @staticmethod
2265 def _BuildCommentSummary(msg, comments, readable):
Josip Sokcevic266129c2021-11-09 00:22:00 +00002266 if 'email' not in msg['author']:
2267 # Some bot accounts may not have an email associated.
2268 return None
2269
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002270 key = (msg['author']['email'], msg['date'])
2271 # Don't bother showing autogenerated messages that don't have associated
2272 # file or line comments. this will filter out most autogenerated
2273 # messages, but will keep robot comments like those from Tricium.
2274 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
2275 if is_autogenerated and not comments.get(key):
2276 return None
2277 message = msg['message']
2278 # Gerrit spits out nanoseconds.
2279 assert len(msg['date'].split('.')[-1]) == 9
2280 date = datetime.datetime.strptime(msg['date'][:-3],
2281 '%Y-%m-%d %H:%M:%S.%f')
2282 if key in comments:
2283 message += '\n'
2284 for path, patchsets in sorted(comments.get(key, {}).items()):
2285 if readable:
2286 message += '\n%s' % path
2287 for patchset, lines in sorted(patchsets.items()):
2288 for line, (url, content) in sorted(lines.items()):
2289 if line:
2290 line_str = 'Line %d' % line
2291 path_str = '%s:%d:' % (path, line)
2292 else:
2293 line_str = 'File comment'
2294 path_str = '%s:0:' % path
2295 if readable:
2296 message += '\n %s, %s: %s' % (patchset, line_str, url)
2297 message += '\n %s\n' % content
2298 else:
2299 message += '\n%s ' % path_str
2300 message += '\n%s\n' % content
2301
2302 return _CommentSummary(
2303 date=date,
2304 message=message,
2305 sender=msg['author']['email'],
2306 autogenerated=is_autogenerated,
2307 # These could be inferred from the text messages and correlated with
2308 # Code-Review label maximum, however this is not reliable.
2309 # Leaving as is until the need arises.
2310 approval=False,
2311 disapproval=False,
2312 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002313
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002314 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002315 gerrit_util.AbandonChange(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002316 self.GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002317
Xinan Lin1bd4ffa2021-07-28 00:54:22 +00002318 def SubmitIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002319 gerrit_util.SubmitChange(
Xinan Lin1bd4ffa2021-07-28 00:54:22 +00002320 self.GetGerritHost(), self._GerritChangeIdentifier())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002321
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002322 def _GetChangeDetail(self, options=None):
2323 """Returns details of associated Gerrit change and caching results."""
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002324 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002325 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002326
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002327 # Optimization to avoid multiple RPCs:
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002328 if 'CURRENT_REVISION' in options or 'ALL_REVISIONS' in options:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002329 options.append('CURRENT_COMMIT')
2330
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002331 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002332 cache_key = str(self.GetIssue())
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002333 options_set = frozenset(o.upper() for o in options)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002334
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002335 for cached_options_set, data in self._detail_cache.get(cache_key, []):
2336 # Assumption: data fetched before with extra options is suitable
2337 # for return for a smaller set of options.
2338 # For example, if we cached data for
2339 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2340 # and request is for options=[CURRENT_REVISION],
2341 # THEN we can return prior cached data.
2342 if options_set.issubset(cached_options_set):
2343 return data
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002344
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002345 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002346 data = gerrit_util.GetChangeDetail(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002347 self.GetGerritHost(), self._GerritChangeIdentifier(), options_set)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002348 except gerrit_util.GerritError as e:
2349 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002350 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002351 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002352
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002353 self._detail_cache.setdefault(cache_key, []).append((options_set, data))
tandriic2405f52016-10-10 08:13:15 -07002354 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002355
Gavin Mak4e5e3992022-11-14 22:40:12 +00002356 def _GetChangeCommit(self, revision='current'):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002357 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002358 try:
Gavin Mak4e5e3992022-11-14 22:40:12 +00002359 data = gerrit_util.GetChangeCommit(self.GetGerritHost(),
2360 self._GerritChangeIdentifier(),
2361 revision)
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002362 except gerrit_util.GerritError as e:
2363 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002364 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002365 raise
agable32978d92016-11-01 12:55:02 -07002366 return data
2367
Karen Qian40c19422019-03-13 21:28:29 +00002368 def _IsCqConfigured(self):
2369 detail = self._GetChangeDetail(['LABELS'])
Andrii Shyshkalov8effa4d2020-01-21 13:23:36 +00002370 return u'Commit-Queue' in detail.get('labels', {})
Karen Qian40c19422019-03-13 21:28:29 +00002371
Saagar Sanghavi03b15132020-08-10 16:43:41 +00002372 def CMDLand(self, force, bypass_hooks, verbose, parallel, resultdb, realm):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002373 if git_common.is_dirty_git_tree('land'):
2374 return 1
Karen Qian40c19422019-03-13 21:28:29 +00002375
tandriid60367b2016-06-22 05:25:12 -07002376 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00002377 if not force and self._IsCqConfigured():
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002378 confirm_or_exit('\nIt seems this repository has a CQ, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002379 'which can test and land changes for you. '
2380 'Are you sure you wish to bypass it?\n',
2381 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002382 differs = True
Gavin Makbe2e9262022-11-08 23:41:55 +00002383 last_upload = self._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002384 # Note: git diff outputs nothing if there is no diff.
2385 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002386 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002387 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002388 if detail['current_revision'] == last_upload:
2389 differs = False
2390 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002391 print('WARNING: Local branch contents differ from latest uploaded '
2392 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002393 if differs:
2394 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002395 confirm_or_exit(
2396 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2397 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002398 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002399 elif not bypass_hooks:
Edward Lemur227d5102020-02-25 23:45:35 +00002400 upstream = self.GetCommonAncestorWithUpstream()
2401 if self.GetIssue():
2402 description = self.FetchDescription()
2403 else:
Edward Lemura12175c2020-03-09 16:58:26 +00002404 description = _create_description_from_log([upstream])
Edward Lemur227d5102020-02-25 23:45:35 +00002405 self.RunHook(
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002406 committing=True,
2407 may_prompt=not force,
2408 verbose=verbose,
Edward Lemur227d5102020-02-25 23:45:35 +00002409 parallel=parallel,
2410 upstream=upstream,
2411 description=description,
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00002412 all_files=False,
Saagar Sanghavi03b15132020-08-10 16:43:41 +00002413 resultdb=resultdb,
2414 realm=realm)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002415
Xinan Lin1bd4ffa2021-07-28 00:54:22 +00002416 self.SubmitIssue()
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002417 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002418 links = self._GetChangeCommit().get('web_links', [])
2419 for link in links:
Michael Mosse371c642021-09-29 16:41:04 +00002420 if link.get('name') in ['gitiles', 'browse'] and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002421 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002422 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002423 return 0
2424
Bruce Dawsonf362f6f2021-02-18 23:15:17 +00002425 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force,
2426 newbranch):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002427 assert parsed_issue_arg.valid
2428
Edward Lemur125d60a2019-09-13 18:25:41 +00002429 self.issue = parsed_issue_arg.issue
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002430
2431 if parsed_issue_arg.hostname:
2432 self._gerrit_host = parsed_issue_arg.hostname
2433 self._gerrit_server = 'https://%s' % self._gerrit_host
2434
tandriic2405f52016-10-10 08:13:15 -07002435 try:
2436 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002437 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002438 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002439
2440 if not parsed_issue_arg.patchset:
2441 # Use current revision by default.
2442 revision_info = detail['revisions'][detail['current_revision']]
2443 patchset = int(revision_info['_number'])
2444 else:
2445 patchset = parsed_issue_arg.patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002446 for revision_info in detail['revisions'].values():
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002447 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2448 break
2449 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002450 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002451 (parsed_issue_arg.patchset, self.GetIssue()))
2452
Edward Lemur125d60a2019-09-13 18:25:41 +00002453 remote_url = self.GetRemoteUrl()
Aaron Gable697a91b2018-01-19 15:20:15 -08002454 if remote_url.endswith('.git'):
2455 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002456 remote_url = remote_url.rstrip('/')
2457
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002458 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002459 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002460
2461 if remote_url != fetch_info['url']:
2462 DieWithError('Trying to patch a change from %s but this repo appears '
2463 'to be %s.' % (fetch_info['url'], remote_url))
2464
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002465 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002466
Joanna Wangc023a632023-01-26 17:59:25 +00002467 # Set issue immediately in case the cherry-pick fails, which happens
2468 # when resolving conflicts.
2469 if self.GetBranch():
Bruce Dawsonf362f6f2021-02-18 23:15:17 +00002470 self.SetIssue(parsed_issue_arg.issue)
2471
Aaron Gable62619a32017-06-16 08:22:09 -07002472 if force:
2473 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2474 print('Checked out commit for change %i patchset %i locally' %
2475 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002476 elif nocommit:
2477 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2478 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002479 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002480 RunGit(['cherry-pick', 'FETCH_HEAD'])
2481 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002482 (parsed_issue_arg.issue, patchset))
2483 print('Note: this created a local commit which does not have '
2484 'the same hash as the one uploaded for review. This will make '
2485 'uploading changes based on top of this branch difficult.\n'
2486 'If you want to do that, use "git cl patch --force" instead.')
2487
Stefan Zagerd08043c2017-10-12 12:07:02 -07002488 if self.GetBranch():
Stefan Zagerd08043c2017-10-12 12:07:02 -07002489 self.SetPatchset(patchset)
Edward Lesmes50da7702020-03-30 19:23:43 +00002490 fetched_hash = scm.GIT.ResolveCommit(settings.GetRoot(), 'FETCH_HEAD')
Gavin Makbe2e9262022-11-08 23:41:55 +00002491 self._GitSetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY, fetched_hash)
2492 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY, fetched_hash)
Stefan Zagerd08043c2017-10-12 12:07:02 -07002493 else:
2494 print('WARNING: You are in detached HEAD state.\n'
2495 'The patch has been applied to your checkout, but you will not be '
2496 'able to upload a new patch set to the gerrit issue.\n'
2497 'Try using the \'-b\' option if you would like to work on a '
2498 'branch and/or upload a new patch set.')
2499
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002500 return 0
2501
Joanna Wang18de1f62023-01-21 01:24:24 +00002502 @staticmethod
2503 def _GerritCommitMsgHookCheck(offer_removal):
2504 # type: (bool) -> None
2505 """Checks for the gerrit's commit-msg hook and removes it if necessary."""
tandrii16e0b4e2016-06-07 10:34:28 -07002506 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2507 if not os.path.exists(hook):
2508 return
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002509 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
2510 # custom developer-made one.
tandrii16e0b4e2016-06-07 10:34:28 -07002511 data = gclient_utils.FileRead(hook)
2512 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2513 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002514 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002515 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002516 'and may interfere with it in subtle ways.\n'
2517 'We recommend you remove the commit-msg hook.')
2518 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002519 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002520 gclient_utils.rm_file_or_tree(hook)
2521 print('Gerrit commit-msg hook removed.')
2522 else:
2523 print('OK, will keep Gerrit commit-msg hook in place.')
2524
Edward Lemur1b52d872019-05-09 21:12:12 +00002525 def _CleanUpOldTraces(self):
2526 """Keep only the last |MAX_TRACES| traces."""
2527 try:
2528 traces = sorted([
2529 os.path.join(TRACES_DIR, f)
2530 for f in os.listdir(TRACES_DIR)
2531 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2532 and not f.startswith('tmp'))
2533 ])
2534 traces_to_delete = traces[:-MAX_TRACES]
2535 for trace in traces_to_delete:
Daniel Chengcf6269b2019-05-18 01:02:12 +00002536 os.remove(trace)
Edward Lemur1b52d872019-05-09 21:12:12 +00002537 except OSError:
2538 print('WARNING: Failed to remove old git traces from\n'
2539 ' %s'
2540 'Consider removing them manually.' % TRACES_DIR)
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002541
Edward Lemur5737f022019-05-17 01:24:00 +00002542 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
Edward Lemur1b52d872019-05-09 21:12:12 +00002543 """Zip and write the git push traces stored in traces_dir."""
2544 gclient_utils.safe_makedirs(TRACES_DIR)
Edward Lemur1b52d872019-05-09 21:12:12 +00002545 traces_zip = trace_name + '-traces'
2546 traces_readme = trace_name + '-README'
Michael Mosse7f0b4c2019-05-08 04:36:24 +00002547 # Create a temporary dir to store git config and gitcookies in. It will be
2548 # compressed and stored next to the traces.
2549 git_info_dir = tempfile.mkdtemp()
Edward Lemur1b52d872019-05-09 21:12:12 +00002550 git_info_zip = trace_name + '-git-info'
2551
Josip Sokcevic5e18b602020-04-23 21:47:00 +00002552 git_push_metadata['now'] = datetime_now().strftime('%Y-%m-%dT%H:%M:%S.%f')
sangwoo.ko7a614332019-05-22 02:46:19 +00002553
Edward Lemur1b52d872019-05-09 21:12:12 +00002554 git_push_metadata['trace_name'] = trace_name
2555 gclient_utils.FileWrite(
2556 traces_readme, TRACES_README_FORMAT % git_push_metadata)
2557
2558 # Keep only the first 6 characters of the git hashes on the packet
2559 # trace. This greatly decreases size after compression.
2560 packet_traces = os.path.join(traces_dir, 'trace-packet')
2561 if os.path.isfile(packet_traces):
2562 contents = gclient_utils.FileRead(packet_traces)
2563 gclient_utils.FileWrite(
2564 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2565 shutil.make_archive(traces_zip, 'zip', traces_dir)
2566
2567 # Collect and compress the git config and gitcookies.
2568 git_config = RunGit(['config', '-l'])
2569 gclient_utils.FileWrite(
2570 os.path.join(git_info_dir, 'git-config'),
2571 git_config)
2572
2573 cookie_auth = gerrit_util.Authenticator.get()
2574 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2575 gitcookies_path = cookie_auth.get_gitcookies_path()
2576 if os.path.isfile(gitcookies_path):
2577 gitcookies = gclient_utils.FileRead(gitcookies_path)
2578 gclient_utils.FileWrite(
2579 os.path.join(git_info_dir, 'gitcookies'),
2580 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2581 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2582
Edward Lemur1b52d872019-05-09 21:12:12 +00002583 gclient_utils.rmtree(git_info_dir)
2584
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002585 def _RunGitPushWithTraces(self,
2586 refspec,
2587 refspec_opts,
2588 git_push_metadata,
2589 git_push_options=None):
Edward Lemur1b52d872019-05-09 21:12:12 +00002590 """Run git push and collect the traces resulting from the execution."""
2591 # Create a temporary directory to store traces in. Traces will be compressed
2592 # and stored in a 'traces' dir inside depot_tools.
2593 traces_dir = tempfile.mkdtemp()
Edward Lemur5737f022019-05-17 01:24:00 +00002594 trace_name = os.path.join(
2595 TRACES_DIR, datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
Edward Lemur0f58ae42019-04-30 17:24:12 +00002596
2597 env = os.environ.copy()
2598 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2599 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Jonathan Nieder9779b142019-05-29 23:19:29 +00002600 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002601 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2602 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2603 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2604
2605 try:
2606 push_returncode = 0
Edward Lemur1b52d872019-05-09 21:12:12 +00002607 remote_url = self.GetRemoteUrl()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002608 before_push = time_time()
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002609 push_cmd = ['git', 'push', remote_url, refspec]
2610 if git_push_options:
2611 for opt in git_push_options:
2612 push_cmd.extend(['-o', opt])
2613
Edward Lemur0f58ae42019-04-30 17:24:12 +00002614 push_stdout = gclient_utils.CheckCallAndFilter(
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002615 push_cmd,
Edward Lemur0f58ae42019-04-30 17:24:12 +00002616 env=env,
2617 print_stdout=True,
2618 # Flush after every line: useful for seeing progress when running as
2619 # recipe.
2620 filter_fn=lambda _: sys.stdout.flush())
Edward Lemur79d4f992019-11-11 23:49:02 +00002621 push_stdout = push_stdout.decode('utf-8', 'replace')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002622 except subprocess2.CalledProcessError as e:
2623 push_returncode = e.returncode
Aravind Vasudevanc9508582022-10-18 03:07:41 +00002624 if 'blocked keyword' in str(e.stdout) or 'banned word' in str(e.stdout):
Josip Sokcevic740825e2021-05-12 18:28:34 +00002625 raise GitPushError(
2626 'Failed to create a change, very likely due to blocked keyword. '
2627 'Please examine output above for the reason of the failure.\n'
2628 'If this is a false positive, you can try to bypass blocked '
2629 'keyword by using push option '
2630 '-o uploadvalidator~skip, e.g.:\n'
2631 'git cl upload -o uploadvalidator~skip\n\n'
2632 'If git-cl is not working correctly, file a bug under the '
2633 'Infra>SDK component.')
Josip Sokcevic54e30e72022-02-10 22:32:24 +00002634 if 'git push -o nokeycheck' in str(e.stdout):
2635 raise GitPushError(
2636 'Failed to create a change, very likely due to a private key being '
2637 'detected. Please examine output above for the reason of the '
2638 'failure.\n'
2639 'If this is a false positive, you can try to bypass private key '
2640 'detection by using push option '
2641 '-o nokeycheck, e.g.:\n'
2642 'git cl upload -o nokeycheck\n\n'
2643 'If git-cl is not working correctly, file a bug under the '
2644 'Infra>SDK component.')
Josip Sokcevic740825e2021-05-12 18:28:34 +00002645
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002646 raise GitPushError(
2647 'Failed to create a change. Please examine output above for the '
2648 'reason of the failure.\n'
Josip Sokcevic7386a1e2021-02-12 19:00:34 +00002649 'For emergencies, Googlers can escalate to '
2650 'go/gob-support or go/notify#gob\n'
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002651 'Hint: run command below to diagnose common Git/Gerrit '
2652 'credential problems:\n'
2653 ' git cl creds-check\n'
2654 '\n'
2655 'If git-cl is not working correctly, file a bug under the Infra>SDK '
2656 'component including the files below.\n'
2657 'Review the files before upload, since they might contain sensitive '
2658 'information.\n'
2659 'Set the Restrict-View-Google label so that they are not publicly '
2660 'accessible.\n' + TRACES_MESSAGE % {'trace_name': trace_name})
Edward Lemur0f58ae42019-04-30 17:24:12 +00002661 finally:
2662 execution_time = time_time() - before_push
2663 metrics.collector.add_repeated('sub_commands', {
2664 'command': 'git push',
2665 'execution_time': execution_time,
2666 'exit_code': push_returncode,
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002667 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
Edward Lemur0f58ae42019-04-30 17:24:12 +00002668 })
2669
Edward Lemur1b52d872019-05-09 21:12:12 +00002670 git_push_metadata['execution_time'] = execution_time
2671 git_push_metadata['exit_code'] = push_returncode
Edward Lemur5737f022019-05-17 01:24:00 +00002672 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002673
Edward Lemur1b52d872019-05-09 21:12:12 +00002674 self._CleanUpOldTraces()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002675 gclient_utils.rmtree(traces_dir)
2676
2677 return push_stdout
2678
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002679 def CMDUploadChange(self, options, git_diff_args, custom_cl_base,
2680 change_desc):
2681 """Upload the current branch to Gerrit, retry if new remote HEAD is
2682 found. options and change_desc may be mutated."""
Josip Sokcevicb631a882021-01-06 18:18:10 +00002683 remote, remote_branch = self.GetRemoteBranch()
2684 branch = GetTargetRef(remote, remote_branch, options.target_branch)
2685
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002686 try:
2687 return self._CMDUploadChange(options, git_diff_args, custom_cl_base,
Josip Sokcevicb631a882021-01-06 18:18:10 +00002688 change_desc, branch)
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002689 except GitPushError as e:
Josip Sokcevicb631a882021-01-06 18:18:10 +00002690 # Repository might be in the middle of transition to main branch as
2691 # default, and uploads to old default might be blocked.
2692 if remote_branch not in [DEFAULT_OLD_BRANCH, DEFAULT_NEW_BRANCH]:
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002693 DieWithError(str(e), change_desc)
2694
Josip Sokcevicb631a882021-01-06 18:18:10 +00002695 project_head = gerrit_util.GetProjectHead(self._gerrit_host,
2696 self.GetGerritProject())
2697 if project_head == branch:
2698 DieWithError(str(e), change_desc)
2699 branch = project_head
2700
2701 print("WARNING: Fetching remote state and retrying upload to default "
2702 "branch...")
2703 RunGit(['fetch', '--prune', remote])
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002704 options.edit_description = False
2705 options.force = True
2706 try:
Josip Sokcevicb631a882021-01-06 18:18:10 +00002707 self._CMDUploadChange(options, git_diff_args, custom_cl_base,
2708 change_desc, branch)
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002709 except GitPushError as e:
2710 DieWithError(str(e), change_desc)
2711
2712 def _CMDUploadChange(self, options, git_diff_args, custom_cl_base,
Josip Sokcevicb631a882021-01-06 18:18:10 +00002713 change_desc, branch):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002714 """Upload the current branch to Gerrit."""
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002715 if options.squash:
Joanna Wangc4ac3022023-01-31 21:19:57 +00002716 Changelist._GerritCommitMsgHookCheck(offer_removal=not options.force)
Gavin Mak4e5e3992022-11-14 22:40:12 +00002717 external_parent = None
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002718 if self.GetIssue():
Josipe827b0f2020-01-30 00:07:20 +00002719 # User requested to change description
2720 if options.edit_description:
Josipe827b0f2020-01-30 00:07:20 +00002721 change_desc.prompt()
Gavin Mak4e5e3992022-11-14 22:40:12 +00002722 change_detail = self._GetChangeDetail(['CURRENT_REVISION'])
2723 change_id = change_detail['change_id']
Edward Lemur5a644f82020-03-18 16:44:57 +00002724 change_desc.ensure_change_id(change_id)
Gavin Mak4e5e3992022-11-14 22:40:12 +00002725
2726 # Check if changes outside of this workspace have been uploaded.
2727 current_rev = change_detail['current_revision']
2728 last_uploaded_rev = self._GitGetBranchConfigValue(
2729 GERRIT_SQUASH_HASH_CONFIG_KEY)
2730 if last_uploaded_rev and current_rev != last_uploaded_rev:
2731 external_parent = self._UpdateWithExternalChanges()
Aaron Gableb56ad332017-01-06 15:24:31 -08002732 else: # if not self.GetIssue()
Gavin Mak68e6cf32021-01-25 18:24:08 +00002733 if not options.force and not options.message_file:
Anthony Polito8b955342019-09-24 19:01:36 +00002734 change_desc.prompt()
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002735 change_ids = git_footers.get_footer_change_id(change_desc.description)
Edward Lemur5a644f82020-03-18 16:44:57 +00002736 if len(change_ids) == 1:
2737 change_id = change_ids[0]
2738 else:
2739 change_id = GenerateGerritChangeId(change_desc.description)
2740 change_desc.ensure_change_id(change_id)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002741
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002742 if options.preserve_tryjobs:
2743 change_desc.set_preserve_tryjobs()
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002744
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002745 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Gavin Mak4e5e3992022-11-14 22:40:12 +00002746 parent = external_parent or self._ComputeParent(
Edward Lemur5a644f82020-03-18 16:44:57 +00002747 remote, upstream_branch, custom_cl_base, options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002748 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Edward Lemur1773f372020-02-22 00:27:14 +00002749 with gclient_utils.temporary_file() as desc_tempfile:
2750 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
2751 ref_to_push = RunGit(
2752 ['commit-tree', tree, '-p', parent, '-F', desc_tempfile]).strip()
Anthony Polito8b955342019-09-24 19:01:36 +00002753 else: # if not options.squash
Gregory Nisbet48d9e1e2021-04-15 23:35:54 +00002754 if options.no_add_changeid:
2755 pass
2756 else: # adding Change-Ids is okay.
2757 if not git_footers.get_footer_change_id(change_desc.description):
2758 DownloadGerritHook(False)
2759 change_desc.set_description(
2760 self._AddChangeIdToCommitMessage(change_desc.description,
2761 git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002762 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002763 # For no-squash mode, we assume the remote called "origin" is the one we
2764 # want. It is not worthwhile to support different workflows for
2765 # no-squash mode.
2766 parent = 'origin/%s' % branch
Gregory Nisbet48d9e1e2021-04-15 23:35:54 +00002767 # attempt to extract the changeid from the current description
2768 # fail informatively if not possible.
2769 change_id_candidates = git_footers.get_footer_change_id(
2770 change_desc.description)
2771 if not change_id_candidates:
2772 DieWithError("Unable to extract change-id from message.")
2773 change_id = change_id_candidates[0]
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002774
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002775 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002776 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2777 ref_to_push)]).splitlines()
2778 if len(commits) > 1:
2779 print('WARNING: This will upload %d commits. Run the following command '
2780 'to see which commits will be uploaded: ' % len(commits))
2781 print('git log %s..%s' % (parent, ref_to_push))
2782 print('You can also use `git squash-branch` to squash these into a '
2783 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002784 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002785
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002786 reviewers = sorted(change_desc.get_reviewers())
Edward Lemur4508b422019-10-03 21:56:35 +00002787 cc = []
Joanna Wangc4ac3022023-01-31 21:19:57 +00002788 # Add default, watchlist, presubmit ccs if this is the initial upload
2789 # and CL is not private and auto-ccing has not been disabled.
2790 if not options.private and not options.no_autocc and not self.GetIssue():
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002791 cc = self.GetCCList().split(',')
Gavin Makb1c08f62021-04-01 18:05:58 +00002792 if len(cc) > 100:
2793 lsc = ('https://chromium.googlesource.com/chromium/src/+/HEAD/docs/'
2794 'process/lsc/lsc_workflow.md')
2795 print('WARNING: This will auto-CC %s users.' % len(cc))
2796 print('LSC may be more appropriate: %s' % lsc)
2797 print('You can also use the --no-autocc flag to disable auto-CC.')
2798 confirm_or_exit(action='continue')
Edward Lemur4508b422019-10-03 21:56:35 +00002799 # Add cc's from the --cc flag.
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002800 if options.cc:
2801 cc.extend(options.cc)
Edward Lemur79d4f992019-11-11 23:49:02 +00002802 cc = [email.strip() for email in cc if email.strip()]
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002803 if change_desc.get_cced():
2804 cc.extend(change_desc.get_cced())
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002805 if self.GetGerritHost() == 'chromium-review.googlesource.com':
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002806 valid_accounts = set(reviewers + cc)
2807 # TODO(crbug/877717): relax this for all hosts.
2808 else:
2809 valid_accounts = gerrit_util.ValidAccounts(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002810 self.GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002811 logging.info('accounts %s are recognized, %s invalid',
2812 sorted(valid_accounts),
2813 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002814
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002815 # Extra options that can be specified at push time. Doc:
2816 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Joanna Wanga1abbed2023-01-24 01:41:05 +00002817 refspec_opts = self._GetRefSpecOptions(options, change_desc)
agablec6787972016-09-09 16:13:34 -07002818
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002819 for r in sorted(reviewers):
2820 if r in valid_accounts:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002821 refspec_opts.append('r=%s' % r)
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002822 reviewers.remove(r)
2823 else:
2824 # TODO(tandrii): this should probably be a hard failure.
2825 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2826 % r)
2827 for c in sorted(cc):
2828 # refspec option will be rejected if cc doesn't correspond to an
2829 # account, even though REST call to add such arbitrary cc may succeed.
2830 if c in valid_accounts:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002831 refspec_opts.append('cc=%s' % c)
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002832 cc.remove(c)
2833
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002834 refspec_suffix = ''
2835 if refspec_opts:
2836 refspec_suffix = '%' + ','.join(refspec_opts)
2837 assert ' ' not in refspec_suffix, (
2838 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2839 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002840
Edward Lemur1b52d872019-05-09 21:12:12 +00002841 git_push_metadata = {
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002842 'gerrit_host': self.GetGerritHost(),
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002843 'title': options.title or '<untitled>',
Edward Lemur1b52d872019-05-09 21:12:12 +00002844 'change_id': change_id,
2845 'description': change_desc.description,
2846 }
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002847
Gavin Mak4e5e3992022-11-14 22:40:12 +00002848 # Gerrit may or may not update fast enough to return the correct patchset
2849 # number after we push. Get the pre-upload patchset and increment later.
2850 latest_ps = self.GetMostRecentPatchset(update=False) or 0
2851
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002852 push_stdout = self._RunGitPushWithTraces(refspec, refspec_opts,
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002853 git_push_metadata,
2854 options.push_options)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002855
2856 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002857 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002858 change_numbers = [m.group(1)
2859 for m in map(regex.match, push_stdout.splitlines())
2860 if m]
2861 if len(change_numbers) != 1:
2862 DieWithError(
2863 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002864 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002865 self.SetIssue(change_numbers[0])
Gavin Mak4e5e3992022-11-14 22:40:12 +00002866 self.SetPatchset(latest_ps + 1)
Gavin Makbe2e9262022-11-08 23:41:55 +00002867 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY, ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002868
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002869 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002870 # GetIssue() is not set in case of non-squash uploads according to tests.
Aaron Gable6e7ddb62020-05-27 22:23:29 +00002871 # TODO(crbug.com/751901): non-squash uploads in git cl should be removed.
Thiago Perrottab0fb8d52022-08-30 21:26:19 +00002872 gerrit_util.AddReviewers(self.GetGerritHost(),
2873 self._GerritChangeIdentifier(),
2874 reviewers,
2875 cc,
2876 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002877
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002878 return 0
2879
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002880 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2881 change_desc):
2882 """Computes parent of the generated commit to be uploaded to Gerrit.
2883
2884 Returns revision or a ref name.
2885 """
2886 if custom_cl_base:
2887 # Try to avoid creating additional unintended CLs when uploading, unless
2888 # user wants to take this risk.
2889 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2890 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2891 local_ref_of_target_remote])
2892 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002893 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002894 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2895 'If you proceed with upload, more than 1 CL may be created by '
2896 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2897 'If you are certain that specified base `%s` has already been '
2898 'uploaded to Gerrit as another CL, you may proceed.\n' %
2899 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2900 if not force:
2901 confirm_or_exit(
2902 'Do you take responsibility for cleaning up potential mess '
2903 'resulting from proceeding with upload?',
2904 action='upload')
2905 return custom_cl_base
2906
Aaron Gablef97e33d2017-03-30 15:44:27 -07002907 if remote != '.':
2908 return self.GetCommonAncestorWithUpstream()
2909
2910 # If our upstream branch is local, we base our squashed commit on its
2911 # squashed version.
2912 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2913
Aaron Gablef97e33d2017-03-30 15:44:27 -07002914 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002915 return self.GetCommonAncestorWithUpstream()
Glen Robertson7d98e222020-08-27 17:53:11 +00002916 if upstream_branch_name == 'main':
2917 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002918
2919 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002920 # TODO(tandrii): consider checking parent change in Gerrit and using its
2921 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2922 # the tree hash of the parent branch. The upside is less likely bogus
2923 # requests to reupload parent change just because it's uploadhash is
2924 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Gavin Makbe2e9262022-11-08 23:41:55 +00002925 parent = scm.GIT.GetBranchConfig(settings.GetRoot(), upstream_branch_name,
2926 GERRIT_SQUASH_HASH_CONFIG_KEY)
Aaron Gablef97e33d2017-03-30 15:44:27 -07002927 # Verify that the upstream branch has been uploaded too, otherwise
2928 # Gerrit will create additional CLs when uploading.
2929 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2930 RunGitSilent(['rev-parse', parent + ':'])):
2931 DieWithError(
2932 '\nUpload upstream branch %s first.\n'
2933 'It is likely that this branch has been rebased since its last '
2934 'upload, so you just need to upload it again.\n'
2935 '(If you uploaded it with --no-squash, then branch dependencies '
2936 'are not supported, and you should reupload with --squash.)'
2937 % upstream_branch_name,
2938 change_desc)
2939 return parent
2940
Gavin Mak4e5e3992022-11-14 22:40:12 +00002941 def _UpdateWithExternalChanges(self):
2942 """Updates workspace with external changes.
2943
2944 Returns the commit hash that should be used as the merge base on upload.
2945 """
2946 local_ps = self.GetPatchset()
2947 if local_ps is None:
2948 return
2949
2950 external_ps = self.GetMostRecentPatchset(update=False)
Gavin Makf35a9eb2022-11-17 18:34:36 +00002951 if external_ps is None or local_ps == external_ps or \
2952 not self._IsPatchsetRangeSignificant(local_ps + 1, external_ps):
Gavin Mak4e5e3992022-11-14 22:40:12 +00002953 return
2954
2955 num_changes = external_ps - local_ps
Gavin Mak6f905472023-01-06 21:01:36 +00002956 if num_changes > 1:
2957 change_words = 'changes were'
2958 else:
2959 change_words = 'change was'
2960 print('\n%d external %s published to %s:\n' %
2961 (num_changes, change_words, self.GetIssueURL(short=True)))
2962
2963 # Print an overview of external changes.
2964 ps_to_commit = {}
2965 ps_to_info = {}
2966 revisions = self._GetChangeDetail(['ALL_REVISIONS'])
2967 for commit_id, revision_info in revisions.get('revisions', {}).items():
2968 ps_num = revision_info['_number']
2969 ps_to_commit[ps_num] = commit_id
2970 ps_to_info[ps_num] = revision_info
2971
2972 for ps in range(external_ps, local_ps, -1):
2973 commit = ps_to_commit[ps][:8]
2974 desc = ps_to_info[ps].get('description', '')
2975 print('Patchset %d [%s] %s' % (ps, commit, desc))
2976
2977 if not ask_for_explicit_yes('\nUploading as-is will override them. '
2978 'Get the latest changes and apply?'):
Gavin Mak4e5e3992022-11-14 22:40:12 +00002979 return
2980
2981 # Get latest Gerrit merge base. Use the first parent even if multiple exist.
2982 external_parent = self._GetChangeCommit(revision=external_ps)['parents'][0]
2983 external_base = external_parent['commit']
2984
2985 branch = git_common.current_branch()
2986 local_base = self.GetCommonAncestorWithUpstream()
2987 if local_base != external_base:
2988 print('\nLocal merge base %s is different from Gerrit %s.\n' %
2989 (local_base, external_base))
2990 if git_common.upstream(branch):
2991 DieWithError('Upstream branch set. Consider using `git rebase-update` '
2992 'to make these the same.')
2993 print('No upstream branch set. Consider setting it and using '
2994 '`git rebase-update`.\nContinuing upload with Gerrit merge base.')
2995
2996 # Fetch Gerrit's CL base if it doesn't exist locally.
2997 remote, _ = self.GetRemoteBranch()
2998 if not scm.GIT.IsValidRevision(settings.GetRoot(), external_base):
2999 RunGitSilent(['fetch', remote, external_base])
3000
3001 # Get the diff between local_ps and external_ps.
3002 issue = self.GetIssue()
Gavin Mak591ebaf2022-12-06 18:05:07 +00003003 changes_ref = 'refs/changes/%02d/%d/' % (issue % 100, issue)
Gavin Mak4e5e3992022-11-14 22:40:12 +00003004 RunGitSilent(['fetch', remote, changes_ref + str(local_ps)])
3005 last_uploaded = RunGitSilent(['rev-parse', 'FETCH_HEAD']).strip()
3006 RunGitSilent(['fetch', remote, changes_ref + str(external_ps)])
3007 latest_external = RunGitSilent(['rev-parse', 'FETCH_HEAD']).strip()
3008 diff = RunGitSilent(['diff', '%s..%s' % (last_uploaded, latest_external)])
3009
3010 # Diff can be empty in the case of trivial rebases.
3011 if not diff:
3012 return external_base
3013
3014 # Apply the diff.
3015 with gclient_utils.temporary_file() as diff_tempfile:
3016 gclient_utils.FileWrite(diff_tempfile, diff)
3017 clean_patch = RunGitWithCode(['apply', '--check', diff_tempfile])[0] == 0
3018 RunGitSilent(['apply', '-3', '--intent-to-add', diff_tempfile])
3019 if not clean_patch:
3020 # Normally patchset is set after upload. But because we exit, that never
3021 # happens. Updating here makes sure that subsequent uploads don't need
3022 # to fetch/apply the same diff again.
3023 self.SetPatchset(external_ps)
3024 DieWithError('\nPatch did not apply cleanly. Please resolve any '
3025 'conflicts and reupload.')
3026
3027 message = 'Incorporate external changes from '
3028 if num_changes == 1:
3029 message += 'patchset %d' % external_ps
3030 else:
3031 message += 'patchsets %d to %d' % (local_ps + 1, external_ps)
3032 RunGitSilent(['commit', '-am', message])
3033 # TODO(crbug.com/1382528): Use the previous commit's message as a default
3034 # patchset title instead of this 'Incorporate' message.
3035 return external_base
3036
Edward Lemura12175c2020-03-09 16:58:26 +00003037 def _AddChangeIdToCommitMessage(self, log_desc, args):
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003038 """Re-commits using the current message, assumes the commit hook is in
3039 place.
3040 """
Edward Lemura12175c2020-03-09 16:58:26 +00003041 RunGit(['commit', '--amend', '-m', log_desc])
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00003042 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003043 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003044 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003045 return new_log_desc
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003046
3047 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003048
tandriie113dfd2016-10-11 10:20:12 -07003049 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003050 try:
3051 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003052 except GerritChangeNotExists:
3053 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003054
3055 if data['status'] in ('ABANDONED', 'MERGED'):
3056 return 'CL %s is closed' % self.GetIssue()
3057
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003058 def GetGerritChange(self, patchset=None):
3059 """Returns a buildbucket.v2.GerritChange message for the current issue."""
Edward Lemur79d4f992019-11-11 23:49:02 +00003060 host = urllib.parse.urlparse(self.GetCodereviewServer()).hostname
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003061 issue = self.GetIssue()
Edward Lemur2c210a42019-09-16 23:58:35 +00003062 patchset = int(patchset or self.GetPatchset())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003063 data = self._GetChangeDetail(['ALL_REVISIONS'])
3064
3065 assert host and issue and patchset, 'CL must be uploaded first'
3066
3067 has_patchset = any(
3068 int(revision_data['_number']) == patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003069 for revision_data in data['revisions'].values())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003070 if not has_patchset:
Aaron Gablea45ee112016-11-22 15:14:38 -08003071 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003072 (patchset, self.GetIssue()))
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003073
tandrii8c5a3532016-11-04 07:52:02 -07003074 return {
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003075 'host': host,
3076 'change': issue,
3077 'project': data['project'],
3078 'patchset': patchset,
tandrii8c5a3532016-11-04 07:52:02 -07003079 }
tandriie113dfd2016-10-11 10:20:12 -07003080
tandriide281ae2016-10-12 06:02:30 -07003081 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003082 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003083
Edward Lemur707d70b2018-02-07 00:50:14 +01003084 def GetReviewers(self):
3085 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00003086 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01003087
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003088
Lei Zhang8a0efc12020-08-05 19:58:45 +00003089def _get_bug_line_values(default_project_prefix, bugs):
3090 """Given default_project_prefix and comma separated list of bugs, yields bug
3091 line values.
tandriif9aefb72016-07-01 09:06:51 -07003092
3093 Each bug can be either:
Lei Zhang8a0efc12020-08-05 19:58:45 +00003094 * a number, which is combined with default_project_prefix
tandriif9aefb72016-07-01 09:06:51 -07003095 * string, which is left as is.
3096
3097 This function may produce more than one line, because bugdroid expects one
3098 project per line.
3099
Lei Zhang8a0efc12020-08-05 19:58:45 +00003100 >>> list(_get_bug_line_values('v8:', '123,chromium:789'))
tandriif9aefb72016-07-01 09:06:51 -07003101 ['v8:123', 'chromium:789']
3102 """
3103 default_bugs = []
3104 others = []
3105 for bug in bugs.split(','):
3106 bug = bug.strip()
3107 if bug:
3108 try:
3109 default_bugs.append(int(bug))
3110 except ValueError:
3111 others.append(bug)
3112
3113 if default_bugs:
3114 default_bugs = ','.join(map(str, default_bugs))
Lei Zhang8a0efc12020-08-05 19:58:45 +00003115 if default_project_prefix:
3116 if not default_project_prefix.endswith(':'):
3117 default_project_prefix += ':'
3118 yield '%s%s' % (default_project_prefix, default_bugs)
tandriif9aefb72016-07-01 09:06:51 -07003119 else:
3120 yield default_bugs
3121 for other in sorted(others):
3122 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3123 yield other
3124
3125
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003126class ChangeDescription(object):
3127 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003128 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003129 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003130 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Dan Beamd8b04ca2019-10-10 21:23:26 +00003131 FIXED_LINE = r'^[ \t]*Fixed[ \t]*:[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003132 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003133 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3134 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
Anthony Polito02b5af32019-12-02 19:49:47 +00003135 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):($|[^:])'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003136 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003137
Dan Beamd8b04ca2019-10-10 21:23:26 +00003138 def __init__(self, description, bug=None, fixed=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003139 self._description_lines = (description or '').strip().splitlines()
Anthony Polito8b955342019-09-24 19:01:36 +00003140 if bug:
3141 regexp = re.compile(self.BUG_LINE)
3142 prefix = settings.GetBugPrefix()
3143 if not any((regexp.match(line) for line in self._description_lines)):
3144 values = list(_get_bug_line_values(prefix, bug))
3145 self.append_footer('Bug: %s' % ', '.join(values))
Dan Beamd8b04ca2019-10-10 21:23:26 +00003146 if fixed:
3147 regexp = re.compile(self.FIXED_LINE)
3148 prefix = settings.GetBugPrefix()
3149 if not any((regexp.match(line) for line in self._description_lines)):
3150 values = list(_get_bug_line_values(prefix, fixed))
3151 self.append_footer('Fixed: %s' % ', '.join(values))
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003152
agable@chromium.org42c20792013-09-12 17:34:49 +00003153 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003154 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003155 return '\n'.join(self._description_lines)
3156
3157 def set_description(self, desc):
3158 if isinstance(desc, basestring):
3159 lines = desc.splitlines()
3160 else:
3161 lines = [line.rstrip() for line in desc]
3162 while lines and not lines[0]:
3163 lines.pop(0)
3164 while lines and not lines[-1]:
3165 lines.pop(-1)
3166 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003167
Edward Lemur5a644f82020-03-18 16:44:57 +00003168 def ensure_change_id(self, change_id):
3169 description = self.description
3170 footer_change_ids = git_footers.get_footer_change_id(description)
3171 # Make sure that the Change-Id in the description matches the given one.
3172 if footer_change_ids != [change_id]:
3173 if footer_change_ids:
3174 # Remove any existing Change-Id footers since they don't match the
3175 # expected change_id footer.
3176 description = git_footers.remove_footer(description, 'Change-Id')
3177 print('WARNING: Change-Id has been set to %s. Use `git cl issue 0` '
3178 'if you want to set a new one.')
3179 # Add the expected Change-Id footer.
3180 description = git_footers.add_footer_change_id(description, change_id)
3181 self.set_description(description)
3182
Joanna Wang39811b12023-01-20 23:09:48 +00003183 def update_reviewers(self, reviewers):
3184 """Rewrites the R= line(s) as a single line each.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003185
3186 Args:
3187 reviewers (list(str)) - list of additional emails to use for reviewers.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003188 """
Joanna Wang39811b12023-01-20 23:09:48 +00003189 if not reviewers:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003190 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003191
3192 reviewers = set(reviewers)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003193
Joanna Wang39811b12023-01-20 23:09:48 +00003194 # Get the set of R= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003195 regexp = re.compile(self.R_LINE)
3196 matches = [regexp.match(line) for line in self._description_lines]
3197 new_desc = [l for i, l in enumerate(self._description_lines)
3198 if not matches[i]]
3199 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003200
Joanna Wang39811b12023-01-20 23:09:48 +00003201 # Construct new unified R= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003202
Joanna Wang39811b12023-01-20 23:09:48 +00003203 # First, update reviewers with names from the R= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003204 for match in matches:
3205 if not match:
3206 continue
Joanna Wang39811b12023-01-20 23:09:48 +00003207 reviewers.update(cleanup_list([match.group(2).strip()]))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003208
Joanna Wang39811b12023-01-20 23:09:48 +00003209 new_r_line = 'R=' + ', '.join(sorted(reviewers))
agable@chromium.org42c20792013-09-12 17:34:49 +00003210
3211 # Put the new lines in the description where the old first R= line was.
3212 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3213 if 0 <= line_loc < len(self._description_lines):
Joanna Wang39811b12023-01-20 23:09:48 +00003214 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003215 else:
Joanna Wang39811b12023-01-20 23:09:48 +00003216 self.append_footer(new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003217
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00003218 def set_preserve_tryjobs(self):
3219 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
3220 footers = git_footers.parse_footers(self.description)
3221 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
3222 if v.lower() == 'true':
3223 return
3224 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
3225
Anthony Polito8b955342019-09-24 19:01:36 +00003226 def prompt(self):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003227 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003228 self.set_description([
3229 '# Enter a description of the change.',
3230 '# This will be displayed on the codereview site.',
3231 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003232 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003233 '--------------------',
3234 ] + self._description_lines)
Dan Beamd8b04ca2019-10-10 21:23:26 +00003235 bug_regexp = re.compile(self.BUG_LINE)
3236 fixed_regexp = re.compile(self.FIXED_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00003237 prefix = settings.GetBugPrefix()
Sigurd Schneider8630bb12020-11-11 14:02:49 +00003238 has_issue = lambda l: bug_regexp.match(l) or fixed_regexp.match(l)
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00003239
Dan Beamd8b04ca2019-10-10 21:23:26 +00003240 if not any((has_issue(line) for line in self._description_lines)):
Anthony Polito8b955342019-09-24 19:01:36 +00003241 self.append_footer('Bug: %s' % prefix)
tandriif9aefb72016-07-01 09:06:51 -07003242
Bruce Dawsonfc487042020-10-27 19:11:37 +00003243 print('Waiting for editor...')
agable@chromium.org42c20792013-09-12 17:34:49 +00003244 content = gclient_utils.RunEditor(self.description, True,
Edward Lemur79d4f992019-11-11 23:49:02 +00003245 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003246 if not content:
3247 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003248 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003249
Bruce Dawson2377b012018-01-11 16:46:49 -08003250 # Strip off comments and default inserted "Bug:" line.
3251 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00003252 (line.startswith('#') or
3253 line.rstrip() == "Bug:" or
3254 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00003255 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003256 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003257 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003258
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003259 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003260 """Adds a footer line to the description.
3261
3262 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3263 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3264 that Gerrit footers are always at the end.
3265 """
3266 parsed_footer_line = git_footers.parse_footer(line)
3267 if parsed_footer_line:
3268 # Line is a gerrit footer in the form: Footer-Key: any value.
3269 # Thus, must be appended observing Gerrit footer rules.
3270 self.set_description(
3271 git_footers.add_footer(self.description,
3272 key=parsed_footer_line[0],
3273 value=parsed_footer_line[1]))
3274 return
3275
3276 if not self._description_lines:
3277 self._description_lines.append(line)
3278 return
3279
3280 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3281 if gerrit_footers:
3282 # git_footers.split_footers ensures that there is an empty line before
3283 # actual (gerrit) footers, if any. We have to keep it that way.
3284 assert top_lines and top_lines[-1] == ''
3285 top_lines, separator = top_lines[:-1], top_lines[-1:]
3286 else:
3287 separator = [] # No need for separator if there are no gerrit_footers.
3288
3289 prev_line = top_lines[-1] if top_lines else ''
Josip Sokcevic7958e302023-03-01 23:02:21 +00003290 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3291 not presubmit_support.Change.TAG_LINE_RE.match(line)):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003292 top_lines.append('')
3293 top_lines.append(line)
3294 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003295
tandrii99a72f22016-08-17 14:33:24 -07003296 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003297 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003298 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003299 reviewers = [match.group(2).strip()
3300 for match in matches
3301 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003302 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003303
bradnelsond975b302016-10-23 12:20:23 -07003304 def get_cced(self):
3305 """Retrieves the list of reviewers."""
3306 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3307 cced = [match.group(2).strip() for match in matches if match]
3308 return cleanup_list(cced)
3309
Nodir Turakulov23b82142017-11-16 11:04:25 -08003310 def get_hash_tags(self):
3311 """Extracts and sanitizes a list of Gerrit hashtags."""
3312 subject = (self._description_lines or ('',))[0]
3313 subject = re.sub(
3314 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3315
3316 tags = []
3317 start = 0
3318 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3319 while True:
3320 m = bracket_exp.match(subject, start)
3321 if not m:
3322 break
3323 tags.append(self.sanitize_hash_tag(m.group(1)))
3324 start = m.end()
3325
3326 if not tags:
3327 # Try "Tag: " prefix.
3328 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3329 if m:
3330 tags.append(self.sanitize_hash_tag(m.group(1)))
3331 return tags
3332
3333 @classmethod
3334 def sanitize_hash_tag(cls, tag):
3335 """Returns a sanitized Gerrit hash tag.
3336
3337 A sanitized hashtag can be used as a git push refspec parameter value.
3338 """
3339 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3340
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003341
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003342def FindCodereviewSettingsFile(filename='codereview.settings'):
3343 """Finds the given file starting in the cwd and going up.
3344
3345 Only looks up to the top of the repository unless an
3346 'inherit-review-settings-ok' file exists in the root of the repository.
3347 """
3348 inherit_ok_file = 'inherit-review-settings-ok'
3349 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003350 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003351 if os.path.isfile(os.path.join(root, inherit_ok_file)):
Aleksey Khoroshilov2a229712022-06-02 16:24:11 +00003352 root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003353 while True:
Aleksey Khoroshilov2a229712022-06-02 16:24:11 +00003354 if os.path.isfile(os.path.join(cwd, filename)):
3355 return open(os.path.join(cwd, filename))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003356 if cwd == root:
3357 break
Aleksey Khoroshilov2a229712022-06-02 16:24:11 +00003358 parent_dir = os.path.dirname(cwd)
3359 if parent_dir == cwd:
3360 # We hit the system root directory.
3361 break
3362 cwd = parent_dir
3363 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003364
3365
3366def LoadCodereviewSettingsFromFile(fileobj):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003367 """Parses a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003368 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003369
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003370 def SetProperty(name, setting, unset_error_ok=False):
3371 fullname = 'rietveld.' + name
3372 if setting in keyvals:
3373 RunGit(['config', fullname, keyvals[setting]])
3374 else:
3375 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3376
tandrii48df5812016-10-17 03:55:37 -07003377 if not keyvals.get('GERRIT_HOST', False):
3378 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003379 # Only server setting is required. Other settings can be absent.
3380 # In that case, we ignore errors raised during option deletion attempt.
Joanna Wangc8f23e22023-01-19 21:18:10 +00003381 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003382 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3383 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003384 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003385 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3386 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003387 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3388 unset_error_ok=True)
Jamie Madilldc4d19e2019-10-24 21:50:02 +00003389 SetProperty(
3390 'format-full-by-default', 'FORMAT_FULL_BY_DEFAULT', unset_error_ok=True)
Dirk Pranke6f0df682021-06-25 00:42:33 +00003391 SetProperty('use-python3', 'USE_PYTHON3', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003392
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003393 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003394 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003395
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003396 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
Edward Lesmes4de54132020-05-05 19:41:33 +00003397 RunGit(['config', 'gerrit.squash-uploads',
3398 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003399
tandrii@chromium.org28253532016-04-14 13:46:56 +00003400 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003401 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003402 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3403
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003404 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003405 # should be of the form
3406 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3407 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003408 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3409 keyvals['ORIGIN_URL_CONFIG']])
3410
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003411
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003412def urlretrieve(source, destination):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003413 """Downloads a network object to a local file, like urllib.urlretrieve.
3414
3415 This is necessary because urllib is broken for SSL connections via a proxy.
3416 """
Vadim Shtayuraf7b8f8f2021-11-15 19:10:05 +00003417 with open(destination, 'wb') as f:
Edward Lemur79d4f992019-11-11 23:49:02 +00003418 f.write(urllib.request.urlopen(source).read())
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003419
3420
ukai@chromium.org712d6102013-11-27 00:52:58 +00003421def hasSheBang(fname):
3422 """Checks fname is a #! script."""
3423 with open(fname) as f:
3424 return f.read(2).startswith('#!')
3425
3426
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003427def DownloadGerritHook(force):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003428 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003429
3430 Args:
3431 force: True to update hooks. False to install hooks if not present.
3432 """
ukai@chromium.org712d6102013-11-27 00:52:58 +00003433 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003434 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3435 if not os.access(dst, os.X_OK):
3436 if os.path.exists(dst):
3437 if not force:
3438 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003439 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003440 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003441 if not hasSheBang(dst):
3442 DieWithError('Not a script: %s\n'
3443 'You need to download from\n%s\n'
3444 'into .git/hooks/commit-msg and '
3445 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003446 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3447 except Exception:
3448 if os.path.exists(dst):
3449 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003450 DieWithError('\nFailed to download hooks.\n'
3451 'You need to download from\n%s\n'
3452 'into .git/hooks/commit-msg and '
3453 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003454
3455
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003456class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003457 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003458
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003459 def __init__(self):
3460 # Cached list of [host, identity, source], where source is either
3461 # .gitcookies or .netrc.
3462 self._all_hosts = None
3463
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003464 def ensure_configured_gitcookies(self):
3465 """Runs checks and suggests fixes to make git use .gitcookies from default
3466 path."""
3467 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3468 configured_path = RunGitSilent(
3469 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003470 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003471 if configured_path:
3472 self._ensure_default_gitcookies_path(configured_path, default)
3473 else:
3474 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003475
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003476 @staticmethod
3477 def _ensure_default_gitcookies_path(configured_path, default_path):
3478 assert configured_path
3479 if configured_path == default_path:
3480 print('git is already configured to use your .gitcookies from %s' %
3481 configured_path)
3482 return
3483
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003484 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003485 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3486 (configured_path, default_path))
3487
3488 if not os.path.exists(configured_path):
3489 print('However, your configured .gitcookies file is missing.')
3490 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3491 action='reconfigure')
3492 RunGit(['config', '--global', 'http.cookiefile', default_path])
3493 return
3494
3495 if os.path.exists(default_path):
3496 print('WARNING: default .gitcookies file already exists %s' %
3497 default_path)
3498 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3499 default_path)
3500
3501 confirm_or_exit('Move existing .gitcookies to default location?',
3502 action='move')
3503 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003504 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003505 print('Moved and reconfigured git to use .gitcookies from %s' %
3506 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003507
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003508 @staticmethod
3509 def _configure_gitcookies_path(default_path):
3510 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3511 if os.path.exists(netrc_path):
3512 print('You seem to be using outdated .netrc for git credentials: %s' %
3513 netrc_path)
3514 print('This tool will guide you through setting up recommended '
3515 '.gitcookies store for git credentials.\n'
3516 '\n'
3517 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3518 ' git config --global --unset http.cookiefile\n'
3519 ' mv %s %s.backup\n\n' % (default_path, default_path))
3520 confirm_or_exit(action='setup .gitcookies')
3521 RunGit(['config', '--global', 'http.cookiefile', default_path])
3522 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003523
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003524 def get_hosts_with_creds(self, include_netrc=False):
3525 if self._all_hosts is None:
3526 a = gerrit_util.CookiesAuthenticator()
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003527 self._all_hosts = [(h, u, s) for h, u, s in itertools.chain((
3528 (h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.items()), (
3529 (h, u, '.gitcookies') for h, (u, _) in a.gitcookies.items()))
3530 if h.endswith(_GOOGLESOURCE)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003531
3532 if include_netrc:
3533 return self._all_hosts
3534 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3535
3536 def print_current_creds(self, include_netrc=False):
3537 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3538 if not hosts:
3539 print('No Git/Gerrit credentials found')
3540 return
Edward Lemur79d4f992019-11-11 23:49:02 +00003541 lengths = [max(map(len, (row[i] for row in hosts))) for i in range(3)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003542 header = [('Host', 'User', 'Which file'),
3543 ['=' * l for l in lengths]]
3544 for row in (header + hosts):
3545 print('\t'.join((('%%+%ds' % l) % s)
3546 for l, s in zip(lengths, row)))
3547
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003548 @staticmethod
3549 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003550 """Parses identity "git-<username>.domain" into <username> and domain."""
3551 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003552 # distinguishable from sub-domains. But we do know typical domains:
3553 if identity.endswith('.chromium.org'):
3554 domain = 'chromium.org'
3555 username = identity[:-len('.chromium.org')]
3556 else:
3557 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003558 if username.startswith('git-'):
3559 username = username[len('git-'):]
3560 return username, domain
3561
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003562 def has_generic_host(self):
3563 """Returns whether generic .googlesource.com has been configured.
3564
3565 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3566 """
3567 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003568 if host == '.' + _GOOGLESOURCE:
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003569 return True
3570 return False
3571
3572 def _get_git_gerrit_identity_pairs(self):
3573 """Returns map from canonic host to pair of identities (Git, Gerrit).
3574
3575 One of identities might be None, meaning not configured.
3576 """
3577 host_to_identity_pairs = {}
3578 for host, identity, _ in self.get_hosts_with_creds():
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003579 canonical = _canonical_git_googlesource_host(host)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003580 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3581 idx = 0 if canonical == host else 1
3582 pair[idx] = identity
3583 return host_to_identity_pairs
3584
3585 def get_partially_configured_hosts(self):
3586 return set(
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003587 (host if i1 else _canonical_gerrit_googlesource_host(host))
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003588 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003589 if None in (i1, i2) and host != '.' + _GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003590
3591 def get_conflicting_hosts(self):
3592 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003593 host
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003594 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003595 if None not in (i1, i2) and i1 != i2)
3596
3597 def get_duplicated_hosts(self):
3598 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003599 return set(host for host, count in counters.items() if count > 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003600
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003601
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003602 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003603 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003604 hosts = sorted(hosts)
3605 assert hosts
3606 if extra_column_func is None:
3607 extras = [''] * len(hosts)
3608 else:
3609 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003610 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3611 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003612 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003613 lines.append(tmpl % he)
3614 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003615
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003616 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003617 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003618 yield ('.googlesource.com wildcard record detected',
3619 ['Chrome Infrastructure team recommends to list full host names '
3620 'explicitly.'],
3621 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003622
3623 dups = self.get_duplicated_hosts()
3624 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003625 yield ('The following hosts were defined twice',
3626 self._format_hosts(dups),
3627 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003628
3629 partial = self.get_partially_configured_hosts()
3630 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003631 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3632 'These hosts are missing',
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003633 self._format_hosts(
3634 partial, lambda host: 'but %s defined' % _get_counterpart_host(
3635 host)), partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003636
3637 conflicting = self.get_conflicting_hosts()
3638 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003639 yield ('The following Git hosts have differing credentials from their '
3640 'Gerrit counterparts',
3641 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3642 tuple(self._get_git_gerrit_identity_pairs()[host])),
3643 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003644
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003645 def find_and_report_problems(self):
3646 """Returns True if there was at least one problem, else False."""
3647 found = False
3648 bad_hosts = set()
3649 for title, sublines, hosts in self._find_problems():
3650 if not found:
3651 found = True
3652 print('\n\n.gitcookies problem report:\n')
3653 bad_hosts.update(hosts or [])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003654 print(' %s%s' % (title, (':' if sublines else '')))
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003655 if sublines:
3656 print()
3657 print(' %s' % '\n '.join(sublines))
3658 print()
3659
3660 if bad_hosts:
3661 assert found
3662 print(' You can manually remove corresponding lines in your %s file and '
3663 'visit the following URLs with correct account to generate '
3664 'correct credential lines:\n' %
3665 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003666 print(' %s' % '\n '.join(
3667 sorted(
3668 set(gerrit_util.CookiesAuthenticator().get_new_password_url(
3669 _canonical_git_googlesource_host(host))
3670 for host in bad_hosts))))
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003671 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003672
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003673
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003674@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003675def CMDcreds_check(parser, args):
3676 """Checks credentials and suggests changes."""
3677 _, _ = parser.parse_args(args)
3678
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003679 # Code below checks .gitcookies. Abort if using something else.
3680 authn = gerrit_util.Authenticator.get()
3681 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
Edward Lemur57d47422020-03-06 20:43:07 +00003682 message = (
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003683 'This command is not designed for bot environment. It checks '
3684 '~/.gitcookies file not generally used on bots.')
Edward Lemur57d47422020-03-06 20:43:07 +00003685 # TODO(crbug.com/1059384): Automatically detect when running on cloudtop.
3686 if isinstance(authn, gerrit_util.GceAuthenticator):
3687 message += (
3688 '\n'
3689 'If you need to run this on GCE or a cloudtop instance, '
3690 'export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
3691 DieWithError(message)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003692
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003693 checker = _GitCookiesChecker()
3694 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003695
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003696 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003697 checker.print_current_creds(include_netrc=True)
3698
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003699 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003700 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003701 return 0
3702 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003703
3704
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003705@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003706def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003707 """Gets or sets base-url for this branch."""
Thiago Perrotta16d08f02022-07-20 18:18:50 +00003708 _, args = parser.parse_args(args)
Edward Lesmes50da7702020-03-30 19:23:43 +00003709 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
Edward Lemur85153282020-02-14 22:06:29 +00003710 branch = scm.GIT.ShortBranchName(branchref)
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003711 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003712 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003713 return RunGit(['config', 'branch.%s.base-url' % branch],
3714 error_ok=False).strip()
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003715
3716 print('Setting base-url to %s' % args[0])
3717 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3718 error_ok=False).strip()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003719
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003720
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003721def color_for_status(status):
3722 """Maps a Changelist status to color, for CMDstatus and other tools."""
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00003723 BOLD = '\033[1m'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003724 return {
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00003725 'unsent': BOLD + Fore.YELLOW,
3726 'waiting': BOLD + Fore.RED,
3727 'reply': BOLD + Fore.YELLOW,
3728 'not lgtm': BOLD + Fore.RED,
3729 'lgtm': BOLD + Fore.GREEN,
3730 'commit': BOLD + Fore.MAGENTA,
3731 'closed': BOLD + Fore.CYAN,
3732 'error': BOLD + Fore.WHITE,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003733 }.get(status, Fore.WHITE)
3734
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003735
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003736def get_cl_statuses(changes, fine_grained, max_processes=None):
3737 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003738
3739 If fine_grained is true, this will fetch CL statuses from the server.
3740 Otherwise, simply indicate if there's a matching url for the given branches.
3741
3742 If max_processes is specified, it is used as the maximum number of processes
3743 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3744 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003745
3746 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003747 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003748 if not changes:
Edward Lemur61bf4172020-02-24 23:22:37 +00003749 return
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003750
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003751 if not fine_grained:
3752 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003753 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003754 for cl in changes:
3755 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003756 return
3757
3758 # First, sort out authentication issues.
3759 logging.debug('ensuring credentials exist')
3760 for cl in changes:
3761 cl.EnsureAuthenticated(force=False, refresh=True)
3762
3763 def fetch(cl):
3764 try:
3765 return (cl, cl.GetStatus())
3766 except:
3767 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003768 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003769 raise
3770
3771 threads_count = len(changes)
3772 if max_processes:
3773 threads_count = max(1, min(threads_count, max_processes))
3774 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3775
Edward Lemur61bf4172020-02-24 23:22:37 +00003776 pool = multiprocessing.pool.ThreadPool(threads_count)
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003777 fetched_cls = set()
3778 try:
3779 it = pool.imap_unordered(fetch, changes).__iter__()
3780 while True:
3781 try:
3782 cl, status = it.next(timeout=5)
Edward Lemur61bf4172020-02-24 23:22:37 +00003783 except (multiprocessing.TimeoutError, StopIteration):
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003784 break
3785 fetched_cls.add(cl)
3786 yield cl, status
3787 finally:
3788 pool.close()
3789
3790 # Add any branches that failed to fetch.
3791 for cl in set(changes) - fetched_cls:
3792 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003793
rmistry@google.com2dd99862015-06-22 12:22:18 +00003794
Jose Lopes3863fc52020-04-07 17:00:25 +00003795def upload_branch_deps(cl, args, force=False):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003796 """Uploads CLs of local branches that are dependents of the current branch.
3797
3798 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003799
3800 test1 -> test2.1 -> test3.1
3801 -> test3.2
3802 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003803
3804 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3805 run on the dependent branches in this order:
3806 test2.1, test3.1, test3.2, test2.2, test3.3
3807
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003808 Note: This function does not rebase your local dependent branches. Use it
3809 when you make a change to the parent branch that will not conflict
3810 with its dependent branches, and you would like their dependencies
3811 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003812 """
3813 if git_common.is_dirty_git_tree('upload-branch-deps'):
3814 return 1
3815
3816 root_branch = cl.GetBranch()
3817 if root_branch is None:
3818 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3819 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003820 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003821 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3822 'patchset dependencies without an uploaded CL.')
3823
3824 branches = RunGit(['for-each-ref',
3825 '--format=%(refname:short) %(upstream:short)',
3826 'refs/heads'])
3827 if not branches:
3828 print('No local branches found.')
3829 return 0
3830
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003831 # Create a dictionary of all local branches to the branches that are
3832 # dependent on it.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003833 tracked_to_dependents = collections.defaultdict(list)
3834 for b in branches.splitlines():
3835 tokens = b.split()
3836 if len(tokens) == 2:
3837 branch_name, tracked = tokens
3838 tracked_to_dependents[tracked].append(branch_name)
3839
vapiera7fbd5a2016-06-16 09:17:49 -07003840 print()
3841 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003842 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003843
rmistry@google.com2dd99862015-06-22 12:22:18 +00003844 def traverse_dependents_preorder(branch, padding=''):
3845 dependents_to_process = tracked_to_dependents.get(branch, [])
3846 padding += ' '
3847 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003848 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003849 dependents.append(dependent)
3850 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003851
rmistry@google.com2dd99862015-06-22 12:22:18 +00003852 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003853 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003854
3855 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003856 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003857 return 0
3858
rmistry@google.com2dd99862015-06-22 12:22:18 +00003859 # Record all dependents that failed to upload.
3860 failures = {}
3861 # Go through all dependents, checkout the branch and upload.
3862 try:
3863 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003864 print()
3865 print('--------------------------------------')
3866 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003867 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003868 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003869 try:
3870 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003871 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003872 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003873 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003874 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003875 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003876 finally:
3877 # Swap back to the original root branch.
3878 RunGit(['checkout', '-q', root_branch])
3879
vapiera7fbd5a2016-06-16 09:17:49 -07003880 print()
3881 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003882 for dependent_branch in dependents:
3883 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003884 print(' %s : %s' % (dependent_branch, upload_status))
3885 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003886
3887 return 0
3888
3889
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003890def GetArchiveTagForBranch(issue_num, branch_name, existing_tags, pattern):
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003891 """Given a proposed tag name, returns a tag name that is guaranteed to be
3892 unique. If 'foo' is proposed but already exists, then 'foo-2' is used,
3893 or 'foo-3', and so on."""
3894
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003895 proposed_tag = pattern.format(**{'issue': issue_num, 'branch': branch_name})
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003896 for suffix_num in itertools.count(1):
3897 if suffix_num == 1:
3898 to_check = proposed_tag
3899 else:
3900 to_check = '%s-%d' % (proposed_tag, suffix_num)
3901
3902 if to_check not in existing_tags:
3903 return to_check
3904
3905
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003906@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003907def CMDarchive(parser, args):
3908 """Archives and deletes branches associated with closed changelists."""
3909 parser.add_option(
3910 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003911 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003912 parser.add_option(
3913 '-f', '--force', action='store_true',
3914 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003915 parser.add_option(
3916 '-d', '--dry-run', action='store_true',
3917 help='Skip the branch tagging and removal steps.')
3918 parser.add_option(
3919 '-t', '--notags', action='store_true',
3920 help='Do not tag archived branches. '
3921 'Note: local commit history may be lost.')
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003922 parser.add_option(
3923 '-p',
3924 '--pattern',
3925 default='git-cl-archived-{issue}-{branch}',
3926 help='Format string for archive tags. '
3927 'E.g. \'archived-{issue}-{branch}\'.')
kmarshall3bff56b2016-06-06 18:31:47 -07003928
kmarshall3bff56b2016-06-06 18:31:47 -07003929 options, args = parser.parse_args(args)
3930 if args:
3931 parser.error('Unsupported args: %s' % ' '.join(args))
kmarshall3bff56b2016-06-06 18:31:47 -07003932
3933 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3934 if not branches:
3935 return 0
3936
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003937 tags = RunGit(['for-each-ref', '--format=%(refname)',
3938 'refs/tags']).splitlines() or []
3939 tags = [t.split('/')[-1] for t in tags]
3940
vapiera7fbd5a2016-06-16 09:17:49 -07003941 print('Finding all branches associated with closed issues...')
Edward Lemur934836a2019-09-09 20:16:54 +00003942 changes = [Changelist(branchref=b)
3943 for b in branches.splitlines()]
kmarshall3bff56b2016-06-06 18:31:47 -07003944 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3945 statuses = get_cl_statuses(changes,
3946 fine_grained=True,
3947 max_processes=options.maxjobs)
3948 proposal = [(cl.GetBranch(),
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003949 GetArchiveTagForBranch(cl.GetIssue(), cl.GetBranch(), tags,
3950 options.pattern))
kmarshall3bff56b2016-06-06 18:31:47 -07003951 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00003952 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07003953 proposal.sort()
3954
3955 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003956 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003957 return 0
3958
Edward Lemur85153282020-02-14 22:06:29 +00003959 current_branch = scm.GIT.GetBranch(settings.GetRoot())
kmarshall3bff56b2016-06-06 18:31:47 -07003960
vapiera7fbd5a2016-06-16 09:17:49 -07003961 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003962 if options.notags:
3963 for next_item in proposal:
3964 print(' ' + next_item[0])
3965 else:
3966 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3967 for next_item in proposal:
3968 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003969
kmarshall9249e012016-08-23 12:02:16 -07003970 # Quit now on precondition failure or if instructed by the user, either
3971 # via an interactive prompt or by command line flags.
3972 if options.dry_run:
3973 print('\nNo changes were made (dry run).\n')
3974 return 0
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003975
3976 if any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003977 print('You are currently on a branch \'%s\' which is associated with a '
3978 'closed codereview issue, so archive cannot proceed. Please '
3979 'checkout another branch and run this command again.' %
3980 current_branch)
3981 return 1
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003982
3983 if not options.force:
Edward Lesmesae3586b2020-03-23 21:21:14 +00003984 answer = gclient_utils.AskForData('\nProceed with deletion (Y/n)? ').lower()
sergiyb4a5ecbe2016-06-20 09:46:00 -07003985 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003986 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003987 return 1
3988
3989 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003990 if not options.notags:
3991 RunGit(['tag', tagname, branch])
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003992
3993 if RunGitWithCode(['branch', '-D', branch])[0] != 0:
3994 # Clean up the tag if we failed to delete the branch.
3995 RunGit(['tag', '-d', tagname])
kmarshall9249e012016-08-23 12:02:16 -07003996
vapiera7fbd5a2016-06-16 09:17:49 -07003997 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003998
3999 return 0
4000
4001
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004002@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004003def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004004 """Show status of changelists.
4005
4006 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004007 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004008 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004009 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004010 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004011 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004012 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004013 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004014
4015 Also see 'git cl comments'.
4016 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00004017 parser.add_option(
4018 '--no-branch-color',
4019 action='store_true',
4020 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004021 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004022 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004023 parser.add_option('-f', '--fast', action='store_true',
4024 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004025 parser.add_option(
4026 '-j', '--maxjobs', action='store', type=int,
4027 help='The maximum number of jobs to use when retrieving review status')
Edward Lemur52969c92020-02-06 18:15:28 +00004028 parser.add_option(
4029 '-i', '--issue', type=int,
4030 help='Operate on this issue instead of the current branch\'s implicit '
4031 'issue. Requires --field to be set.')
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00004032 parser.add_option('-d',
4033 '--date-order',
4034 action='store_true',
4035 help='Order branches by committer date.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004036 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004037 if args:
4038 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004039
iannuccie53c9352016-08-17 14:40:40 -07004040 if options.issue is not None and not options.field:
Edward Lemur6c6827c2020-02-06 21:15:18 +00004041 parser.error('--field must be given when --issue is set.')
iannucci3c972b92016-08-17 13:24:10 -07004042
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004043 if options.field:
Edward Lemur934836a2019-09-09 20:16:54 +00004044 cl = Changelist(issue=options.issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004045 if options.field.startswith('desc'):
Edward Lemur6c6827c2020-02-06 21:15:18 +00004046 if cl.GetIssue():
4047 print(cl.FetchDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004048 elif options.field == 'id':
4049 issueid = cl.GetIssue()
4050 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004051 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004052 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004053 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004054 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004055 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004056 elif options.field == 'status':
4057 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004058 elif options.field == 'url':
4059 url = cl.GetIssueURL()
4060 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004061 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004062 return 0
4063
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00004064 branches = RunGit([
4065 'for-each-ref', '--format=%(refname) %(committerdate:unix)', 'refs/heads'
4066 ])
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004067 if not branches:
4068 print('No local branch found.')
4069 return 0
4070
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004071 changes = [
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00004072 Changelist(branchref=b, commit_date=ct)
4073 for b, ct in map(lambda line: line.split(' '), branches.splitlines())
4074 ]
vapiera7fbd5a2016-06-16 09:17:49 -07004075 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004076 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004077 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004078 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004079
Edward Lemur85153282020-02-14 22:06:29 +00004080 current_branch = scm.GIT.GetBranch(settings.GetRoot())
Daniel McArdlea23bf592019-02-12 00:25:12 +00004081
4082 def FormatBranchName(branch, colorize=False):
4083 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
4084 an asterisk when it is the current branch."""
4085
4086 asterisk = ""
4087 color = Fore.RESET
4088 if branch == current_branch:
4089 asterisk = "* "
4090 color = Fore.GREEN
Edward Lemur85153282020-02-14 22:06:29 +00004091 branch_name = scm.GIT.ShortBranchName(branch)
Daniel McArdlea23bf592019-02-12 00:25:12 +00004092
4093 if colorize:
4094 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00004095 return asterisk + branch_name
4096
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004097 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00004098
4099 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +00004100
4101 if options.date_order or settings.IsStatusCommitOrderByDate():
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00004102 sorted_changes = sorted(changes,
4103 key=lambda c: c.GetCommitDate(),
4104 reverse=True)
4105 else:
4106 sorted_changes = sorted(changes, key=lambda c: c.GetBranch())
4107 for cl in sorted_changes:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004108 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004109 while branch not in branch_statuses:
Edward Lemur79d4f992019-11-11 23:49:02 +00004110 c, status = next(output)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004111 branch_statuses[c.GetBranch()] = status
4112 status = branch_statuses.pop(branch)
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00004113 url = cl.GetIssueURL(short=True)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004114 if url and (not status or status == 'error'):
4115 # The issue probably doesn't exist anymore.
4116 url += ' (broken)'
4117
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004118 color = color_for_status(status)
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00004119 # Turn off bold as well as colors.
4120 END = '\033[0m'
4121 reset = Fore.RESET + END
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004122 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004123 color = ''
4124 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004125 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00004126
Alan Cuttera3be9a52019-03-04 18:50:33 +00004127 branch_display = FormatBranchName(branch)
4128 padding = ' ' * (alignment - len(branch_display))
4129 if not options.no_branch_color:
4130 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00004131
Alan Cuttera3be9a52019-03-04 18:50:33 +00004132 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
4133 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004134
vapiera7fbd5a2016-06-16 09:17:49 -07004135 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00004136 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004137 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00004138 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004139 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004140 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004141 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004142 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004143 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004144 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004145 print('Issue description:')
Edward Lemur6c6827c2020-02-06 21:15:18 +00004146 print(cl.FetchDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004147 return 0
4148
4149
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004150def colorize_CMDstatus_doc():
4151 """To be called once in main() to add colors to git cl status help."""
4152 colors = [i for i in dir(Fore) if i[0].isupper()]
4153
4154 def colorize_line(line):
4155 for color in colors:
4156 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004157 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004158 indent = len(line) - len(line.lstrip(' ')) + 1
4159 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4160 return line
4161
4162 lines = CMDstatus.__doc__.splitlines()
4163 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4164
4165
phajdan.jre328cf92016-08-22 04:12:17 -07004166def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004167 if path == '-':
4168 json.dump(contents, sys.stdout)
4169 else:
4170 with open(path, 'w') as f:
4171 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004172
4173
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004174@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004175@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004176def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004177 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004178
4179 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004180 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004181 parser.add_option('-r', '--reverse', action='store_true',
4182 help='Lookup the branch(es) for the specified issues. If '
4183 'no issues are specified, all branches with mapped '
4184 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004185 parser.add_option('--json',
4186 help='Path to JSON output file, or "-" for stdout.')
dnj@chromium.org406c4402015-03-03 17:22:28 +00004187 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004188
dnj@chromium.org406c4402015-03-03 17:22:28 +00004189 if options.reverse:
4190 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004191 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004192 # Reverse issue lookup.
4193 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004194
4195 git_config = {}
4196 for config in RunGit(['config', '--get-regexp',
4197 r'branch\..*issue']).splitlines():
4198 name, _space, val = config.partition(' ')
4199 git_config[name] = val
4200
dnj@chromium.org406c4402015-03-03 17:22:28 +00004201 for branch in branches:
Edward Lesmes50da7702020-03-30 19:23:43 +00004202 issue = git_config.get(
4203 'branch.%s.%s' % (scm.GIT.ShortBranchName(branch), ISSUE_CONFIG_KEY))
Edward Lemur52969c92020-02-06 18:15:28 +00004204 if issue:
4205 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004206 if not args:
Carlos Caballero81923d62020-07-06 18:22:27 +00004207 args = sorted(issue_branch_map.keys())
phajdan.jre328cf92016-08-22 04:12:17 -07004208 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004209 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00004210 try:
4211 issue_num = int(issue)
4212 except ValueError:
4213 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004214 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00004215 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07004216 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00004217 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004218 if options.json:
4219 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004220 return 0
4221
4222 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00004223 issue = ParseIssueNumberArgument(args[0])
Aaron Gable78753da2017-06-15 10:35:49 -07004224 if not issue.valid:
4225 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4226 'or no argument to list it.\n'
4227 'Maybe you want to run git cl status?')
Edward Lemurf38bc172019-09-03 21:02:13 +00004228 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07004229 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004230 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00004231 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07004232 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4233 if options.json:
4234 write_json(options.json, {
Nodir Turakulov27379632021-03-17 18:53:29 +00004235 'gerrit_host': cl.GetGerritHost(),
4236 'gerrit_project': cl.GetGerritProject(),
Aaron Gable78753da2017-06-15 10:35:49 -07004237 'issue_url': cl.GetIssueURL(),
Nodir Turakulov27379632021-03-17 18:53:29 +00004238 'issue': cl.GetIssue(),
Aaron Gable78753da2017-06-15 10:35:49 -07004239 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004240 return 0
4241
4242
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004243@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004244def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004245 """Shows or posts review comments for any changelist."""
4246 parser.add_option('-a', '--add-comment', dest='comment',
4247 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004248 parser.add_option('-p', '--publish', action='store_true',
4249 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004250 parser.add_option('-i', '--issue', dest='issue',
Edward Lemurf38bc172019-09-03 21:02:13 +00004251 help='review issue id (defaults to current issue).')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004252 parser.add_option('-m', '--machine-readable', dest='readable',
4253 action='store_false', default=True,
4254 help='output comments in a format compatible with '
4255 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004256 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004257 help='File to write JSON summary to, or "-" for stdout')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004258 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004259
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004260 issue = None
4261 if options.issue:
4262 try:
4263 issue = int(options.issue)
4264 except ValueError:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004265 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004266
Edward Lemur934836a2019-09-09 20:16:54 +00004267 cl = Changelist(issue=issue)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004268
4269 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004270 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004271 return 0
4272
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004273 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4274 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004275 for comment in summary:
4276 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004277 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004278 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004279 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004280 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004281 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00004282 elif comment.autogenerated:
4283 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004284 else:
4285 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004286 print('\n%s%s %s%s\n%s' % (
4287 color,
4288 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4289 comment.sender,
4290 Fore.RESET,
4291 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4292
smut@google.comc85ac942015-09-15 16:34:43 +00004293 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004294 def pre_serialize(c):
Edward Lemur79d4f992019-11-11 23:49:02 +00004295 dct = c._asdict().copy()
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004296 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4297 return dct
Edward Lemur79d4f992019-11-11 23:49:02 +00004298 write_json(options.json_file, [pre_serialize(x) for x in summary])
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004299 return 0
4300
4301
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004302@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004303@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004304def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004305 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004306 parser.add_option('-d', '--display', action='store_true',
4307 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004308 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004309 help='New description to set for this issue (- for stdin, '
4310 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004311 parser.add_option('-f', '--force', action='store_true',
4312 help='Delete any unpublished Gerrit edits for this issue '
4313 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004314
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004315 options, args = parser.parse_args(args)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004316
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004317 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004318 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00004319 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004320 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004321 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004322
Edward Lemur934836a2019-09-09 20:16:54 +00004323 kwargs = {}
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004324 if target_issue_arg:
4325 kwargs['issue'] = target_issue_arg.issue
4326 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07004327
4328 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004329 if not cl.GetIssue():
4330 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004331
Edward Lemur678a6842019-10-03 22:25:05 +00004332 if args and not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00004333 logging.info('canonical issue/change URL: %s\n', cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004334
Edward Lemur6c6827c2020-02-06 21:15:18 +00004335 description = ChangeDescription(cl.FetchDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004336
smut@google.com34fb6b12015-07-13 20:03:26 +00004337 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004338 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004339 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004340
4341 if options.new_description:
4342 text = options.new_description
4343 if text == '-':
4344 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004345 elif text == '+':
4346 base_branch = cl.GetCommonAncestorWithUpstream()
Edward Lemura12175c2020-03-09 16:58:26 +00004347 text = _create_description_from_log([base_branch])
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004348
4349 description.set_description(text)
4350 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00004351 description.prompt()
Edward Lemur6c6827c2020-02-06 21:15:18 +00004352 if cl.FetchDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004353 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004354 return 0
4355
4356
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004357@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004358def CMDlint(parser, args):
4359 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004360 parser.add_option('--filter', action='append', metavar='-x,+y',
4361 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004362 options, args = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004363
4364 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004365 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004366 try:
4367 import cpplint
4368 import cpplint_chromium
4369 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004370 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004371 return 1
4372
4373 # Change the current working directory before calling lint so that it
4374 # shows the correct base.
4375 previous_cwd = os.getcwd()
4376 os.chdir(settings.GetRoot())
4377 try:
Edward Lemur934836a2019-09-09 20:16:54 +00004378 cl = Changelist()
Edward Lemur2c62b332020-03-12 22:12:33 +00004379 files = cl.GetAffectedFiles(cl.GetCommonAncestorWithUpstream())
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004380 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004381 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004382 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004383
Lei Zhangb8c62cf2020-07-15 20:09:37 +00004384 # Process cpplint arguments, if any.
4385 filters = presubmit_canned_checks.GetCppLintFilters(options.filter)
4386 command = ['--filter=' + ','.join(filters)] + args + files
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004387 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004388
Lei Zhang379d1ad2020-07-15 19:40:06 +00004389 include_regex = re.compile(settings.GetLintRegex())
4390 ignore_regex = re.compile(settings.GetLintIgnoreRegex())
thestig@chromium.org44202a22014-03-11 19:22:18 +00004391 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4392 for filename in filenames:
Lei Zhang379d1ad2020-07-15 19:40:06 +00004393 if not include_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004394 print('Skipping file %s' % filename)
Lei Zhang379d1ad2020-07-15 19:40:06 +00004395 continue
4396
4397 if ignore_regex.match(filename):
4398 print('Ignoring file %s' % filename)
4399 continue
4400
4401 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4402 extra_check_functions)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004403 finally:
4404 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004405 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004406 if cpplint._cpplint_state.error_count != 0:
4407 return 1
4408 return 0
4409
4410
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004411@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004412def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004413 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004414 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004415 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004416 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004417 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004418 parser.add_option('--all', action='store_true',
4419 help='Run checks against all files, not just modified ones')
Josip Sokcevic017544d2022-03-31 23:47:53 +00004420 parser.add_option('--files',
4421 nargs=1,
4422 help='Semicolon-separated list of files to be marked as '
4423 'modified when executing presubmit or post-upload hooks. '
4424 'fnmatch wildcards can also be used.')
Edward Lesmes8e282792018-04-03 18:50:29 -04004425 parser.add_option('--parallel', action='store_true',
4426 help='Run all tests specified by input_api.RunTests in all '
4427 'PRESUBMIT files in parallel.')
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00004428 parser.add_option('--resultdb', action='store_true',
4429 help='Run presubmit checks in the ResultSink environment '
4430 'and send results to the ResultDB database.')
Saagar Sanghavi03b15132020-08-10 16:43:41 +00004431 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004432 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004433
sbc@chromium.org71437c02015-04-09 19:29:40 +00004434 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004435 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004436 return 1
4437
Edward Lemur934836a2019-09-09 20:16:54 +00004438 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004439 if args:
4440 base_branch = args[0]
4441 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004442 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004443 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004444
Bruce Dawsoneb8426e2022-08-05 23:58:15 +00004445 start = time.time()
4446 try:
4447 if not 'PRESUBMIT_SKIP_NETWORK' in os.environ and cl.GetIssue():
4448 description = cl.FetchDescription()
4449 else:
4450 description = _create_description_from_log([base_branch])
4451 except Exception as e:
4452 print('Failed to fetch CL description - %s' % str(e))
Edward Lemura12175c2020-03-09 16:58:26 +00004453 description = _create_description_from_log([base_branch])
Bruce Dawsoneb8426e2022-08-05 23:58:15 +00004454 elapsed = time.time() - start
4455 if elapsed > 5:
4456 print('%.1f s to get CL description.' % elapsed)
Aaron Gable8076c282017-11-29 14:39:41 -08004457
Bruce Dawson13acea32022-05-03 22:13:08 +00004458 if not base_branch:
4459 if not options.force:
4460 print('use --force to check even when not on a branch.')
4461 return 1
4462 base_branch = 'HEAD'
4463
Josip Sokcevic017544d2022-03-31 23:47:53 +00004464 cl.RunHook(committing=not options.upload,
4465 may_prompt=False,
4466 verbose=options.verbose,
4467 parallel=options.parallel,
4468 upstream=base_branch,
4469 description=description,
4470 all_files=options.all,
4471 files=options.files,
4472 resultdb=options.resultdb,
4473 realm=options.realm)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004474 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004475
4476
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004477def GenerateGerritChangeId(message):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004478 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004479
4480 Works the same way as
4481 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4482 but can be called on demand on all platforms.
4483
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004484 The basic idea is to generate git hash of a state of the tree, original
4485 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004486 """
4487 lines = []
4488 tree_hash = RunGitSilent(['write-tree'])
4489 lines.append('tree %s' % tree_hash.strip())
4490 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4491 if code == 0:
4492 lines.append('parent %s' % parent.strip())
4493 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4494 lines.append('author %s' % author.strip())
4495 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4496 lines.append('committer %s' % committer.strip())
4497 lines.append('')
4498 # Note: Gerrit's commit-hook actually cleans message of some lines and
4499 # whitespace. This code is not doing this, but it clearly won't decrease
4500 # entropy.
4501 lines.append(message)
4502 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00004503 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004504 return 'I%s' % change_hash.strip()
4505
4506
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004507def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004508 """Computes the remote branch ref to use for the CL.
4509
4510 Args:
4511 remote (str): The git remote for the CL.
4512 remote_branch (str): The git remote branch for the CL.
4513 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004514 """
4515 if not (remote and remote_branch):
4516 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004517
wittman@chromium.org455dc922015-01-26 20:15:50 +00004518 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004519 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004520 # refs, which are then translated into the remote full symbolic refs
4521 # below.
4522 if '/' not in target_branch:
4523 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4524 else:
4525 prefix_replacements = (
4526 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4527 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4528 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4529 )
4530 match = None
4531 for regex, replacement in prefix_replacements:
4532 match = re.search(regex, target_branch)
4533 if match:
4534 remote_branch = target_branch.replace(match.group(0), replacement)
4535 break
4536 if not match:
4537 # This is a branch path but not one we recognize; use as-is.
4538 remote_branch = target_branch
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00004539 # pylint: disable=consider-using-get
rmistry@google.comc68112d2015-03-03 12:48:06 +00004540 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00004541 # pylint: enable=consider-using-get
rmistry@google.comc68112d2015-03-03 12:48:06 +00004542 # Handle the refs that need to land in different refs.
4543 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004544
wittman@chromium.org455dc922015-01-26 20:15:50 +00004545 # Create the true path to the remote branch.
4546 # Does the following translation:
4547 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
Josip Sokcevicc39ab992020-09-24 20:09:15 +00004548 # * refs/remotes/origin/main -> refs/heads/main
wittman@chromium.org455dc922015-01-26 20:15:50 +00004549 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4550 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4551 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4552 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4553 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4554 'refs/heads/')
4555 elif remote_branch.startswith('refs/remotes/branch-heads'):
4556 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004557
wittman@chromium.org455dc922015-01-26 20:15:50 +00004558 return remote_branch
4559
4560
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004561def cleanup_list(l):
4562 """Fixes a list so that comma separated items are put as individual items.
4563
4564 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4565 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4566 """
4567 items = sum((i.split(',') for i in l), [])
4568 stripped_items = (i.strip() for i in items)
4569 return sorted(filter(None, stripped_items))
4570
4571
Aaron Gable4db38df2017-11-03 14:59:07 -07004572@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004573@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004574def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004575 """Uploads the current changelist to codereview.
4576
4577 Can skip dependency patchset uploads for a branch by running:
4578 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004579 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00004580 git config --unset branch.branch_name.skip-deps-uploads
4581 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004582
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004583 If the name of the checked out branch starts with "bug-" or "fix-" followed
4584 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02004585 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004586
4587 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004588 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004589 [git-cl] add support for hashtags
4590 Foo bar: implement foo
4591 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004592 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004593 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4594 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004595 parser.add_option('--bypass-watchlists', action='store_true',
4596 dest='bypass_watchlists',
4597 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004598 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004599 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004600 parser.add_option('--message', '-m', dest='message',
4601 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004602 parser.add_option('-b', '--bug',
4603 help='pre-populate the bug number(s) for this issue. '
4604 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004605 parser.add_option('--message-file', dest='message_file',
4606 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004607 parser.add_option('--title', '-t', dest='title',
4608 help='title for patchset')
Mario Bianuccicebfb4e2020-07-22 23:08:16 +00004609 parser.add_option('-T', '--skip-title', action='store_true',
4610 dest='skip_title',
4611 help='Use the most recent commit message as the title of '
4612 'the patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004613 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004614 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004615 help='reviewer email addresses')
4616 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004617 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004618 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004619 parser.add_option('--hashtag', dest='hashtags',
4620 action='append', default=[],
4621 help=('Gerrit hashtag for new CL; '
4622 'can be applied multiple times'))
Thiago Perrottab0fb8d52022-08-30 21:26:19 +00004623 parser.add_option('-s',
4624 '--send-mail',
4625 '--send-email',
4626 dest='send_mail',
4627 action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004628 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004629 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004630 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004631 metavar='TARGET',
4632 help='Apply CL to remote ref TARGET. ' +
Josip Sokcevicc39ab992020-09-24 20:09:15 +00004633 'Default: remote branch head, or main')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004634 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004635 help='Squash multiple commits into one')
Mike Frysingera989d552019-08-14 20:51:23 +00004636 parser.add_option('--no-squash', action='store_false', dest='squash',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004637 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004638 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004639 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004640 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4641 const='R', help='add a set of OWNERS to R')
Thiago Perrottab0fb8d52022-08-30 21:26:19 +00004642 parser.add_option('-c',
4643 '--use-commit-queue',
4644 action='store_true',
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004645 default=False,
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004646 help='tell the CQ to commit this patchset; '
Thiago Perrottab0fb8d52022-08-30 21:26:19 +00004647 'implies --send-mail')
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004648 parser.add_option('-d', '--cq-dry-run',
4649 action='store_true', default=False,
rmistry@google.comef966222015-04-07 11:15:01 +00004650 help='Send the patchset to do a CQ dry run right after '
4651 'upload.')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004652 parser.add_option(
4653 '-q',
4654 '--cq-quick-run',
4655 action='store_true',
4656 default=False,
4657 help='Send the patchset to do a CQ quick run right after '
4658 'upload (https://source.chromium.org/chromium/chromium/src/+/main:do'
4659 'cs/cq_quick_run.md) (chromium only).')
Edward Lesmes10c3dd62021-02-08 21:13:57 +00004660 parser.add_option('--set-bot-commit', action='store_true',
4661 help=optparse.SUPPRESS_HELP)
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00004662 parser.add_option('--preserve-tryjobs', action='store_true',
4663 help='instruct the CQ to let tryjobs running even after '
4664 'new patchsets are uploaded instead of canceling '
4665 'prior patchset\' tryjobs')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004666 parser.add_option('--dependencies', action='store_true',
4667 help='Uploads CLs of all the local branches that depend on '
4668 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004669 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4670 help='Sends your change to the CQ after an approval. Only '
4671 'works on repos that have the Auto-Submit label '
4672 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004673 parser.add_option('--parallel', action='store_true',
4674 help='Run all tests specified by input_api.RunTests in all '
4675 'PRESUBMIT files in parallel.')
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004676 parser.add_option('--no-autocc', action='store_true',
4677 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004678 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004679 help='Set the review private. This implies --no-autocc.')
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004680 parser.add_option('-R', '--retry-failed', action='store_true',
4681 help='Retry failed tryjobs from old patchset immediately '
4682 'after uploading new patchset. Cannot be used with '
4683 '--use-commit-queue or --cq-dry-run.')
Dan Beamd8b04ca2019-10-10 21:23:26 +00004684 parser.add_option('--fixed', '-x',
4685 help='List of bugs that will be commented on and marked '
4686 'fixed (pre-populates "Fixed:" tag). Same format as '
4687 '-b option / "Bug:" tag. If fixing several issues, '
4688 'separate with commas.')
Josipe827b0f2020-01-30 00:07:20 +00004689 parser.add_option('--edit-description', action='store_true', default=False,
4690 help='Modify description before upload. Cannot be used '
4691 'with --force. It is a noop when --no-squash is set '
4692 'or a new commit is created.')
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00004693 parser.add_option('--git-completion-helper', action="store_true",
4694 help=optparse.SUPPRESS_HELP)
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00004695 parser.add_option('-o',
4696 '--push-options',
4697 action='append',
4698 default=[],
4699 help='Transmit the given string to the server when '
4700 'performing git push (pass-through). See git-push '
4701 'documentation for more details.')
Gregory Nisbet48d9e1e2021-04-15 23:35:54 +00004702 parser.add_option('--no-add-changeid',
4703 action='store_true',
4704 dest='no_add_changeid',
4705 help='Do not add change-ids to messages.')
Brian Sheedy7326ca22022-11-02 18:36:17 +00004706 parser.add_option('--no-python2-post-upload-hooks',
4707 action='store_true',
4708 help='Only run post-upload hooks in Python 3.')
Joanna Wangd75fc882023-03-01 21:53:34 +00004709 parser.add_option('--cherry-pick-stacked',
4710 '--cp',
4711 dest='cherry_pick_stacked',
4712 action='store_true',
4713 help='If parent branch has un-uploaded updates, '
4714 'automatically skip parent branches and just upload '
4715 'the current branch cherry-pick on its parent\'s last '
4716 'uploaded commit. Allows users to skip the potential '
4717 'interactive confirmation step.')
Joanna Wanga1abbed2023-01-24 01:41:05 +00004718 # TODO(b/265929888): Add --wip option of --cl-status option.
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004719
rmistry@google.com2dd99862015-06-22 12:22:18 +00004720 orig_args = args
ukai@chromium.orge8077812012-02-03 03:41:46 +00004721 (options, args) = parser.parse_args(args)
4722
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00004723 if options.git_completion_helper:
Edward Lesmesb7db1832020-06-22 20:22:27 +00004724 print(' '.join(opt.get_opt_string() for opt in parser.option_list
4725 if opt.help != optparse.SUPPRESS_HELP))
4726 return
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00004727
sbc@chromium.org71437c02015-04-09 19:29:40 +00004728 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004729 return 1
4730
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004731 options.reviewers = cleanup_list(options.reviewers)
4732 options.cc = cleanup_list(options.cc)
4733
Josipe827b0f2020-01-30 00:07:20 +00004734 if options.edit_description and options.force:
4735 parser.error('Only one of --force and --edit-description allowed')
4736
tandriib80458a2016-06-23 12:20:07 -07004737 if options.message_file:
4738 if options.message:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004739 parser.error('Only one of --message and --message-file allowed.')
tandriib80458a2016-06-23 12:20:07 -07004740 options.message = gclient_utils.FileRead(options.message_file)
tandriib80458a2016-06-23 12:20:07 -07004741
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004742 if ([options.cq_dry_run,
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004743 options.cq_quick_run,
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004744 options.use_commit_queue,
4745 options.retry_failed].count(True) > 1):
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004746 parser.error('Only one of --use-commit-queue, --cq-dry-run, --cq-quick-run '
4747 'or --retry-failed is allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07004748
Mario Bianuccicebfb4e2020-07-22 23:08:16 +00004749 if options.skip_title and options.title:
4750 parser.error('Only one of --title and --skip-title allowed.')
4751
Aaron Gableedbc4132017-09-11 13:22:28 -07004752 if options.use_commit_queue:
4753 options.send_mail = True
4754
Edward Lesmes0dd54822020-03-26 18:24:25 +00004755 if options.squash is None:
4756 # Load default for user, repo, squash=true, in this order.
4757 options.squash = settings.GetSquashGerritUploads()
4758
Joanna Wang5051ffe2023-03-01 22:24:07 +00004759 cl = Changelist(branchref=options.target_branch)
4760
4761 # Warm change details cache now to avoid RPCs later, reducing latency for
4762 # developers.
4763 if cl.GetIssue():
4764 cl._GetChangeDetail(
4765 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
4766
4767 if options.retry_failed and not cl.GetIssue():
4768 print('No previous patchsets, so --retry-failed has no effect.')
4769 options.retry_failed = False
4770
4771 remote = cl.GetRemoteUrl()
4772 dogfood_stacked_changes = (os.environ.get('DOGFOOD_STACKED_CHANGES')
4773 not in ['1', '0']
4774 and any(repo in remote
4775 for repo in DOGFOOD_STACKED_CHANGES_REPOS))
4776
4777 if dogfood_stacked_changes:
4778 print('This repo has been enrolled in the stacked changes dogfood. '
4779 'To opt-out use `export DOGFOOD_STACKED_CHANGES=0`. '
4780 'File bugs at https://bit.ly/3Y6opoI')
4781
4782 if options.squash and (dogfood_stacked_changes
4783 or os.environ.get('DOGFOOD_STACKED_CHANGES') == '1'):
Joanna Wangdd12deb2023-01-26 20:43:28 +00004784 if options.dependencies:
4785 parser.error('--dependencies is not available for this workflow.')
Joanna Wang18de1f62023-01-21 01:24:24 +00004786
Joanna Wangd75fc882023-03-01 21:53:34 +00004787 if options.cherry_pick_stacked:
4788 try:
4789 orig_args.remove('--cherry-pick-stacked')
4790 except ValueError:
4791 orig_args.remove('--cp')
Joanna Wang18de1f62023-01-21 01:24:24 +00004792 UploadAllSquashed(options, orig_args)
4793 return 0
4794
Joanna Wangd75fc882023-03-01 21:53:34 +00004795 if options.cherry_pick_stacked:
4796 parser.error('--cherry-pick-stacked is not available for this workflow.')
4797
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004798 # cl.GetMostRecentPatchset uses cached information, and can return the last
4799 # patchset before upload. Calling it here makes it clear that it's the
4800 # last patchset before upload. Note that GetMostRecentPatchset will fail
4801 # if no CL has been uploaded yet.
4802 if options.retry_failed:
4803 patchset = cl.GetMostRecentPatchset()
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004804
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004805 ret = cl.CMDUpload(options, args, orig_args)
4806
4807 if options.retry_failed:
4808 if ret != 0:
4809 print('Upload failed, so --retry-failed has no effect.')
4810 return ret
Joanna Wanga8db0cb2023-01-24 15:43:17 +00004811 builds, _ = _fetch_latest_builds(cl,
4812 DEFAULT_BUILDBUCKET_HOST,
4813 latest_patchset=patchset)
Edward Lemur45768512020-03-02 19:03:14 +00004814 jobs = _filter_failed_for_retry(builds)
4815 if len(jobs) == 0:
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004816 print('No failed tryjobs, so --retry-failed has no effect.')
4817 return ret
Quinten Yearsley777660f2020-03-04 23:37:06 +00004818 _trigger_tryjobs(cl, jobs, options, patchset + 1)
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004819
4820 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00004821
4822
Joanna Wang18de1f62023-01-21 01:24:24 +00004823def UploadAllSquashed(options, orig_args):
4824 # type: (optparse.Values, Sequence[str]) -> Tuple[Sequence[Changelist], bool]
4825 """Uploads the current and upstream branches (if necessary)."""
Joanna Wangc710e2d2023-01-25 14:53:22 +00004826 cls, cherry_pick_current = _UploadAllPrecheck(options, orig_args)
Joanna Wang18de1f62023-01-21 01:24:24 +00004827
Joanna Wangc710e2d2023-01-25 14:53:22 +00004828 # Create commits.
4829 uploads_by_cl = [] #type: Sequence[Tuple[Changelist, _NewUpload]]
4830 if cherry_pick_current:
4831 parent = cls[1]._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
4832 new_upload = cls[0].PrepareCherryPickSquashedCommit(options, parent)
4833 uploads_by_cl.append((cls[0], new_upload))
4834 else:
Joanna Wangc710e2d2023-01-25 14:53:22 +00004835 ordered_cls = list(reversed(cls))
4836
Joanna Wang6215dd02023-02-07 15:58:03 +00004837 cl = ordered_cls[0]
Joanna Wang7603f042023-03-01 22:17:36 +00004838 # We can only support external changes when we're only uploading one
4839 # branch.
4840 parent = cl._UpdateWithExternalChanges() if len(ordered_cls) == 1 else None
4841 if parent is None:
4842 origin = '.'
4843 branch = cl.GetBranch()
Joanna Wang74c53b62023-03-01 22:00:22 +00004844
Joanna Wang7603f042023-03-01 22:17:36 +00004845 while origin == '.':
4846 # Search for cl's closest ancestor with a gerrit hash.
4847 origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(branch)
4848 if origin == '.':
4849 upstream_branch = scm.GIT.ShortBranchName(upstream_branch_ref)
4850
4851 # Support the `git merge` and `git pull` workflow.
4852 if upstream_branch in ['master', 'main']:
4853 parent = cl.GetCommonAncestorWithUpstream()
4854 else:
4855 parent = scm.GIT.GetBranchConfig(settings.GetRoot(),
4856 upstream_branch,
4857 GERRIT_SQUASH_HASH_CONFIG_KEY)
4858 if parent:
4859 break
4860 branch = upstream_branch
4861 else:
4862 # Either the root of the tree is the cl's direct parent and the while
4863 # loop above only found empty branches between cl and the root of the
4864 # tree.
4865 parent = cl.GetCommonAncestorWithUpstream()
Joanna Wang6215dd02023-02-07 15:58:03 +00004866
Joanna Wangc710e2d2023-01-25 14:53:22 +00004867 for i, cl in enumerate(ordered_cls):
4868 # If we're in the middle of the stack, set end_commit to downstream's
4869 # direct ancestor.
4870 if i + 1 < len(ordered_cls):
4871 child_base_commit = ordered_cls[i + 1].GetCommonAncestorWithUpstream()
4872 else:
4873 child_base_commit = None
4874 new_upload = cl.PrepareSquashedCommit(options,
Joanna Wang6215dd02023-02-07 15:58:03 +00004875 parent,
Joanna Wangc710e2d2023-01-25 14:53:22 +00004876 end_commit=child_base_commit)
4877 uploads_by_cl.append((cl, new_upload))
Joanna Wangc710e2d2023-01-25 14:53:22 +00004878 parent = new_upload.commit_to_push
4879
4880 # Create refspec options
4881 cl, new_upload = uploads_by_cl[-1]
4882 refspec_opts = cl._GetRefSpecOptions(
4883 options,
4884 new_upload.change_desc,
Joanna Wang562481d2023-01-26 21:57:14 +00004885 multi_change_upload=len(uploads_by_cl) > 1,
4886 dogfood_path=True)
Joanna Wangc710e2d2023-01-25 14:53:22 +00004887 refspec_suffix = ''
4888 if refspec_opts:
4889 refspec_suffix = '%' + ','.join(refspec_opts)
4890 assert ' ' not in refspec_suffix, ('spaces not allowed in refspec: "%s"' %
4891 refspec_suffix)
4892
4893 remote, remote_branch = cl.GetRemoteBranch()
4894 branch = GetTargetRef(remote, remote_branch, options.target_branch)
4895 refspec = '%s:refs/for/%s%s' % (new_upload.commit_to_push, branch,
4896 refspec_suffix)
4897
4898 # Git push
4899 git_push_metadata = {
4900 'gerrit_host':
4901 cl.GetGerritHost(),
4902 'title':
4903 options.title or '<untitled>',
4904 'change_id':
4905 git_footers.get_footer_change_id(new_upload.change_desc.description),
4906 'description':
4907 new_upload.change_desc.description,
4908 }
4909 push_stdout = cl._RunGitPushWithTraces(refspec, refspec_opts,
4910 git_push_metadata)
4911
4912 # Post push updates
4913 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
4914 change_numbers = [
4915 m.group(1) for m in map(regex.match, push_stdout.splitlines()) if m
4916 ]
4917
4918 for i, (cl, new_upload) in enumerate(uploads_by_cl):
4919 cl.PostUploadUpdates(options, new_upload, change_numbers[i])
4920
4921 return 0
Joanna Wang18de1f62023-01-21 01:24:24 +00004922
4923
4924def _UploadAllPrecheck(options, orig_args):
4925 # type: (optparse.Values, Sequence[str]) -> Tuple[Sequence[Changelist], bool]
4926 """Checks the state of the tree and gives the user uploading options
4927
4928 Returns: A tuple of the ordered list of changes that have new commits
4929 since their last upload and a boolean of whether the user wants to
4930 cherry-pick and upload the current branch instead of uploading all cls.
4931 """
Joanna Wang6b98cdc2023-02-16 00:37:20 +00004932 cl = Changelist()
4933 if cl.GetBranch() is None:
4934 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
4935
Joanna Wang18de1f62023-01-21 01:24:24 +00004936 branch_ref = None
4937 cls = []
4938 must_upload_upstream = False
Joanna Wang6215dd02023-02-07 15:58:03 +00004939 first_pass = True
Joanna Wang18de1f62023-01-21 01:24:24 +00004940
4941 Changelist._GerritCommitMsgHookCheck(offer_removal=not options.force)
4942
4943 while True:
4944 if len(cls) > _MAX_STACKED_BRANCHES_UPLOAD:
4945 DieWithError(
4946 'More than %s branches in the stack have not been uploaded.\n'
4947 'Are your branches in a misconfigured state?\n'
4948 'If not, please upload some upstream changes first.' %
4949 (_MAX_STACKED_BRANCHES_UPLOAD))
4950
4951 cl = Changelist(branchref=branch_ref)
Joanna Wang18de1f62023-01-21 01:24:24 +00004952
Joanna Wang6215dd02023-02-07 15:58:03 +00004953 # Only add CL if it has anything to commit.
4954 base_commit = cl.GetCommonAncestorWithUpstream()
4955 end_commit = RunGit(['rev-parse', cl.GetBranchRef()]).strip()
4956
4957 diff = RunGitSilent(['diff', '%s..%s' % (base_commit, end_commit)])
4958 if diff:
4959 cls.append(cl)
4960 if (not first_pass and
4961 cl._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY) is None):
4962 # We are mid-stack and the user must upload their upstream branches.
4963 must_upload_upstream = True
4964 elif first_pass: # The current branch has nothing to commit. Exit.
4965 DieWithError('Branch %s has nothing to commit' % cl.GetBranch())
4966 # Else: A mid-stack branch has nothing to commit. We do not add it to cls.
4967 first_pass = False
4968
4969 # Cases below determine if we should continue to traverse up the tree.
Joanna Wang18de1f62023-01-21 01:24:24 +00004970 origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(cl.GetBranch())
Joanna Wang18de1f62023-01-21 01:24:24 +00004971 branch_ref = upstream_branch_ref # set branch for next run.
4972
Joanna Wang6215dd02023-02-07 15:58:03 +00004973 upstream_branch = scm.GIT.ShortBranchName(upstream_branch_ref)
4974 upstream_last_upload = scm.GIT.GetBranchConfig(settings.GetRoot(),
4975 upstream_branch,
4976 LAST_UPLOAD_HASH_CONFIG_KEY)
4977
Joanna Wang18de1f62023-01-21 01:24:24 +00004978 # Case 1: We've reached the beginning of the tree.
4979 if origin != '.':
4980 break
4981
Joanna Wang18de1f62023-01-21 01:24:24 +00004982 # Case 2: If any upstream branches have never been uploaded,
Joanna Wang6215dd02023-02-07 15:58:03 +00004983 # the user MUST upload them unless they are empty. Continue to
4984 # next loop to add upstream if it is not empty.
Joanna Wang18de1f62023-01-21 01:24:24 +00004985 if not upstream_last_upload:
Joanna Wang18de1f62023-01-21 01:24:24 +00004986 continue
4987
Joanna Wang18de1f62023-01-21 01:24:24 +00004988 # Case 3: If upstream's last_upload == cl.base_commit we do
4989 # not need to upload any more upstreams from this point on.
4990 # (Even if there may be diverged branches higher up the tree)
4991 if base_commit == upstream_last_upload:
4992 break
4993
4994 # Case 4: If upstream's last_upload < cl.base_commit we are
4995 # uploading cl and upstream_cl.
4996 # Continue up the tree to check other branch relations.
Joanna Wangab9c6ba2023-01-21 01:46:36 +00004997 if scm.GIT.IsAncestor(upstream_last_upload, base_commit):
Joanna Wang18de1f62023-01-21 01:24:24 +00004998 continue
4999
5000 # Case 5: If cl.base_commit < upstream's last_upload the user
5001 # must rebase before uploading.
Joanna Wangab9c6ba2023-01-21 01:46:36 +00005002 if scm.GIT.IsAncestor(base_commit, upstream_last_upload):
Joanna Wang18de1f62023-01-21 01:24:24 +00005003 DieWithError(
5004 'At least one branch in the stack has diverged from its upstream '
5005 'branch and does not contain its upstream\'s last upload.\n'
5006 'Please rebase the stack with `git rebase-update` before uploading.')
5007
5008 # The tree went through a rebase. LAST_UPLOAD_HASH_CONFIG_KEY no longer has
5009 # any relation to commits in the tree. Continue up the tree until we hit
5010 # the root.
5011
5012 # We assume all cls in the stack have the same auth requirements and only
5013 # check this once.
5014 cls[0].EnsureAuthenticated(force=options.force)
5015
5016 cherry_pick = False
5017 if len(cls) > 1:
Joanna Wangd75fc882023-03-01 21:53:34 +00005018 opt_message = ''
Joanna Wang6215dd02023-02-07 15:58:03 +00005019 branches = ', '.join([cl.branch for cl in cls])
Joanna Wang18de1f62023-01-21 01:24:24 +00005020 if len(orig_args):
Joanna Wangd75fc882023-03-01 21:53:34 +00005021 opt_message = ('options %s will be used for all uploads.\n' % orig_args)
Joanna Wang18de1f62023-01-21 01:24:24 +00005022 if must_upload_upstream:
Joanna Wangd75fc882023-03-01 21:53:34 +00005023 msg = ('At least one parent branch in `%s` has never been uploaded '
5024 'and must be uploaded before/with `%s`.\n' %
5025 (branches, cls[1].branch))
5026 if options.cherry_pick_stacked:
5027 DieWithError(msg)
5028 if not options.force:
5029 confirm_or_exit('\n' + opt_message + msg)
Joanna Wang18de1f62023-01-21 01:24:24 +00005030 else:
Joanna Wangd75fc882023-03-01 21:53:34 +00005031 if options.cherry_pick_stacked:
5032 print('cherry-picking `%s` on %s\'s last upload' %
5033 (cls[0].branch, cls[1].branch))
Joanna Wang18de1f62023-01-21 01:24:24 +00005034 cherry_pick = True
Joanna Wangd75fc882023-03-01 21:53:34 +00005035 elif not options.force:
5036 answer = gclient_utils.AskForData(
5037 '\n' + opt_message +
5038 'Press enter to update branches %s.\nOr type `n` to upload only '
5039 '`%s` cherry-picked on %s\'s last upload:' %
5040 (branches, cls[0].branch, cls[1].branch))
5041 if answer.lower() == 'n':
5042 cherry_pick = True
Joanna Wang18de1f62023-01-21 01:24:24 +00005043 return cls, cherry_pick
5044
5045
Francois Dorayd42c6812017-05-30 15:10:20 -04005046@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005047@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04005048def CMDsplit(parser, args):
5049 """Splits a branch into smaller branches and uploads CLs.
5050
5051 Creates a branch and uploads a CL for each group of files modified in the
5052 current branch that share a common OWNERS file. In the CL description and
Edward Lemurac5c55f2020-02-29 00:17:16 +00005053 comment, the string '$directory', is replaced with the directory containing
5054 the shared OWNERS file.
Francois Dorayd42c6812017-05-30 15:10:20 -04005055 """
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005056 parser.add_option('-d', '--description', dest='description_file',
5057 help='A text file containing a CL description in which '
5058 '$directory will be replaced by each CL\'s directory.')
5059 parser.add_option('-c', '--comment', dest='comment_file',
5060 help='A text file containing a CL comment.')
5061 parser.add_option('-n', '--dry-run', dest='dry_run', action='store_true',
Chris Watkinsba28e462017-12-13 11:22:17 +11005062 default=False,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005063 help='List the files and reviewers for each CL that would '
5064 'be created, but don\'t create branches or CLs.')
5065 parser.add_option('--cq-dry-run', action='store_true',
5066 help='If set, will do a cq dry run for each uploaded CL. '
5067 'Please be careful when doing this; more than ~10 CLs '
5068 'has the potential to overload our build '
5069 'infrastructure. Try to upload these not during high '
5070 'load times (usually 11-3 Mountain View time). Email '
5071 'infra-dev@chromium.org with any questions.')
Takuto Ikuta51eca592019-02-14 19:40:52 +00005072 parser.add_option('-a', '--enable-auto-submit', action='store_true',
5073 default=True,
5074 help='Sends your change to the CQ after an approval. Only '
5075 'works on repos that have the Auto-Submit label '
5076 'enabled')
Daniel Cheng403c44e2022-10-05 22:24:58 +00005077 parser.add_option('--max-depth',
5078 type='int',
5079 default=0,
5080 help='The max depth to look for OWNERS files. Useful for '
5081 'controlling the granularity of the split CLs, e.g. '
5082 '--max-depth=1 will only split by top-level '
5083 'directory. Specifying a value less than 1 means no '
5084 'limit on max depth.')
Francois Dorayd42c6812017-05-30 15:10:20 -04005085 options, _ = parser.parse_args(args)
5086
5087 if not options.description_file:
5088 parser.error('No --description flag specified.')
5089
5090 def WrappedCMDupload(args):
5091 return CMDupload(OptionParser(), args)
5092
Daniel Cheng403c44e2022-10-05 22:24:58 +00005093 return split_cl.SplitCl(options.description_file, options.comment_file,
5094 Changelist, WrappedCMDupload, options.dry_run,
5095 options.cq_dry_run, options.enable_auto_submit,
5096 options.max_depth, settings.GetRoot())
Francois Dorayd42c6812017-05-30 15:10:20 -04005097
5098
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005099@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005100@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005101def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005102 """DEPRECATED: Used to commit the current changelist via git-svn."""
5103 message = ('git-cl no longer supports committing to SVN repositories via '
5104 'git-svn. You probably want to use `git cl land` instead.')
5105 print(message)
5106 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005107
5108
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005109@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005110@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005111def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005112 """Commits the current changelist via git.
5113
5114 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5115 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005116 """
5117 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5118 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07005119 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005120 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04005121 parser.add_option('--parallel', action='store_true',
5122 help='Run all tests specified by input_api.RunTests in all '
5123 'PRESUBMIT files in parallel.')
Saagar Sanghavi03b15132020-08-10 16:43:41 +00005124 parser.add_option('--resultdb', action='store_true',
5125 help='Run presubmit checks in the ResultSink environment '
5126 'and send results to the ResultDB database.')
5127 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005128 (options, args) = parser.parse_args(args)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005129
Edward Lemur934836a2019-09-09 20:16:54 +00005130 cl = Changelist()
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005131
Robert Iannucci2e73d432018-03-14 01:10:47 -07005132 if not cl.GetIssue():
5133 DieWithError('You must upload the change first to Gerrit.\n'
5134 ' If you would rather have `git cl land` upload '
5135 'automatically for you, see http://crbug.com/642759')
Saagar Sanghavi03b15132020-08-10 16:43:41 +00005136 return cl.CMDLand(options.force, options.bypass_hooks, options.verbose,
5137 options.parallel, options.resultdb, options.realm)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005138
5139
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005140@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005141@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005142def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005143 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005144 parser.add_option('-b', dest='newbranch',
5145 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005146 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005147 help='overwrite state on the current or chosen branch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005148 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
Edward Lemurf38bc172019-09-03 21:02:13 +00005149 help='don\'t commit after patch applies.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005150
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005151 group = optparse.OptionGroup(
5152 parser,
5153 'Options for continuing work on the current issue uploaded from a '
5154 'different clone (e.g. different machine). Must be used independently '
5155 'from the other options. No issue number should be specified, and the '
5156 'branch must have an issue number associated with it')
5157 group.add_option('--reapply', action='store_true', dest='reapply',
5158 help='Reset the branch and reapply the issue.\n'
5159 'CAUTION: This will undo any local changes in this '
5160 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005161
5162 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005163 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005164 parser.add_option_group(group)
5165
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005166 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005167
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005168 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005169 if options.newbranch:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005170 parser.error('--reapply works on the current branch only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005171 if len(args) > 0:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005172 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005173
Edward Lemur934836a2019-09-09 20:16:54 +00005174 cl = Changelist()
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005175 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005176 parser.error('Current branch must have an associated issue.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005177
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005178 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005179 if upstream is None:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005180 parser.error('No upstream branch specified. Cannot reset branch.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005181
5182 RunGit(['reset', '--hard', upstream])
5183 if options.pull:
5184 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005185
Edward Lemur678a6842019-10-03 22:25:05 +00005186 target_issue_arg = ParseIssueNumberArgument(cl.GetIssue())
Joanna Wang44e9bee2023-01-25 21:51:42 +00005187 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit,
5188 options.force, False)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005189
5190 if len(args) != 1 or not args[0]:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005191 parser.error('Must specify issue number or URL.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005192
Edward Lemurf38bc172019-09-03 21:02:13 +00005193 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005194 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005195 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005196
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005197 # We don't want uncommitted changes mixed up with the patch.
5198 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005199 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005200
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005201 if options.newbranch:
5202 if options.force:
5203 RunGit(['branch', '-D', options.newbranch],
5204 stderr=subprocess2.PIPE, error_ok=True)
Edward Lemur84101642020-02-21 21:40:34 +00005205 git_new_branch.create_new_branch(options.newbranch)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005206
Edward Lemur678a6842019-10-03 22:25:05 +00005207 cl = Changelist(
5208 codereview_host=target_issue_arg.hostname, issue=target_issue_arg.issue)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005209
Edward Lemur678a6842019-10-03 22:25:05 +00005210 if not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00005211 print('canonical issue/change URL: %s\n' % cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005212
Bruce Dawsonf362f6f2021-02-18 23:15:17 +00005213 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit,
5214 options.force, options.newbranch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005215
5216
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005217def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005218 """Fetches the tree status and returns either 'open', 'closed',
5219 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005220 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005221 if url:
Daniel McArdle8b4eeff2020-07-20 17:02:47 +00005222 status = str(urllib.request.urlopen(url).read().lower())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005223 if status.find('closed') != -1 or status == '0':
5224 return 'closed'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005225
5226 if status.find('open') != -1 or status == '1':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005227 return 'open'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005228
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005229 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005230 return 'unset'
5231
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005232
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005233def GetTreeStatusReason():
5234 """Fetches the tree status from a json url and returns the message
5235 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005236 url = settings.GetTreeStatusUrl()
Daniel McArdle8b4eeff2020-07-20 17:02:47 +00005237 json_url = urllib.parse.urljoin(url, '/current?format=json')
Edward Lemur79d4f992019-11-11 23:49:02 +00005238 connection = urllib.request.urlopen(json_url)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005239 status = json.loads(connection.read())
5240 connection.close()
5241 return status['message']
5242
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005243
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005244@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005245def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005246 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005247 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005248 status = GetTreeStatus()
5249 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005250 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005251 return 2
5252
vapiera7fbd5a2016-06-16 09:17:49 -07005253 print('The tree is %s' % status)
5254 print()
5255 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005256 if status != 'open':
5257 return 1
5258 return 0
5259
5260
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005261@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005262def CMDtry(parser, args):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005263 """Triggers tryjobs using either Buildbucket or CQ dry run."""
5264 group = optparse.OptionGroup(parser, 'Tryjob options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005265 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005266 '-b', '--bot', action='append',
5267 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5268 'times to specify multiple builders. ex: '
5269 '"-b win_rel -b win_layout". See '
5270 'the try server waterfall for the builders name and the tests '
5271 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005272 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005273 '-B', '--bucket', default='',
Ben Pastene08a30b22022-05-04 17:46:38 +00005274 help=('Buildbucket bucket to send the try requests. Format: '
5275 '"luci.$LUCI_PROJECT.$LUCI_BUCKET". eg: "luci.chromium.try"'))
borenet6c0efe62016-10-19 08:13:29 -07005276 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005277 '-r', '--revision',
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005278 help='Revision to use for the tryjob; default: the revision will '
tandriif7b29d42016-10-07 08:45:41 -07005279 'be determined by the try recipe that builder runs, which usually '
Josip Sokcevicc39ab992020-09-24 20:09:15 +00005280 'defaults to HEAD of origin/master or origin/main')
maruel@chromium.org15192402012-09-06 12:38:29 +00005281 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005282 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005283 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005284 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005285 group.add_option(
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005286 '-q',
5287 '--quick-run',
5288 action='store_true',
5289 default=False,
5290 help='trigger in quick run mode '
5291 '(https://source.chromium.org/chromium/chromium/src/+/main:docs/cq_q'
5292 'uick_run.md) (chromium only).')
5293 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005294 '--category', default='git_cl_try', help='Specify custom build category.')
5295 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005296 '--project',
5297 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005298 'in recipe to determine to which repository or directory to '
5299 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005300 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005301 '-p', '--property', dest='properties', action='append', default=[],
5302 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005303 'key2=value2 etc. The value will be treated as '
5304 'json if decodable, or as string otherwise. '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005305 'NOTE: using this may make your tryjob not usable for CQ, '
5306 'which will then schedule another tryjob with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005307 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005308 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5309 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005310 parser.add_option_group(group)
Quinten Yearsley983111f2019-09-26 17:18:48 +00005311 parser.add_option(
5312 '-R', '--retry-failed', action='store_true', default=False,
5313 help='Retry failed jobs from the latest set of tryjobs. '
5314 'Not allowed with --bucket and --bot options.')
Edward Lemur52969c92020-02-06 18:15:28 +00005315 parser.add_option(
5316 '-i', '--issue', type=int,
5317 help='Operate on this issue instead of the current branch\'s implicit '
5318 'issue.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005319 options, args = parser.parse_args(args)
5320
machenbach@chromium.org45453142015-09-15 08:45:22 +00005321 # Make sure that all properties are prop=value pairs.
5322 bad_params = [x for x in options.properties if '=' not in x]
5323 if bad_params:
5324 parser.error('Got properties with missing "=": %s' % bad_params)
5325
maruel@chromium.org15192402012-09-06 12:38:29 +00005326 if args:
5327 parser.error('Unknown arguments: %s' % args)
5328
Edward Lemur934836a2019-09-09 20:16:54 +00005329 cl = Changelist(issue=options.issue)
maruel@chromium.org15192402012-09-06 12:38:29 +00005330 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005331 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005332
Edward Lemurf38bc172019-09-03 21:02:13 +00005333 # HACK: warm up Gerrit change detail cache to save on RPCs.
Edward Lemur125d60a2019-09-13 18:25:41 +00005334 cl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005335
tandriie113dfd2016-10-11 10:20:12 -07005336 error_message = cl.CannotTriggerTryJobReason()
5337 if error_message:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005338 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005339
Edward Lemur45768512020-03-02 19:03:14 +00005340 if options.bot:
5341 if options.retry_failed:
5342 parser.error('--bot is not compatible with --retry-failed.')
5343 if not options.bucket:
5344 parser.error('A bucket (e.g. "chromium/try") is required.')
5345
5346 triggered = [b for b in options.bot if 'triggered' in b]
5347 if triggered:
5348 parser.error(
5349 'Cannot schedule builds on triggered bots: %s.\n'
5350 'This type of bot requires an initial job from a parent (usually a '
5351 'builder). Schedule a job on the parent instead.\n' % triggered)
5352
5353 if options.bucket.startswith('.master'):
5354 parser.error('Buildbot masters are not supported.')
5355
5356 project, bucket = _parse_bucket(options.bucket)
5357 if project is None or bucket is None:
5358 parser.error('Invalid bucket: %s.' % options.bucket)
5359 jobs = sorted((project, bucket, bot) for bot in options.bot)
5360 elif options.retry_failed:
Quinten Yearsley983111f2019-09-26 17:18:48 +00005361 print('Searching for failed tryjobs...')
Joanna Wanga8db0cb2023-01-24 15:43:17 +00005362 builds, patchset = _fetch_latest_builds(cl, DEFAULT_BUILDBUCKET_HOST)
Quinten Yearsley983111f2019-09-26 17:18:48 +00005363 if options.verbose:
5364 print('Got %d builds in patchset #%d' % (len(builds), patchset))
Edward Lemur45768512020-03-02 19:03:14 +00005365 jobs = _filter_failed_for_retry(builds)
5366 if not jobs:
Quinten Yearsley983111f2019-09-26 17:18:48 +00005367 print('There are no failed jobs in the latest set of jobs '
5368 '(patchset #%d), doing nothing.' % patchset)
5369 return 0
Edward Lemur45768512020-03-02 19:03:14 +00005370 num_builders = len(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +00005371 if num_builders > 10:
5372 confirm_or_exit('There are %d builders with failed builds.'
5373 % num_builders, action='continue')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005374 elif options.quick_run:
5375 print('Scheduling CQ quick run on: %s' % cl.GetIssueURL())
5376 return cl.SetCQState(_CQState.QUICK_RUN)
Quinten Yearsley983111f2019-09-26 17:18:48 +00005377 else:
qyearsley1fdfcb62016-10-24 13:22:03 -07005378 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005379 print('git cl try with no bots now defaults to CQ dry run.')
5380 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5381 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005382
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005383 patchset = cl.GetMostRecentPatchset()
Edward Lemur2c210a42019-09-16 23:58:35 +00005384 try:
Quinten Yearsley777660f2020-03-04 23:37:06 +00005385 _trigger_tryjobs(cl, jobs, options, patchset)
Edward Lemur2c210a42019-09-16 23:58:35 +00005386 except BuildbucketResponseException as ex:
5387 print('ERROR: %s' % ex)
5388 return 1
5389 return 0
maruel@chromium.org15192402012-09-06 12:38:29 +00005390
5391
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005392@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005393def CMDtry_results(parser, args):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005394 """Prints info about results for tryjobs associated with the current CL."""
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005395 group = optparse.OptionGroup(parser, 'Tryjob results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005396 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005397 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005398 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005399 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005400 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005401 '--color', action='store_true', default=setup_color.IS_TTY,
5402 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005403 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005404 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5405 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005406 group.add_option(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005407 '--json', help=('Path of JSON output file to write tryjob results to,'
Stefan Zager1306bd02017-06-22 19:26:46 -07005408 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005409 parser.add_option_group(group)
Edward Lemur52969c92020-02-06 18:15:28 +00005410 parser.add_option(
5411 '-i', '--issue', type=int,
5412 help='Operate on this issue instead of the current branch\'s implicit '
5413 'issue.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005414 options, args = parser.parse_args(args)
5415 if args:
5416 parser.error('Unrecognized args: %s' % ' '.join(args))
5417
Edward Lemur934836a2019-09-09 20:16:54 +00005418 cl = Changelist(issue=options.issue)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005419 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005420 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005421
tandrii221ab252016-10-06 08:12:04 -07005422 patchset = options.patchset
5423 if not patchset:
Gavin Make61ccc52020-11-13 00:12:57 +00005424 patchset = cl.GetMostRecentDryRunPatchset()
tandrii221ab252016-10-06 08:12:04 -07005425 if not patchset:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005426 parser.error('Code review host doesn\'t know about issue %s. '
tandrii221ab252016-10-06 08:12:04 -07005427 'No access to issue or wrong issue number?\n'
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005428 'Either upload first, or pass --patchset explicitly.' %
tandrii221ab252016-10-06 08:12:04 -07005429 cl.GetIssue())
5430
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005431 try:
Joanna Wanga8db0cb2023-01-24 15:43:17 +00005432 jobs = _fetch_tryjobs(cl, DEFAULT_BUILDBUCKET_HOST, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005433 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005434 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005435 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005436 if options.json:
Edward Lemurbaaf6be2019-10-09 18:00:44 +00005437 write_json(options.json, jobs)
qyearsley53f48a12016-09-01 10:45:13 -07005438 else:
Quinten Yearsley777660f2020-03-04 23:37:06 +00005439 _print_tryjobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005440 return 0
5441
5442
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005443@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005444@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005445def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005446 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005447 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005448 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005449 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005450
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005451 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005452 if args:
5453 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005454 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005455 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005456 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005457 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005458
5459 # Clear configured merge-base, if there is one.
5460 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005461 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005462 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005463 return 0
5464
5465
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005466@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005467def CMDweb(parser, args):
5468 """Opens the current CL in the web browser."""
Orr Bernstein0b960582022-12-22 20:16:18 +00005469 parser.add_option('-p',
5470 '--print-only',
5471 action='store_true',
5472 dest='print_only',
5473 help='Only print the Gerrit URL, don\'t open it in the '
5474 'browser.')
5475 (options, args) = parser.parse_args(args)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005476 if args:
5477 parser.error('Unrecognized args: %s' % ' '.join(args))
5478
5479 issue_url = Changelist().GetIssueURL()
5480 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005481 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005482 return 1
5483
Orr Bernstein0b960582022-12-22 20:16:18 +00005484 if options.print_only:
5485 print(issue_url)
5486 return 0
5487
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005488 # Redirect I/O before invoking browser to hide its output. For example, this
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005489 # allows us to hide the "Created new window in existing browser session."
5490 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005491 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005492 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005493 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005494 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005495 os.open(os.devnull, os.O_RDWR)
5496 try:
5497 webbrowser.open(issue_url)
5498 finally:
5499 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005500 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005501 return 0
5502
5503
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005504@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005505def CMDset_commit(parser, args):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00005506 """Sets the commit bit to trigger the CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005507 parser.add_option('-d', '--dry-run', action='store_true',
5508 help='trigger in dry run mode')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005509 parser.add_option(
5510 '-q',
5511 '--quick-run',
5512 action='store_true',
5513 help='trigger in quick run mode '
5514 '(https://source.chromium.org/chromium/chromium/src/+/main:docs/cq_qu'
5515 'ick_run.md) (chromium only).')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005516 parser.add_option('-c', '--clear', action='store_true',
5517 help='stop CQ run, if any')
Edward Lemur52969c92020-02-06 18:15:28 +00005518 parser.add_option(
5519 '-i', '--issue', type=int,
5520 help='Operate on this issue instead of the current branch\'s implicit '
5521 'issue.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005522 options, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005523 if args:
5524 parser.error('Unrecognized args: %s' % ' '.join(args))
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005525 if [options.dry_run, options.quick_run, options.clear].count(True) > 1:
5526 parser.error('Only one of --dry-run, --quick-run, and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005527
Edward Lemur934836a2019-09-09 20:16:54 +00005528 cl = Changelist(issue=options.issue)
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005529 if not cl.GetIssue():
5530 parser.error('Must upload the issue first.')
5531
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005532 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005533 state = _CQState.NONE
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005534 elif options.quick_run:
5535 state = _CQState.QUICK_RUN
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005536 elif options.dry_run:
5537 state = _CQState.DRY_RUN
5538 else:
5539 state = _CQState.COMMIT
tandrii9de9ec62016-07-13 03:01:59 -07005540 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005541 return 0
5542
5543
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005544@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005545def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005546 """Closes the issue."""
Edward Lemur52969c92020-02-06 18:15:28 +00005547 parser.add_option(
5548 '-i', '--issue', type=int,
5549 help='Operate on this issue instead of the current branch\'s implicit '
5550 'issue.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005551 options, args = parser.parse_args(args)
groby@chromium.org411034a2013-02-26 15:12:01 +00005552 if args:
5553 parser.error('Unrecognized args: %s' % ' '.join(args))
Edward Lemur934836a2019-09-09 20:16:54 +00005554 cl = Changelist(issue=options.issue)
groby@chromium.org411034a2013-02-26 15:12:01 +00005555 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005556 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005557 DieWithError('ERROR: No issue to close.')
groby@chromium.org411034a2013-02-26 15:12:01 +00005558 cl.CloseIssue()
5559 return 0
5560
5561
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005562@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005563def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005564 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005565 parser.add_option(
5566 '--stat',
5567 action='store_true',
5568 dest='stat',
5569 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005570 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005571 if args:
5572 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005573
Edward Lemur934836a2019-09-09 20:16:54 +00005574 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005575 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005576 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005577 if not issue:
5578 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005579
Gavin Makbe2e9262022-11-08 23:41:55 +00005580 base = cl._GitGetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY)
Aaron Gablea718c3e2017-08-28 17:47:28 -07005581 if not base:
Gavin Makbe2e9262022-11-08 23:41:55 +00005582 base = cl._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
Aaron Gablea718c3e2017-08-28 17:47:28 -07005583 if not base:
5584 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5585 revision_info = detail['revisions'][detail['current_revision']]
5586 fetch_info = revision_info['fetch']['http']
5587 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5588 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005589
Aaron Gablea718c3e2017-08-28 17:47:28 -07005590 cmd = ['git', 'diff']
5591 if options.stat:
5592 cmd.append('--stat')
5593 cmd.append(base)
5594 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005595
5596 return 0
5597
5598
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005599@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005600def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005601 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005602 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005603 '--ignore-current',
5604 action='store_true',
5605 help='Ignore the CL\'s current reviewers and start from scratch.')
5606 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005607 '--ignore-self',
5608 action='store_true',
5609 help='Do not consider CL\'s author as an owners.')
5610 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005611 '--no-color',
5612 action='store_true',
5613 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005614 parser.add_option(
5615 '--batch',
5616 action='store_true',
5617 help='Do not run interactively, just suggest some')
Yang Guo6e269a02019-06-26 11:17:02 +00005618 # TODO: Consider moving this to another command, since other
5619 # git-cl owners commands deal with owners for a given CL.
5620 parser.add_option(
5621 '--show-all',
5622 action='store_true',
5623 help='Show all owners for a particular file')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005624 options, args = parser.parse_args(args)
5625
Edward Lemur934836a2019-09-09 20:16:54 +00005626 cl = Changelist()
Edward Lesmes50da7702020-03-30 19:23:43 +00005627 author = cl.GetAuthor()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005628
Yang Guo6e269a02019-06-26 11:17:02 +00005629 if options.show_all:
Bruce Dawson97ed44a2020-05-06 17:04:03 +00005630 if len(args) == 0:
5631 print('No files specified for --show-all. Nothing to do.')
5632 return 0
Edward Lesmese1576912021-02-16 21:53:34 +00005633 owners_by_path = cl.owners_client.BatchListOwners(args)
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +00005634 for path in args:
5635 print('Owners for %s:' % path)
5636 print('\n'.join(
5637 ' - %s' % owner
5638 for owner in owners_by_path.get(path, ['No owners found'])))
Yang Guo6e269a02019-06-26 11:17:02 +00005639 return 0
5640
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005641 if args:
5642 if len(args) > 1:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005643 parser.error('Unknown args.')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005644 base_branch = args[0]
5645 else:
5646 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005647 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005648
Edward Lemur2c62b332020-03-12 22:12:33 +00005649 affected_files = cl.GetAffectedFiles(base_branch)
Dirk Prankebf980882017-09-02 15:08:00 -07005650
5651 if options.batch:
Edward Lesmese1576912021-02-16 21:53:34 +00005652 owners = cl.owners_client.SuggestOwners(affected_files, exclude=[author])
5653 print('\n'.join(owners))
Dirk Prankebf980882017-09-02 15:08:00 -07005654 return 0
5655
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005656 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005657 affected_files,
Edward Lemur707d70b2018-02-07 00:50:14 +01005658 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005659 [] if options.ignore_current else cl.GetReviewers(),
Edward Lesmes5cd75472021-02-19 00:34:25 +00005660 cl.owners_client,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005661 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005662 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005663
5664
Aiden Bennerc08566e2018-10-03 17:52:42 +00005665def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005666 """Generates a diff command."""
5667 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005668 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5669
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005670 if allow_prefix:
5671 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5672 # case that diff.noprefix is set in the user's git config.
5673 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5674 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005675 diff_cmd += ['--no-prefix']
5676
5677 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005678
5679 if args:
5680 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005681 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005682 diff_cmd.append(arg)
5683 else:
5684 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005685
5686 return diff_cmd
5687
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005688
Jamie Madill5e96ad12020-01-13 16:08:35 +00005689def _RunClangFormatDiff(opts, clang_diff_files, top_dir, upstream_commit):
5690 """Runs clang-format-diff and sets a return value if necessary."""
5691
5692 if not clang_diff_files:
5693 return 0
5694
5695 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5696 # formatted. This is used to block during the presubmit.
5697 return_value = 0
5698
5699 # Locate the clang-format binary in the checkout
5700 try:
5701 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
5702 except clang_format.NotFoundError as e:
5703 DieWithError(e)
5704
5705 if opts.full or settings.GetFormatFullByDefault():
5706 cmd = [clang_format_tool]
5707 if not opts.dry_run and not opts.diff:
5708 cmd.append('-i')
5709 if opts.dry_run:
5710 for diff_file in clang_diff_files:
5711 with open(diff_file, 'r') as myfile:
5712 code = myfile.read().replace('\r\n', '\n')
5713 stdout = RunCommand(cmd + [diff_file], cwd=top_dir)
5714 stdout = stdout.replace('\r\n', '\n')
5715 if opts.diff:
5716 sys.stdout.write(stdout)
5717 if code != stdout:
5718 return_value = 2
5719 else:
5720 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
5721 if opts.diff:
5722 sys.stdout.write(stdout)
5723 else:
Jamie Madill5e96ad12020-01-13 16:08:35 +00005724 try:
5725 script = clang_format.FindClangFormatScriptInChromiumTree(
5726 'clang-format-diff.py')
5727 except clang_format.NotFoundError as e:
5728 DieWithError(e)
5729
Josip Sokcevic2a827fc2022-03-04 17:51:47 +00005730 cmd = ['vpython3', script, '-p0']
Jamie Madill5e96ad12020-01-13 16:08:35 +00005731 if not opts.dry_run and not opts.diff:
5732 cmd.append('-i')
5733
5734 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
Edward Lemur1a83da12020-03-04 21:18:36 +00005735 diff_output = RunGit(diff_cmd).encode('utf-8')
Jamie Madill5e96ad12020-01-13 16:08:35 +00005736
Edward Lesmes89624cd2020-04-06 17:51:56 +00005737 env = os.environ.copy()
5738 env['PATH'] = (
5739 str(os.path.dirname(clang_format_tool)) + os.pathsep + env['PATH'])
5740 stdout = RunCommand(
5741 cmd, stdin=diff_output, cwd=top_dir, env=env,
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00005742 shell=sys.platform.startswith('win32'))
Jamie Madill5e96ad12020-01-13 16:08:35 +00005743 if opts.diff:
5744 sys.stdout.write(stdout)
5745 if opts.dry_run and len(stdout) > 0:
5746 return_value = 2
5747
5748 return return_value
5749
5750
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005751def _RunRustFmt(opts, rust_diff_files, top_dir, upstream_commit):
5752 """Runs rustfmt. Just like _RunClangFormatDiff returns 2 to indicate that
5753 presubmit checks have failed (and returns 0 otherwise)."""
5754
5755 if not rust_diff_files:
5756 return 0
5757
5758 # Locate the rustfmt binary.
5759 try:
5760 rustfmt_tool = rustfmt.FindRustfmtToolInChromiumTree()
5761 except rustfmt.NotFoundError as e:
5762 DieWithError(e)
5763
5764 # TODO(crbug.com/1231317): Support formatting only the changed lines
5765 # if `opts.full or settings.GetFormatFullByDefault()` is False. See also:
5766 # https://github.com/emilio/rustfmt-format-diff
5767 cmd = [rustfmt_tool]
5768 if opts.dry_run:
5769 cmd.append('--check')
5770 cmd += rust_diff_files
5771 rustfmt_exitcode = subprocess2.call(cmd)
5772
5773 if opts.presubmit and rustfmt_exitcode != 0:
5774 return 2
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005775
5776 return 0
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005777
5778
Olivier Robin0a6b5442022-04-07 07:25:04 +00005779def _RunSwiftFormat(opts, swift_diff_files, top_dir, upstream_commit):
5780 """Runs swift-format. Just like _RunClangFormatDiff returns 2 to indicate
5781 that presubmit checks have failed (and returns 0 otherwise)."""
5782
5783 if not swift_diff_files:
5784 return 0
5785
5786 # Locate the swift-format binary.
5787 try:
5788 swift_format_tool = swift_format.FindSwiftFormatToolInChromiumTree()
5789 except swift_format.NotFoundError as e:
5790 DieWithError(e)
5791
5792 cmd = [swift_format_tool]
5793 if opts.dry_run:
Olivier Robin7f39e3d2022-04-28 08:20:49 +00005794 cmd += ['lint', '-s']
Olivier Robin0a6b5442022-04-07 07:25:04 +00005795 else:
5796 cmd += ['format', '-i']
5797 cmd += swift_diff_files
5798 swift_format_exitcode = subprocess2.call(cmd)
5799
5800 if opts.presubmit and swift_format_exitcode != 0:
5801 return 2
5802
5803 return 0
5804
5805
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005806def MatchingFileType(file_name, extensions):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005807 """Returns True if the file name ends with one of the given extensions."""
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005808 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005809
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005810
enne@chromium.org555cfe42014-01-29 18:21:39 +00005811@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005812@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005813def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005814 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005815 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005816 GN_EXTS = ['.gn', '.gni', '.typemap']
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005817 RUST_EXTS = ['.rs']
Olivier Robin0a6b5442022-04-07 07:25:04 +00005818 SWIFT_EXTS = ['.swift']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005819 parser.add_option('--full', action='store_true',
5820 help='Reformat the full content of all touched files')
Tomasz Åšniatowski58194462021-08-27 17:36:16 +00005821 parser.add_option('--upstream', help='Branch to check against')
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005822 parser.add_option('--dry-run', action='store_true',
5823 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005824 parser.add_option(
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00005825 '--no-clang-format',
5826 dest='clang_format',
5827 action='store_false',
5828 default=True,
5829 help='Disables formatting of various file types using clang-format.')
5830 parser.add_option(
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005831 '--python',
5832 action='store_true',
5833 default=None,
5834 help='Enables python formatting on all python files.')
5835 parser.add_option(
5836 '--no-python',
5837 action='store_true',
Garrett Beaty91a6f332020-01-06 16:57:24 +00005838 default=False,
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005839 help='Disables python formatting on all python files. '
Garrett Beaty91a6f332020-01-06 16:57:24 +00005840 'If neither --python or --no-python are set, python files that have a '
5841 '.style.yapf file in an ancestor directory will be formatted. '
5842 'It is an error to set both.')
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00005843 parser.add_option(
5844 '--js',
5845 action='store_true',
5846 help='Format javascript code with clang-format. '
5847 'Has no effect if --no-clang-format is set.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005848 parser.add_option('--diff', action='store_true',
5849 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005850 parser.add_option('--presubmit', action='store_true',
5851 help='Used when running the script from a presubmit.')
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005852
5853 parser.add_option('--rust-fmt',
5854 dest='use_rust_fmt',
5855 action='store_true',
5856 default=rustfmt.IsRustfmtSupported(),
5857 help='Enables formatting of Rust file types using rustfmt.')
5858 parser.add_option(
5859 '--no-rust-fmt',
5860 dest='use_rust_fmt',
5861 action='store_false',
5862 help='Disables formatting of Rust file types using rustfmt.')
5863
Olivier Robin0a6b5442022-04-07 07:25:04 +00005864 parser.add_option(
5865 '--swift-format',
5866 dest='use_swift_format',
5867 action='store_true',
Olivier Robin7f39e3d2022-04-28 08:20:49 +00005868 default=swift_format.IsSwiftFormatSupported(),
Olivier Robin0a6b5442022-04-07 07:25:04 +00005869 help='Enables formatting of Swift file types using swift-format '
5870 '(macOS host only).')
5871 parser.add_option(
5872 '--no-swift-format',
5873 dest='use_swift_format',
5874 action='store_false',
5875 help='Disables formatting of Swift file types using swift-format.')
5876
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005877 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005878
Garrett Beaty91a6f332020-01-06 16:57:24 +00005879 if opts.python is not None and opts.no_python:
5880 raise parser.error('Cannot set both --python and --no-python')
5881 if opts.no_python:
5882 opts.python = False
5883
Daniel Chengc55eecf2016-12-30 03:11:02 -08005884 # Normalize any remaining args against the current path, so paths relative to
5885 # the current directory are still resolved as expected.
5886 args = [os.path.join(os.getcwd(), arg) for arg in args]
5887
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005888 # git diff generates paths against the root of the repository. Change
5889 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005890 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005891 if rel_base_path:
5892 os.chdir(rel_base_path)
5893
digit@chromium.org29e47272013-05-17 17:01:46 +00005894 # Grab the merge-base commit, i.e. the upstream commit of the current
5895 # branch when it was created or the last time it was rebased. This is
5896 # to cover the case where the user may have called "git fetch origin",
5897 # moving the origin branch to a newer commit, but hasn't rebased yet.
5898 upstream_commit = None
Tomasz Åšniatowski58194462021-08-27 17:36:16 +00005899 upstream_branch = opts.upstream
5900 if not upstream_branch:
5901 cl = Changelist()
5902 upstream_branch = cl.GetUpstreamBranch()
digit@chromium.org29e47272013-05-17 17:01:46 +00005903 if upstream_branch:
5904 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5905 upstream_commit = upstream_commit.strip()
5906
5907 if not upstream_commit:
5908 DieWithError('Could not find base commit for this branch. '
5909 'Are you in detached state?')
5910
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005911 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5912 diff_output = RunGit(changed_files_cmd)
5913 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005914 # Filter out files deleted by this CL
5915 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005916
Andreas Haas417d89c2020-02-06 10:24:27 +00005917 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005918 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005919
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00005920 clang_diff_files = []
5921 if opts.clang_format:
5922 clang_diff_files = [
5923 x for x in diff_files if MatchingFileType(x, CLANG_EXTS)
5924 ]
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005925 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005926 rust_diff_files = [x for x in diff_files if MatchingFileType(x, RUST_EXTS)]
Olivier Robin0a6b5442022-04-07 07:25:04 +00005927 swift_diff_files = [x for x in diff_files if MatchingFileType(x, SWIFT_EXTS)]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005928 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005929
Edward Lesmes50da7702020-03-30 19:23:43 +00005930 top_dir = settings.GetRoot()
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005931
Jamie Madill5e96ad12020-01-13 16:08:35 +00005932 return_value = _RunClangFormatDiff(opts, clang_diff_files, top_dir,
5933 upstream_commit)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005934
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005935 if opts.use_rust_fmt:
5936 rust_fmt_return_value = _RunRustFmt(opts, rust_diff_files, top_dir,
5937 upstream_commit)
5938 if rust_fmt_return_value == 2:
5939 return_value = 2
5940
Olivier Robin0a6b5442022-04-07 07:25:04 +00005941 if opts.use_swift_format:
5942 if sys.platform != 'darwin':
5943 DieWithError('swift-format is only supported on macOS.')
5944 swift_format_return_value = _RunSwiftFormat(opts, swift_diff_files, top_dir,
5945 upstream_commit)
5946 if swift_format_return_value == 2:
5947 return_value = 2
5948
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005949 # Similar code to above, but using yapf on .py files rather than clang-format
5950 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005951 py_explicitly_disabled = opts.python is not None and not opts.python
5952 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005953 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5954 yapf_tool = os.path.join(depot_tools_path, 'yapf')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005955
Aiden Bennerc08566e2018-10-03 17:52:42 +00005956 # Used for caching.
5957 yapf_configs = {}
5958 for f in python_diff_files:
5959 # Find the yapf style config for the current file, defaults to depot
5960 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005961 _FindYapfConfigFile(f, yapf_configs, top_dir)
5962
5963 # Turn on python formatting by default if a yapf config is specified.
5964 # This breaks in the case of this repo though since the specified
5965 # style file is also the global default.
5966 if opts.python is None:
5967 filtered_py_files = []
5968 for f in python_diff_files:
5969 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5970 filtered_py_files.append(f)
5971 else:
5972 filtered_py_files = python_diff_files
5973
5974 # Note: yapf still seems to fix indentation of the entire file
5975 # even if line ranges are specified.
5976 # See https://github.com/google/yapf/issues/499
5977 if not opts.full and filtered_py_files:
5978 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5979
Brian Sheedyb4307d52019-12-02 19:18:17 +00005980 yapfignore_patterns = _GetYapfIgnorePatterns(top_dir)
5981 filtered_py_files = _FilterYapfIgnoredFiles(filtered_py_files,
5982 yapfignore_patterns)
Brian Sheedy59b06a82019-10-14 17:03:29 +00005983
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005984 for f in filtered_py_files:
Andrew Grievefa40bfa2020-01-07 02:32:57 +00005985 yapf_style = _FindYapfConfigFile(f, yapf_configs, top_dir)
5986 # Default to pep8 if not .style.yapf is found.
5987 if not yapf_style:
5988 yapf_style = 'pep8'
Aiden Bennerc08566e2018-10-03 17:52:42 +00005989
Peter Wend9399922020-06-17 17:33:49 +00005990 with open(f, 'r') as py_f:
Andrew Grieveb9e694c2021-11-15 19:04:46 +00005991 if 'python2' in py_f.readline():
Peter Wend9399922020-06-17 17:33:49 +00005992 vpython_script = 'vpython'
Andrew Grieveb9e694c2021-11-15 19:04:46 +00005993 else:
5994 vpython_script = 'vpython3'
Peter Wend9399922020-06-17 17:33:49 +00005995
5996 cmd = [vpython_script, yapf_tool, '--style', yapf_style, f]
Aiden Bennerc08566e2018-10-03 17:52:42 +00005997
5998 has_formattable_lines = False
5999 if not opts.full:
6000 # Only run yapf over changed line ranges.
6001 for diff_start, diff_len in py_line_diffs[f]:
6002 diff_end = diff_start + diff_len - 1
6003 # Yapf errors out if diff_end < diff_start but this
6004 # is a valid line range diff for a removal.
6005 if diff_end >= diff_start:
6006 has_formattable_lines = True
6007 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
6008 # If all line diffs were removals we have nothing to format.
6009 if not has_formattable_lines:
6010 continue
6011
6012 if opts.diff or opts.dry_run:
6013 cmd += ['--diff']
6014 # Will return non-zero exit code if non-empty diff.
Edward Lesmesb7db1832020-06-22 20:22:27 +00006015 stdout = RunCommand(cmd,
6016 error_ok=True,
Josip Sokcevic673e8ed2021-10-27 23:46:18 +00006017 stderr=subprocess2.PIPE,
Edward Lesmesb7db1832020-06-22 20:22:27 +00006018 cwd=top_dir,
6019 shell=sys.platform.startswith('win32'))
Aiden Bennerc08566e2018-10-03 17:52:42 +00006020 if opts.diff:
6021 sys.stdout.write(stdout)
6022 elif len(stdout) > 0:
6023 return_value = 2
6024 else:
6025 cmd += ['-i']
Edward Lesmesb7db1832020-06-22 20:22:27 +00006026 RunCommand(cmd, cwd=top_dir, shell=sys.platform.startswith('win32'))
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006027
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006028 # Format GN build files. Always run on full build files for canonical form.
6029 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006030 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07006031 if opts.dry_run or opts.diff:
6032 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006033 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07006034 gn_ret = subprocess2.call(cmd + [gn_diff_file],
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00006035 shell=sys.platform.startswith('win'),
brettw4b8ed592016-08-05 16:19:12 -07006036 cwd=top_dir)
6037 if opts.dry_run and gn_ret == 2:
6038 return_value = 2 # Not formatted.
6039 elif opts.diff and gn_ret == 2:
6040 # TODO this should compute and print the actual diff.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006041 print('This change has GN build file diff for ' + gn_diff_file)
brettw4b8ed592016-08-05 16:19:12 -07006042 elif gn_ret != 0:
6043 # For non-dry run cases (and non-2 return values for dry-run), a
6044 # nonzero error code indicates a failure, probably because the file
6045 # doesn't parse.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006046 DieWithError('gn format failed on ' + gn_diff_file +
6047 '\nTry running `gn format` on this file manually.')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006048
Ilya Shermane081cbe2017-08-15 17:51:04 -07006049 # Skip the metrics formatting from the global presubmit hook. These files have
6050 # a separate presubmit hook that issues an error if the files need formatting,
6051 # whereas the top-level presubmit script merely issues a warning. Formatting
6052 # these files is somewhat slow, so it's important not to duplicate the work.
6053 if not opts.presubmit:
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006054 for diff_xml in GetDiffXMLs(diff_files):
6055 xml_dir = GetMetricsDir(diff_xml)
6056 if not xml_dir:
6057 continue
6058
Ilya Shermane081cbe2017-08-15 17:51:04 -07006059 tool_dir = os.path.join(top_dir, xml_dir)
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00006060 pretty_print_tool = os.path.join(tool_dir, 'pretty_print.py')
Fabrice de Gansecfab092022-09-15 20:59:01 +00006061 cmd = ['vpython3', pretty_print_tool, '--non-interactive']
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006062
6063 # If the XML file is histograms.xml or enums.xml, add the xml path to the
6064 # command as histograms/pretty_print.py now needs a relative path argument
6065 # after splitting the histograms into multiple directories.
6066 # For example, in tools/metrics/ukm, pretty-print could be run using:
6067 # $ python pretty_print.py
6068 # But in tools/metrics/histogrmas, pretty-print should be run with an
6069 # additional relative path argument, like:
Peter Kastingee088882021-08-03 17:57:00 +00006070 # $ python pretty_print.py metadata/UMA/histograms.xml
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006071 # $ python pretty_print.py enums.xml
6072
Weilun Shib92c4b72020-08-27 17:45:11 +00006073 if (diff_xml.endswith('histograms.xml') or diff_xml.endswith('enums.xml')
Weilun Shi4f50adb2023-01-17 20:43:17 +00006074 or diff_xml.endswith('histogram_suffixes_list.xml')):
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006075 cmd.append(diff_xml)
6076
Ilya Shermane081cbe2017-08-15 17:51:04 -07006077 if opts.dry_run or opts.diff:
6078 cmd.append('--diff')
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006079
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00006080 # TODO(isherman): Once this file runs only on Python 3.3+, drop the
6081 # `shell` param and instead replace `'vpython'` with
6082 # `shutil.which('frob')` above: https://stackoverflow.com/a/32799942
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006083 stdout = RunCommand(cmd,
6084 cwd=top_dir,
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00006085 shell=sys.platform.startswith('win32'))
Ilya Shermane081cbe2017-08-15 17:51:04 -07006086 if opts.diff:
6087 sys.stdout.write(stdout)
6088 if opts.dry_run and stdout:
6089 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05006090
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006091 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006092
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006093
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006094def GetDiffXMLs(diff_files):
6095 return [
6096 os.path.normpath(x) for x in diff_files if MatchingFileType(x, ['.xml'])
6097 ]
6098
6099
6100def GetMetricsDir(diff_xml):
Steven Holte2e664bf2017-04-21 13:10:47 -07006101 metrics_xml_dirs = [
6102 os.path.join('tools', 'metrics', 'actions'),
6103 os.path.join('tools', 'metrics', 'histograms'),
6104 os.path.join('tools', 'metrics', 'rappor'),
Ilya Shermanb67e60c2020-05-20 22:27:03 +00006105 os.path.join('tools', 'metrics', 'structured'),
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006106 os.path.join('tools', 'metrics', 'ukm'),
6107 ]
Steven Holte2e664bf2017-04-21 13:10:47 -07006108 for xml_dir in metrics_xml_dirs:
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006109 if diff_xml.startswith(xml_dir):
6110 return xml_dir
6111 return None
Steven Holte2e664bf2017-04-21 13:10:47 -07006112
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006113
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006114@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006115@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006116def CMDcheckout(parser, args):
Edward Lemurf38bc172019-09-03 21:02:13 +00006117 """Checks out a branch associated with a given Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006118 _, args = parser.parse_args(args)
6119
6120 if len(args) != 1:
6121 parser.print_help()
6122 return 1
6123
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00006124 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00006125 if not issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00006126 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006127
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00006128 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006129
Edward Lemur52969c92020-02-06 18:15:28 +00006130 output = RunGit(['config', '--local', '--get-regexp',
Edward Lesmes50da7702020-03-30 19:23:43 +00006131 r'branch\..*\.' + ISSUE_CONFIG_KEY],
Edward Lemur52969c92020-02-06 18:15:28 +00006132 error_ok=True)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006133
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006134 branches = []
Edward Lemur52969c92020-02-06 18:15:28 +00006135 for key, issue in [x.split() for x in output.splitlines()]:
6136 if issue == target_issue:
Edward Lesmes50da7702020-03-30 19:23:43 +00006137 branches.append(re.sub(r'branch\.(.*)\.' + ISSUE_CONFIG_KEY, r'\1', key))
Edward Lemur52969c92020-02-06 18:15:28 +00006138
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006139 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07006140 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006141 return 1
6142 if len(branches) == 1:
6143 RunGit(['checkout', branches[0]])
6144 else:
vapiera7fbd5a2016-06-16 09:17:49 -07006145 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006146 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07006147 print('%d: %s' % (i, branches[i]))
Edward Lesmesae3586b2020-03-23 21:21:14 +00006148 which = gclient_utils.AskForData('Choose by index: ')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006149 try:
6150 RunGit(['checkout', branches[int(which)]])
6151 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07006152 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006153 return 1
6154
6155 return 0
6156
6157
maruel@chromium.org29404b52014-09-08 22:58:00 +00006158def CMDlol(parser, args):
6159 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07006160 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00006161 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6162 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6163 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
Gavin Mak18f45d22020-12-04 21:45:10 +00006164 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')).decode('utf-8'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00006165 return 0
6166
6167
Josip Sokcevic0399e172022-03-21 23:11:51 +00006168def CMDversion(parser, args):
Josip Sokcevic0399e172022-03-21 23:11:51 +00006169 print(utils.depot_tools_version())
6170
6171
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006172class OptionParser(optparse.OptionParser):
6173 """Creates the option parse and add --verbose support."""
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00006174
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006175 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006176 optparse.OptionParser.__init__(
6177 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006178 self.add_option(
6179 '-v', '--verbose', action='count', default=0,
6180 help='Use 2 times for more debugging info')
6181
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006182 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00006183 try:
6184 return self._parse_args(args)
6185 finally:
6186 # Regardless of success or failure of args parsing, we want to report
6187 # metrics, but only after logging has been initialized (if parsing
6188 # succeeded).
6189 global settings
6190 settings = Settings()
6191
Edward Lesmes9c349062021-05-06 20:02:39 +00006192 if metrics.collector.config.should_collect_metrics:
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00006193 # GetViewVCUrl ultimately calls logging method.
6194 project_url = settings.GetViewVCUrl().strip('/+')
6195 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
6196 metrics.collector.add('project_urls', [project_url])
6197
6198 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006199 # Create an optparse.Values object that will store only the actual passed
6200 # options, without the defaults.
6201 actual_options = optparse.Values()
6202 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
6203 # Create an optparse.Values object with the default options.
6204 options = optparse.Values(self.get_default_values().__dict__)
6205 # Update it with the options passed by the user.
6206 options._update_careful(actual_options.__dict__)
6207 # Store the options passed by the user in an _actual_options attribute.
6208 # We store only the keys, and not the values, since the values can contain
6209 # arbitrary information, which might be PII.
Edward Lemur79d4f992019-11-11 23:49:02 +00006210 metrics.collector.add('arguments', list(actual_options.__dict__.keys()))
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006211
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006212 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01006213 logging.basicConfig(
6214 level=levels[min(options.verbose, len(levels) - 1)],
6215 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6216 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00006217
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006218 return options, args
6219
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006220
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006221def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006222 if sys.hexversion < 0x02060000:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006223 print('\nYour Python version %s is unsupported, please upgrade.\n' %
vapiera7fbd5a2016-06-16 09:17:49 -07006224 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006225 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006226
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006227 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006228 dispatcher = subcommand.CommandDispatcher(__name__)
6229 try:
6230 return dispatcher.execute(OptionParser(), argv)
Edward Lemur5b929a42019-10-21 17:57:39 +00006231 except auth.LoginRequiredError as e:
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006232 DieWithError(str(e))
Edward Lemur79d4f992019-11-11 23:49:02 +00006233 except urllib.error.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006234 if e.code != 500:
6235 raise
6236 DieWithError(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006237 ('App Engine is misbehaving and returned HTTP %d, again. Keep faith '
Quinten Yearsleyc4202212019-07-22 23:34:40 +00006238 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006239 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006240
6241
6242if __name__ == '__main__':
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006243 # These affect sys.stdout, so do it outside of main() to simplify mocks in
6244 # the unit tests.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006245 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006246 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00006247 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00006248 sys.exit(main(sys.argv[1:]))