blob: 242bb5b3f747a0e03da36b20e7ecb6392f6744c9 [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
Edward Lemur79d4f992019-11-11 23:49:02 +000058from third_party import six
59from six.moves import urllib
60
61
62if sys.version_info.major == 3:
63 basestring = (str,) # pylint: disable=redefined-builtin
64
Edward Lemurb9830242019-10-30 22:19:20 +000065
tandrii7400cf02016-06-21 08:48:07 -070066__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000067
Edward Lemur0f58ae42019-04-30 17:24:12 +000068# Traces for git push will be stored in a traces directory inside the
69# depot_tools checkout.
70DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
71TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
Edward Lemur227d5102020-02-25 23:45:35 +000072PRESUBMIT_SUPPORT = os.path.join(DEPOT_TOOLS, 'presubmit_support.py')
Edward Lemur0f58ae42019-04-30 17:24:12 +000073
74# When collecting traces, Git hashes will be reduced to 6 characters to reduce
75# the size after compression.
76GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
77# Used to redact the cookies from the gitcookies file.
78GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
79
Edward Lemurd4d1ba42019-09-20 21:46:37 +000080MAX_ATTEMPTS = 3
81
Edward Lemur1b52d872019-05-09 21:12:12 +000082# The maximum number of traces we will keep. Multiplied by 3 since we store
83# 3 files per trace.
84MAX_TRACES = 3 * 10
Edward Lemur5737f022019-05-17 01:24:00 +000085# Message to be displayed to the user to inform where to find the traces for a
86# git-cl upload execution.
Edward Lemur0f58ae42019-04-30 17:24:12 +000087TRACES_MESSAGE = (
Edward Lemur1b52d872019-05-09 21:12:12 +000088'\n'
Edward Lemur5737f022019-05-17 01:24:00 +000089'The traces of this git-cl execution have been recorded at:\n'
Edward Lemur1b52d872019-05-09 21:12:12 +000090' %(trace_name)s-traces.zip\n'
Edward Lemur5737f022019-05-17 01:24:00 +000091'Copies of your gitcookies file and git config have been recorded at:\n'
92' %(trace_name)s-git-info.zip\n')
Edward Lemur1b52d872019-05-09 21:12:12 +000093# Format of the message to be stored as part of the traces to give developers a
94# better context when they go through traces.
95TRACES_README_FORMAT = (
96'Date: %(now)s\n'
97'\n'
98'Change: https://%(gerrit_host)s/q/%(change_id)s\n'
99'Title: %(title)s\n'
100'\n'
101'%(description)s\n'
102'\n'
103'Execution time: %(execution_time)s\n'
104'Exit code: %(exit_code)s\n') + TRACES_MESSAGE
Edward Lemur0f58ae42019-04-30 17:24:12 +0000105
Aaron Gable1bc7bfe2016-12-19 10:08:14 -0800106POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
Henrique Ferreiroff249622019-11-28 23:19:29 +0000107DESCRIPTION_BACKUP_FILE = '.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +0000108REFS_THAT_ALIAS_TO_OTHER_REFS = {
Josip Sokcevic7e133ff2021-07-13 17:44:53 +0000109 'refs/remotes/origin/lkgr': 'refs/remotes/origin/main',
110 'refs/remotes/origin/lkcr': 'refs/remotes/origin/main',
rmistry@google.comc68112d2015-03-03 12:48:06 +0000111}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000112
Josip Sokcevicc39ab992020-09-24 20:09:15 +0000113DEFAULT_OLD_BRANCH = 'refs/remotes/origin/master'
114DEFAULT_NEW_BRANCH = 'refs/remotes/origin/main'
115
Joanna Wanga8db0cb2023-01-24 15:43:17 +0000116DEFAULT_BUILDBUCKET_HOST = 'cr-buildbucket.appspot.com'
117
thestig@chromium.org44202a22014-03-11 19:22:18 +0000118# Valid extensions for files we want to lint.
119DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
120DEFAULT_LINT_IGNORE_REGEX = r"$^"
121
Aiden Bennerc08566e2018-10-03 17:52:42 +0000122# File name for yapf style config files.
123YAPF_CONFIG_FILENAME = '.style.yapf'
124
Edward Lesmes50da7702020-03-30 19:23:43 +0000125# The issue, patchset and codereview server are stored on git config for each
126# branch under branch.<branch-name>.<config-key>.
127ISSUE_CONFIG_KEY = 'gerritissue'
128PATCHSET_CONFIG_KEY = 'gerritpatchset'
129CODEREVIEW_SERVER_CONFIG_KEY = 'gerritserver'
Gavin Makbe2e9262022-11-08 23:41:55 +0000130# When using squash workflow, _CMDUploadChange doesn't simply push the commit(s)
131# you make to Gerrit. Instead, it creates a new commit object that contains all
132# changes you've made, diffed against a parent/merge base.
133# This is the hash of the new squashed commit and you can find this on Gerrit.
134GERRIT_SQUASH_HASH_CONFIG_KEY = 'gerritsquashhash'
135# This is the latest uploaded local commit hash.
136LAST_UPLOAD_HASH_CONFIG_KEY = 'last-upload-hash'
Edward Lesmes50da7702020-03-30 19:23:43 +0000137
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000138# Shortcut since it quickly becomes repetitive.
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000139Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000140
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000141# Initialized in main()
142settings = None
143
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100144# Used by tests/git_cl_test.py to add extra logging.
145# Inside the weirdly failing test, add this:
146# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700147# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100148_IS_BEING_TESTED = False
149
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000150_GOOGLESOURCE = 'googlesource.com'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000151
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000152_KNOWN_GERRIT_TO_SHORT_URLS = {
153 'https://chrome-internal-review.googlesource.com': 'https://crrev.com/i',
154 'https://chromium-review.googlesource.com': 'https://crrev.com/c',
155}
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000156assert len(_KNOWN_GERRIT_TO_SHORT_URLS) == len(
157 set(_KNOWN_GERRIT_TO_SHORT_URLS.values())), 'must have unique values'
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000158
159
Joanna Wang18de1f62023-01-21 01:24:24 +0000160# Maximum number of branches in a stack that can be traversed and uploaded
161# at once. Picked arbitrarily.
162_MAX_STACKED_BRANCHES_UPLOAD = 20
163
164
Joanna Wang892f2ce2023-03-14 21:39:47 +0000165# Environment variable to indicate if user is participating in the stcked
166# changes dogfood.
167DOGFOOD_STACKED_CHANGES_VAR = 'DOGFOOD_STACKED_CHANGES'
168
169
Josip Sokcevicf736cab2020-10-20 23:41:38 +0000170class GitPushError(Exception):
171 pass
172
173
Christopher Lamf732cd52017-01-24 12:40:11 +1100174def DieWithError(message, change_desc=None):
175 if change_desc:
176 SaveDescriptionBackup(change_desc)
Josip Sokcevic953278a2020-02-28 19:46:36 +0000177 print('\n ** Content of CL description **\n' +
178 '='*72 + '\n' +
179 change_desc.description + '\n' +
180 '='*72 + '\n')
Christopher Lamf732cd52017-01-24 12:40:11 +1100181
vapiera7fbd5a2016-06-16 09:17:49 -0700182 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000183 sys.exit(1)
184
185
Christopher Lamf732cd52017-01-24 12:40:11 +1100186def SaveDescriptionBackup(change_desc):
Henrique Ferreiro5ae48172019-11-29 16:14:42 +0000187 backup_path = os.path.join(DEPOT_TOOLS, DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000188 print('\nsaving CL description to %s\n' % backup_path)
sokcevic07152802021-08-18 00:06:34 +0000189 with open(backup_path, 'wb') as backup_file:
190 backup_file.write(change_desc.description.encode('utf-8'))
Christopher Lamf732cd52017-01-24 12:40:11 +1100191
192
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000193def GetNoGitPagerEnv():
194 env = os.environ.copy()
195 # 'cat' is a magical git string that disables pagers on all platforms.
196 env['GIT_PAGER'] = 'cat'
197 return env
198
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000199
bsep@chromium.org627d9002016-04-29 00:00:52 +0000200def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000201 try:
Edward Lemur79d4f992019-11-11 23:49:02 +0000202 stdout = subprocess2.check_output(args, shell=shell, **kwargs)
203 return stdout.decode('utf-8', 'replace')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000204 except subprocess2.CalledProcessError as e:
205 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000206 if not error_ok:
Alan Cutter594fd332020-07-21 23:55:27 +0000207 message = error_message or e.stdout.decode('utf-8', 'replace') or ''
208 DieWithError('Command "%s" failed.\n%s' % (' '.join(args), message))
Josip Sokcevic673e8ed2021-10-27 23:46:18 +0000209 out = e.stdout.decode('utf-8', 'replace')
210 if e.stderr:
211 out += e.stderr.decode('utf-8', 'replace')
212 return out
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000213
214
215def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000216 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000217 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000218
219
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000220def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000221 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700222 if suppress_stderr:
Edward Lesmescf06cad2020-12-14 22:03:23 +0000223 stderr = subprocess2.DEVNULL
tandrii5d48c322016-08-18 16:19:37 -0700224 else:
225 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000226 try:
tandrii5d48c322016-08-18 16:19:37 -0700227 (out, _), code = subprocess2.communicate(['git'] + args,
228 env=GetNoGitPagerEnv(),
229 stdout=subprocess2.PIPE,
230 stderr=stderr)
Edward Lemur79d4f992019-11-11 23:49:02 +0000231 return code, out.decode('utf-8', 'replace')
tandrii5d48c322016-08-18 16:19:37 -0700232 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900233 logging.debug('Failed running %s', ['git'] + args)
Edward Lemur79d4f992019-11-11 23:49:02 +0000234 return e.returncode, e.stdout.decode('utf-8', 'replace')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000235
236
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000237def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000238 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000239 return RunGitWithCode(args, suppress_stderr=True)[1]
240
241
tandrii2a16b952016-10-19 07:09:44 -0700242def time_sleep(seconds):
243 # Use this so that it can be mocked in tests without interfering with python
244 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700245 return time.sleep(seconds)
246
247
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000248def time_time():
249 # Use this so that it can be mocked in tests without interfering with python
250 # system machinery.
251 return time.time()
252
253
Edward Lemur1b52d872019-05-09 21:12:12 +0000254def datetime_now():
255 # Use this so that it can be mocked in tests without interfering with python
256 # system machinery.
257 return datetime.datetime.now()
258
259
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100260def confirm_or_exit(prefix='', action='confirm'):
261 """Asks user to press enter to continue or press Ctrl+C to abort."""
262 if not prefix or prefix.endswith('\n'):
263 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100264 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100265 mid = ' Press'
266 elif prefix.endswith(' '):
267 mid = 'press'
268 else:
269 mid = ' press'
Edward Lesmesae3586b2020-03-23 21:21:14 +0000270 gclient_utils.AskForData(
271 '%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100272
273
274def ask_for_explicit_yes(prompt):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000275 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
Edward Lesmesae3586b2020-03-23 21:21:14 +0000276 result = gclient_utils.AskForData(prompt + ' [Yes/No]: ').lower()
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100277 while True:
278 if 'yes'.startswith(result):
279 return True
280 if 'no'.startswith(result):
281 return False
Edward Lesmesae3586b2020-03-23 21:21:14 +0000282 result = gclient_utils.AskForData('Please, type yes or no: ').lower()
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100283
284
machenbach@chromium.org45453142015-09-15 08:45:22 +0000285def _get_properties_from_options(options):
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000286 prop_list = getattr(options, 'properties', [])
287 properties = dict(x.split('=', 1) for x in prop_list)
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000288 for key, val in properties.items():
machenbach@chromium.org45453142015-09-15 08:45:22 +0000289 try:
290 properties[key] = json.loads(val)
291 except ValueError:
292 pass # If a value couldn't be evaluated, treat it as a string.
293 return properties
294
295
Edward Lemur4c707a22019-09-24 21:13:43 +0000296def _call_buildbucket(http, buildbucket_host, method, request):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000297 """Calls a buildbucket v2 method and returns the parsed json response."""
298 headers = {
299 'Accept': 'application/json',
300 'Content-Type': 'application/json',
301 }
302 request = json.dumps(request)
303 url = 'https://%s/prpc/buildbucket.v2.Builds/%s' % (buildbucket_host, method)
304
305 logging.info('POST %s with %s' % (url, request))
306
307 attempts = 1
308 time_to_sleep = 1
309 while True:
310 response, content = http.request(url, 'POST', body=request, headers=headers)
311 if response.status == 200:
312 return json.loads(content[4:])
313 if attempts >= MAX_ATTEMPTS or 400 <= response.status < 500:
314 msg = '%s error when calling POST %s with %s: %s' % (
315 response.status, url, request, content)
316 raise BuildbucketResponseException(msg)
317 logging.debug(
318 '%s error when calling POST %s with %s. '
319 'Sleeping for %d seconds and retrying...' % (
320 response.status, url, request, time_to_sleep))
321 time.sleep(time_to_sleep)
322 time_to_sleep *= 2
323 attempts += 1
324
325 assert False, 'unreachable'
326
327
Edward Lemur6215c792019-10-03 21:59:05 +0000328def _parse_bucket(raw_bucket):
329 legacy = True
330 project = bucket = None
331 if '/' in raw_bucket:
332 legacy = False
333 project, bucket = raw_bucket.split('/', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000334 # Assume luci.<project>.<bucket>.
Edward Lemur6215c792019-10-03 21:59:05 +0000335 elif raw_bucket.startswith('luci.'):
336 project, bucket = raw_bucket[len('luci.'):].split('.', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000337 # Otherwise, assume prefix is also the project name.
Edward Lemur6215c792019-10-03 21:59:05 +0000338 elif '.' in raw_bucket:
339 project = raw_bucket.split('.')[0]
340 bucket = raw_bucket
341 # Legacy buckets.
Edward Lemur45768512020-03-02 19:03:14 +0000342 if legacy and project and bucket:
Edward Lemur6215c792019-10-03 21:59:05 +0000343 print('WARNING Please use %s/%s to specify the bucket.' % (project, bucket))
344 return project, bucket
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000345
346
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000347def _canonical_git_googlesource_host(host):
348 """Normalizes Gerrit hosts (with '-review') to Git host."""
349 assert host.endswith(_GOOGLESOURCE)
350 # Prefix doesn't include '.' at the end.
351 prefix = host[:-(1 + len(_GOOGLESOURCE))]
352 if prefix.endswith('-review'):
353 prefix = prefix[:-len('-review')]
354 return prefix + '.' + _GOOGLESOURCE
355
356
357def _canonical_gerrit_googlesource_host(host):
358 git_host = _canonical_git_googlesource_host(host)
359 prefix = git_host.split('.', 1)[0]
360 return prefix + '-review.' + _GOOGLESOURCE
361
362
363def _get_counterpart_host(host):
364 assert host.endswith(_GOOGLESOURCE)
365 git = _canonical_git_googlesource_host(host)
366 gerrit = _canonical_gerrit_googlesource_host(git)
367 return git if gerrit == host else gerrit
368
369
Quinten Yearsley777660f2020-03-04 23:37:06 +0000370def _trigger_tryjobs(changelist, jobs, options, patchset):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000371 """Sends a request to Buildbucket to trigger tryjobs for a changelist.
qyearsley1fdfcb62016-10-24 13:22:03 -0700372
373 Args:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000374 changelist: Changelist that the tryjobs are associated with.
Edward Lemur45768512020-03-02 19:03:14 +0000375 jobs: A list of (project, bucket, builder).
qyearsley1fdfcb62016-10-24 13:22:03 -0700376 options: Command-line options.
377 """
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000378 print('Scheduling jobs on:')
Edward Lemur45768512020-03-02 19:03:14 +0000379 for project, bucket, builder in jobs:
380 print(' %s/%s: %s' % (project, bucket, builder))
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000381 print('To see results here, run: git cl try-results')
382 print('To see results in browser, run: git cl web')
tandriide281ae2016-10-12 06:02:30 -0700383
Quinten Yearsley777660f2020-03-04 23:37:06 +0000384 requests = _make_tryjob_schedule_requests(changelist, jobs, options, patchset)
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000385 if not requests:
386 return
387
Edward Lemur5b929a42019-10-21 17:57:39 +0000388 http = auth.Authenticator().authorize(httplib2.Http())
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000389 http.force_exception_to_status_code = True
390
391 batch_request = {'requests': requests}
Joanna Wanga8db0cb2023-01-24 15:43:17 +0000392 batch_response = _call_buildbucket(http, DEFAULT_BUILDBUCKET_HOST, 'Batch',
393 batch_request)
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000394
395 errors = [
396 ' ' + response['error']['message']
397 for response in batch_response.get('responses', [])
398 if 'error' in response
399 ]
400 if errors:
401 raise BuildbucketResponseException(
402 'Failed to schedule builds for some bots:\n%s' % '\n'.join(errors))
403
404
Quinten Yearsley777660f2020-03-04 23:37:06 +0000405def _make_tryjob_schedule_requests(changelist, jobs, options, patchset):
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000406 """Constructs requests for Buildbucket to trigger tryjobs."""
Edward Lemurf0faf482019-09-25 20:40:17 +0000407 gerrit_changes = [changelist.GetGerritChange(patchset)]
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000408 shared_properties = {
409 'category': options.ensure_value('category', 'git_cl_try')
410 }
411 if options.ensure_value('clobber', False):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000412 shared_properties['clobber'] = True
413 shared_properties.update(_get_properties_from_options(options) or {})
414
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000415 shared_tags = [{'key': 'user_agent', 'value': 'git_cl_try'}]
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000416 if options.ensure_value('retry_failed', False):
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000417 shared_tags.append({'key': 'retry_failed',
418 'value': '1'})
419
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000420 requests = []
Edward Lemur45768512020-03-02 19:03:14 +0000421 for (project, bucket, builder) in jobs:
422 properties = shared_properties.copy()
423 if 'presubmit' in builder.lower():
424 properties['dry_run'] = 'true'
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000425
Edward Lemur45768512020-03-02 19:03:14 +0000426 requests.append({
427 'scheduleBuild': {
428 'requestId': str(uuid.uuid4()),
429 'builder': {
430 'project': getattr(options, 'project', None) or project,
431 'bucket': bucket,
432 'builder': builder,
433 },
434 'gerritChanges': gerrit_changes,
435 'properties': properties,
436 'tags': [
437 {'key': 'builder', 'value': builder},
438 ] + shared_tags,
439 }
440 })
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000441
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000442 if options.ensure_value('revision', None):
Josip Sokcevic9011a5b2021-02-12 18:59:44 +0000443 remote, remote_branch = changelist.GetRemoteBranch()
Edward Lemur45768512020-03-02 19:03:14 +0000444 requests[-1]['scheduleBuild']['gitilesCommit'] = {
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000445 'host': _canonical_git_googlesource_host(gerrit_changes[0]['host']),
Edward Lemur45768512020-03-02 19:03:14 +0000446 'project': gerrit_changes[0]['project'],
Josip Sokcevic9011a5b2021-02-12 18:59:44 +0000447 'id': options.revision,
448 'ref': GetTargetRef(remote, remote_branch, None)
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000449 }
Anthony Polito1a5fe232020-01-24 23:17:52 +0000450
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000451 return requests
kjellander@chromium.org44424542015-06-02 18:35:29 +0000452
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000453
Quinten Yearsley777660f2020-03-04 23:37:06 +0000454def _fetch_tryjobs(changelist, buildbucket_host, patchset=None):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000455 """Fetches tryjobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000456
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000457 Returns list of buildbucket.v2.Build with the try jobs for the changelist.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000458 """
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000459 fields = ['id', 'builder', 'status', 'createTime', 'tags']
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000460 request = {
461 'predicate': {
462 'gerritChanges': [changelist.GetGerritChange(patchset)],
463 },
464 'fields': ','.join('builds.*.' + field for field in fields),
465 }
tandrii221ab252016-10-06 08:12:04 -0700466
Edward Lemur5b929a42019-10-21 17:57:39 +0000467 authenticator = auth.Authenticator()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000468 if authenticator.has_cached_credentials():
469 http = authenticator.authorize(httplib2.Http())
470 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700471 print('Warning: Some results might be missing because %s' %
472 # Get the message on how to login.
Andrii Shyshkalov2517afd2021-01-19 17:07:43 +0000473 (str(auth.LoginRequiredError()),))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000474 http = httplib2.Http()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000475 http.force_exception_to_status_code = True
476
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000477 response = _call_buildbucket(http, buildbucket_host, 'SearchBuilds', request)
478 return response.get('builds', [])
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000479
Edward Lemur45768512020-03-02 19:03:14 +0000480
Edward Lemur5b929a42019-10-21 17:57:39 +0000481def _fetch_latest_builds(changelist, buildbucket_host, latest_patchset=None):
Quinten Yearsley983111f2019-09-26 17:18:48 +0000482 """Fetches builds from the latest patchset that has builds (within
483 the last few patchsets).
484
485 Args:
Quinten Yearsley983111f2019-09-26 17:18:48 +0000486 changelist (Changelist): The CL to fetch builds for
487 buildbucket_host (str): Buildbucket host, e.g. "cr-buildbucket.appspot.com"
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000488 lastest_patchset(int|NoneType): the patchset to start fetching builds from.
489 If None (default), starts with the latest available patchset.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000490 Returns:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000491 A tuple (builds, patchset) where builds is a list of buildbucket.v2.Build,
492 and patchset is the patchset number where those builds came from.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000493 """
494 assert buildbucket_host
495 assert changelist.GetIssue(), 'CL must be uploaded first'
496 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000497 if latest_patchset is None:
498 assert changelist.GetMostRecentPatchset()
499 ps = changelist.GetMostRecentPatchset()
500 else:
501 assert latest_patchset > 0, latest_patchset
502 ps = latest_patchset
503
Quinten Yearsley983111f2019-09-26 17:18:48 +0000504 min_ps = max(1, ps - 5)
505 while ps >= min_ps:
Quinten Yearsley777660f2020-03-04 23:37:06 +0000506 builds = _fetch_tryjobs(changelist, buildbucket_host, patchset=ps)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000507 if len(builds):
508 return builds, ps
509 ps -= 1
510 return [], 0
511
512
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000513def _filter_failed_for_retry(all_builds):
514 """Returns a list of buckets/builders that are worth retrying.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000515
516 Args:
Quinten Yearsley777660f2020-03-04 23:37:06 +0000517 all_builds (list): Builds, in the format returned by _fetch_tryjobs,
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000518 i.e. a list of buildbucket.v2.Builds which includes status and builder
519 info.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000520
521 Returns:
Edward Lemur45768512020-03-02 19:03:14 +0000522 A dict {(proj, bucket): [builders]}. This is the same format accepted by
Quinten Yearsley777660f2020-03-04 23:37:06 +0000523 _trigger_tryjobs.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000524 """
Edward Lemur45768512020-03-02 19:03:14 +0000525 grouped = {}
526 for build in all_builds:
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000527 builder = build['builder']
Edward Lemur45768512020-03-02 19:03:14 +0000528 key = (builder['project'], builder['bucket'], builder['builder'])
529 grouped.setdefault(key, []).append(build)
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000530
Edward Lemur45768512020-03-02 19:03:14 +0000531 jobs = []
532 for (project, bucket, builder), builds in grouped.items():
533 if 'triggered' in builder:
534 print('WARNING: Not scheduling %s. Triggered bots require an initial job '
535 'from a parent. Please schedule a manual job for the parent '
536 'instead.')
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000537 continue
538 if any(b['status'] in ('STARTED', 'SCHEDULED') for b in builds):
539 # Don't retry if any are running.
540 continue
Edward Lemur45768512020-03-02 19:03:14 +0000541 # If builder had several builds, retry only if the last one failed.
542 # This is a bit different from CQ, which would re-use *any* SUCCESS-full
543 # build, but in case of retrying failed jobs retrying a flaky one makes
544 # sense.
545 builds = sorted(builds, key=lambda b: b['createTime'])
546 if builds[-1]['status'] not in ('FAILURE', 'INFRA_FAILURE'):
547 continue
548 # Don't retry experimental build previously triggered by CQ.
549 if any(t['key'] == 'cq_experimental' and t['value'] == 'true'
550 for t in builds[-1]['tags']):
551 continue
552 jobs.append((project, bucket, builder))
553
554 # Sort the jobs to make testing easier.
555 return sorted(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000556
557
Quinten Yearsley777660f2020-03-04 23:37:06 +0000558def _print_tryjobs(options, builds):
559 """Prints nicely result of _fetch_tryjobs."""
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000560 if not builds:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000561 print('No tryjobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000562 return
563
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000564 longest_builder = max(len(b['builder']['builder']) for b in builds)
565 name_fmt = '{builder:<%d}' % longest_builder
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000566 if options.print_master:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000567 longest_bucket = max(len(b['builder']['bucket']) for b in builds)
568 name_fmt = ('{bucket:>%d} ' % longest_bucket) + name_fmt
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000569
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000570 builds_by_status = {}
571 for b in builds:
572 builds_by_status.setdefault(b['status'], []).append({
573 'id': b['id'],
574 'name': name_fmt.format(
575 builder=b['builder']['builder'], bucket=b['builder']['bucket']),
576 })
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000577
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000578 sort_key = lambda b: (b['name'], b['id'])
579
580 def print_builds(title, builds, fmt=None, color=None):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000581 """Pop matching builds from `builds` dict and print them."""
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000582 if not builds:
583 return
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000584
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000585 fmt = fmt or '{name} https://ci.chromium.org/b/{id}'
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000586 if not options.color or color is None:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000587 colorize = lambda x: x
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000588 else:
589 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
590
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000591 print(colorize(title))
592 for b in sorted(builds, key=sort_key):
593 print(' ', colorize(fmt.format(**b)))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000594
595 total = len(builds)
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000596 print_builds(
597 'Successes:', builds_by_status.pop('SUCCESS', []), color=Fore.GREEN)
598 print_builds(
599 'Infra Failures:', builds_by_status.pop('INFRA_FAILURE', []),
600 color=Fore.MAGENTA)
601 print_builds('Failures:', builds_by_status.pop('FAILURE', []), color=Fore.RED)
602 print_builds('Canceled:', builds_by_status.pop('CANCELED', []), fmt='{name}',
603 color=Fore.MAGENTA)
Andrii Shyshkalov792630c2020-10-19 16:47:44 +0000604 print_builds('Started:', builds_by_status.pop('STARTED', []),
605 color=Fore.YELLOW)
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000606 print_builds(
607 'Scheduled:', builds_by_status.pop('SCHEDULED', []), fmt='{name} id={id}')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000608 # The last section is just in case buildbucket API changes OR there is a bug.
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000609 print_builds(
610 'Other:', sum(builds_by_status.values(), []), fmt='{name} id={id}')
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000611 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000612
613
Aiden Bennerc08566e2018-10-03 17:52:42 +0000614def _ComputeDiffLineRanges(files, upstream_commit):
615 """Gets the changed line ranges for each file since upstream_commit.
616
617 Parses a git diff on provided files and returns a dict that maps a file name
618 to an ordered list of range tuples in the form (start_line, count).
619 Ranges are in the same format as a git diff.
620 """
621 # If files is empty then diff_output will be a full diff.
622 if len(files) == 0:
623 return {}
624
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000625 # Take the git diff and find the line ranges where there are changes.
Jamie Madill3671a6a2019-10-24 15:13:21 +0000626 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000627 diff_output = RunGit(diff_cmd)
628
629 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
630 # 2 capture groups
631 # 0 == fname of diff file
632 # 1 == 'diff_start,diff_count' or 'diff_start'
633 # will match each of
634 # diff --git a/foo.foo b/foo.py
635 # @@ -12,2 +14,3 @@
636 # @@ -12,2 +17 @@
637 # running re.findall on the above string with pattern will give
638 # [('foo.py', ''), ('', '14,3'), ('', '17')]
639
640 curr_file = None
641 line_diffs = {}
642 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
643 if match[0] != '':
644 # Will match the second filename in diff --git a/a.py b/b.py.
645 curr_file = match[0]
646 line_diffs[curr_file] = []
647 else:
648 # Matches +14,3
649 if ',' in match[1]:
650 diff_start, diff_count = match[1].split(',')
651 else:
652 # Single line changes are of the form +12 instead of +12,1.
653 diff_start = match[1]
654 diff_count = 1
655
656 diff_start = int(diff_start)
657 diff_count = int(diff_count)
658
659 # If diff_count == 0 this is a removal we can ignore.
660 line_diffs[curr_file].append((diff_start, diff_count))
661
662 return line_diffs
663
664
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000665def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000666 """Checks if a yapf file is in any parent directory of fpath until top_dir.
667
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000668 Recursively checks parent directories to find yapf file and if no yapf file
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000669 is found returns None. Uses yapf_config_cache as a cache for previously found
670 configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000671 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000672 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000673 # Return result if we've already computed it.
674 if fpath in yapf_config_cache:
675 return yapf_config_cache[fpath]
676
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000677 parent_dir = os.path.dirname(fpath)
678 if os.path.isfile(fpath):
679 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000680 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000681 # Otherwise fpath is a directory
682 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
683 if os.path.isfile(yapf_file):
684 ret = yapf_file
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000685 elif fpath in (top_dir, parent_dir):
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000686 # If we're at the top level directory, or if we're at root
687 # there is no provided style.
688 ret = None
689 else:
690 # Otherwise recurse on the current directory.
691 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000692 yapf_config_cache[fpath] = ret
693 return ret
694
695
Brian Sheedyb4307d52019-12-02 19:18:17 +0000696def _GetYapfIgnorePatterns(top_dir):
697 """Returns all patterns in the .yapfignore file.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000698
699 yapf is supposed to handle the ignoring of files listed in .yapfignore itself,
700 but this functionality appears to break when explicitly passing files to
701 yapf for formatting. According to
Josip Sokcevicc39ab992020-09-24 20:09:15 +0000702 https://github.com/google/yapf/blob/HEAD/README.rst#excluding-files-from-formatting-yapfignore,
Brian Sheedy59b06a82019-10-14 17:03:29 +0000703 the .yapfignore file should be in the directory that yapf is invoked from,
704 which we assume to be the top level directory in this case.
705
706 Args:
707 top_dir: The top level directory for the repository being formatted.
708
709 Returns:
Brian Sheedyb4307d52019-12-02 19:18:17 +0000710 A set of all fnmatch patterns to be ignored.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000711 """
712 yapfignore_file = os.path.join(top_dir, '.yapfignore')
Brian Sheedyb4307d52019-12-02 19:18:17 +0000713 ignore_patterns = set()
Brian Sheedy59b06a82019-10-14 17:03:29 +0000714 if not os.path.exists(yapfignore_file):
Brian Sheedyb4307d52019-12-02 19:18:17 +0000715 return ignore_patterns
Brian Sheedy59b06a82019-10-14 17:03:29 +0000716
Anthony Politoc64e3902021-04-30 21:55:25 +0000717 for line in gclient_utils.FileRead(yapfignore_file).split('\n'):
718 stripped_line = line.strip()
719 # Comments and blank lines should be ignored.
720 if stripped_line.startswith('#') or stripped_line == '':
721 continue
722 ignore_patterns.add(stripped_line)
Brian Sheedyb4307d52019-12-02 19:18:17 +0000723 return ignore_patterns
724
725
726def _FilterYapfIgnoredFiles(filepaths, patterns):
727 """Filters out any filepaths that match any of the given patterns.
728
729 Args:
730 filepaths: An iterable of strings containing filepaths to filter.
731 patterns: An iterable of strings containing fnmatch patterns to filter on.
732
733 Returns:
734 A list of strings containing all the elements of |filepaths| that did not
735 match any of the patterns in |patterns|.
736 """
737 # Not inlined so that tests can use the same implementation.
738 return [f for f in filepaths
739 if not any(fnmatch.fnmatch(f, p) for p in patterns)]
Brian Sheedy59b06a82019-10-14 17:03:29 +0000740
741
Aaron Gable13101a62018-02-09 13:20:41 -0800742def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000743 """Prints statistics about the change to the user."""
744 # --no-ext-diff is broken in some versions of Git, so try to work around
745 # this by overriding the environment (but there is still a problem if the
746 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000747 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000748 if 'GIT_EXTERNAL_DIFF' in env:
749 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000750
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000751 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800752 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
Edward Lemur0db01f02019-11-12 22:01:51 +0000753 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000754
755
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000756class BuildbucketResponseException(Exception):
757 pass
758
759
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000760class Settings(object):
761 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000762 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000763 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000764 self.tree_status_url = None
765 self.viewvc_url = None
766 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000767 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000768 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000769 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000770 self.git_editor = None
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000771 self.format_full_by_default = None
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +0000772 self.is_status_commit_order_by_date = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000773
Edward Lemur26964072020-02-19 19:18:51 +0000774 def _LazyUpdateIfNeeded(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000775 """Updates the settings from a codereview.settings file, if available."""
Edward Lemur26964072020-02-19 19:18:51 +0000776 if self.updated:
777 return
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000778
Edward Lemur26964072020-02-19 19:18:51 +0000779 # The only value that actually changes the behavior is
780 # autoupdate = "false". Everything else means "true".
781 autoupdate = (
782 scm.GIT.GetConfig(self.GetRoot(), 'rietveld.autoupdate', '').lower())
783
784 cr_settings_file = FindCodereviewSettingsFile()
785 if autoupdate != 'false' and cr_settings_file:
786 LoadCodereviewSettingsFromFile(cr_settings_file)
787 cr_settings_file.close()
788
789 self.updated = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000790
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000791 @staticmethod
792 def GetRelativeRoot():
Edward Lesmes50da7702020-03-30 19:23:43 +0000793 return scm.GIT.GetCheckoutRoot('.')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000794
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000796 if self.root is None:
797 self.root = os.path.abspath(self.GetRelativeRoot())
798 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000799
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000800 def GetTreeStatusUrl(self, error_ok=False):
801 if not self.tree_status_url:
Edward Lemur26964072020-02-19 19:18:51 +0000802 self.tree_status_url = self._GetConfig('rietveld.tree-status-url')
803 if self.tree_status_url is None and not error_ok:
804 DieWithError(
805 'You must configure your tree status URL by running '
806 '"git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807 return self.tree_status_url
808
809 def GetViewVCUrl(self):
810 if not self.viewvc_url:
Edward Lemur26964072020-02-19 19:18:51 +0000811 self.viewvc_url = self._GetConfig('rietveld.viewvc-url')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000812 return self.viewvc_url
813
rmistry@google.com90752582014-01-14 21:04:50 +0000814 def GetBugPrefix(self):
Edward Lemur26964072020-02-19 19:18:51 +0000815 return self._GetConfig('rietveld.bug-prefix')
rmistry@google.com78948ed2015-07-08 23:09:57 +0000816
rmistry@google.com5626a922015-02-26 14:03:30 +0000817 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000818 run_post_upload_hook = self._GetConfig(
Edward Lemur26964072020-02-19 19:18:51 +0000819 'rietveld.run-post-upload-hook')
rmistry@google.com5626a922015-02-26 14:03:30 +0000820 return run_post_upload_hook == "True"
821
Joanna Wangc8f23e22023-01-19 21:18:10 +0000822 def GetDefaultCCList(self):
823 return self._GetConfig('rietveld.cc')
824
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000825 def GetSquashGerritUploads(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000826 """Returns True if uploads to Gerrit should be squashed by default."""
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000827 if self.squash_gerrit_uploads is None:
Edward Lesmes4de54132020-05-05 19:41:33 +0000828 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
829 if self.squash_gerrit_uploads is None:
Edward Lemur26964072020-02-19 19:18:51 +0000830 # Default is squash now (http://crbug.com/611892#c23).
831 self.squash_gerrit_uploads = self._GetConfig(
832 'gerrit.squash-uploads').lower() != 'false'
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000833 return self.squash_gerrit_uploads
834
Edward Lesmes4de54132020-05-05 19:41:33 +0000835 def GetSquashGerritUploadsOverride(self):
836 """Return True or False if codereview.settings should be overridden.
837
838 Returns None if no override has been defined.
839 """
840 # See also http://crbug.com/611892#c23
841 result = self._GetConfig('gerrit.override-squash-uploads').lower()
842 if result == 'true':
843 return True
844 if result == 'false':
845 return False
846 return None
847
Aleksey Khoroshilov35ef5ad2022-06-03 18:29:25 +0000848 def GetIsGerrit(self):
849 """Return True if gerrit.host is set."""
850 if self.is_gerrit is None:
851 self.is_gerrit = bool(self._GetConfig('gerrit.host', False))
852 return self.is_gerrit
853
tandrii@chromium.org28253532016-04-14 13:46:56 +0000854 def GetGerritSkipEnsureAuthenticated(self):
855 """Return True if EnsureAuthenticated should not be done for Gerrit
856 uploads."""
857 if self.gerrit_skip_ensure_authenticated is None:
Edward Lemur26964072020-02-19 19:18:51 +0000858 self.gerrit_skip_ensure_authenticated = self._GetConfig(
859 'gerrit.skip-ensure-authenticated').lower() == 'true'
tandrii@chromium.org28253532016-04-14 13:46:56 +0000860 return self.gerrit_skip_ensure_authenticated
861
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000862 def GetGitEditor(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000863 """Returns the editor specified in the git config, or None if none is."""
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000864 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000865 # Git requires single quotes for paths with spaces. We need to replace
866 # them with double quotes for Windows to treat such paths as a single
867 # path.
Edward Lemur26964072020-02-19 19:18:51 +0000868 self.git_editor = self._GetConfig('core.editor').replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000869 return self.git_editor or None
870
thestig@chromium.org44202a22014-03-11 19:22:18 +0000871 def GetLintRegex(self):
Edward Lemur26964072020-02-19 19:18:51 +0000872 return self._GetConfig('rietveld.cpplint-regex', DEFAULT_LINT_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000873
874 def GetLintIgnoreRegex(self):
Edward Lemur26964072020-02-19 19:18:51 +0000875 return self._GetConfig(
876 'rietveld.cpplint-ignore-regex', DEFAULT_LINT_IGNORE_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000877
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000878 def GetFormatFullByDefault(self):
879 if self.format_full_by_default is None:
Jamie Madillac6f6232021-07-07 20:54:08 +0000880 self._LazyUpdateIfNeeded()
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000881 result = (
882 RunGit(['config', '--bool', 'rietveld.format-full-by-default'],
883 error_ok=True).strip())
884 self.format_full_by_default = (result == 'true')
885 return self.format_full_by_default
886
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +0000887 def IsStatusCommitOrderByDate(self):
888 if self.is_status_commit_order_by_date is None:
889 result = (RunGit(['config', '--bool', 'cl.date-order'],
890 error_ok=True).strip())
891 self.is_status_commit_order_by_date = (result == 'true')
892 return self.is_status_commit_order_by_date
893
Edward Lemur26964072020-02-19 19:18:51 +0000894 def _GetConfig(self, key, default=''):
895 self._LazyUpdateIfNeeded()
896 return scm.GIT.GetConfig(self.GetRoot(), key, default)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000897
898
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000899class _CQState(object):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +0000900 """Enum for states of CL with respect to CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000901 NONE = 'none'
Greg Gutermanbe5fccd2021-06-14 17:58:20 +0000902 QUICK_RUN = 'quick_run'
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000903 DRY_RUN = 'dry_run'
904 COMMIT = 'commit'
905
Greg Gutermanbe5fccd2021-06-14 17:58:20 +0000906 ALL_STATES = [NONE, QUICK_RUN, DRY_RUN, COMMIT]
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000907
908
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000909class _ParsedIssueNumberArgument(object):
Edward Lemurf38bc172019-09-03 21:02:13 +0000910 def __init__(self, issue=None, patchset=None, hostname=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000911 self.issue = issue
912 self.patchset = patchset
913 self.hostname = hostname
914
915 @property
916 def valid(self):
917 return self.issue is not None
918
919
Edward Lemurf38bc172019-09-03 21:02:13 +0000920def ParseIssueNumberArgument(arg):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000921 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
922 fail_result = _ParsedIssueNumberArgument()
923
Edward Lemur678a6842019-10-03 22:25:05 +0000924 if isinstance(arg, int):
925 return _ParsedIssueNumberArgument(issue=arg)
926 if not isinstance(arg, basestring):
927 return fail_result
928
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000929 if arg.isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +0000930 return _ParsedIssueNumberArgument(issue=int(arg))
Aaron Gableaee6c852017-06-26 12:49:01 -0700931
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000932 url = gclient_utils.UpgradeToHttps(arg)
Alex Turner30ae6372022-01-04 02:32:52 +0000933 if not url.startswith('http'):
934 return fail_result
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000935 for gerrit_url, short_url in _KNOWN_GERRIT_TO_SHORT_URLS.items():
936 if url.startswith(short_url):
937 url = gerrit_url + url[len(short_url):]
938 break
939
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000940 try:
Edward Lemur79d4f992019-11-11 23:49:02 +0000941 parsed_url = urllib.parse.urlparse(url)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000942 except ValueError:
943 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +0200944
Alex Turner30ae6372022-01-04 02:32:52 +0000945 # If "https://" was automatically added, fail if `arg` looks unlikely to be a
946 # URL.
947 if not arg.startswith('http') and '.' not in parsed_url.netloc:
948 return fail_result
949
Edward Lemur678a6842019-10-03 22:25:05 +0000950 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
951 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
952 # Short urls like https://domain/<issue_number> can be used, but don't allow
953 # specifying the patchset (you'd 404), but we allow that here.
954 if parsed_url.path == '/':
955 part = parsed_url.fragment
956 else:
957 part = parsed_url.path
958
959 match = re.match(
960 r'(/c(/.*/\+)?)?/(?P<issue>\d+)(/(?P<patchset>\d+)?/?)?$', part)
961 if not match:
962 return fail_result
963
964 issue = int(match.group('issue'))
965 patchset = match.group('patchset')
966 return _ParsedIssueNumberArgument(
967 issue=issue,
968 patchset=int(patchset) if patchset else None,
969 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000970
971
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000972def _create_description_from_log(args):
973 """Pulls out the commit log to use as a base for the CL description."""
974 log_args = []
Bruce Dawson13acea32022-05-03 22:13:08 +0000975 if len(args) == 1 and args[0] == None:
976 # Handle the case where None is passed as the branch.
977 return ''
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000978 if len(args) == 1 and not args[0].endswith('.'):
979 log_args = [args[0] + '..']
980 elif len(args) == 1 and args[0].endswith('...'):
981 log_args = [args[0][:-1]]
982 elif len(args) == 2:
983 log_args = [args[0] + '..' + args[1]]
984 else:
985 log_args = args[:] # Hope for the best!
Manh Nguyene3644862020-08-05 18:25:46 +0000986 return RunGit(['log', '--pretty=format:%B%n'] + log_args)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000987
988
Aaron Gablea45ee112016-11-22 15:14:38 -0800989class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -0700990 def __init__(self, issue, url):
991 self.issue = issue
992 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -0800993 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -0700994
995 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -0800996 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -0700997 self.issue, self.url)
998
999
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001000_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001001 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001002 # TODO(tandrii): these two aren't known in Gerrit.
1003 'approval', 'disapproval'])
1004
1005
Joanna Wang6215dd02023-02-07 15:58:03 +00001006# TODO(b/265929888): Change `parent` to `pushed_commit_base`.
Joanna Wange8523912023-01-21 02:05:40 +00001007_NewUpload = collections.namedtuple('NewUpload', [
Joanna Wang40497912023-01-24 21:18:16 +00001008 'reviewers', 'ccs', 'commit_to_push', 'new_last_uploaded_commit', 'parent',
Joanna Wang7603f042023-03-01 22:17:36 +00001009 'change_desc', 'prev_patchset'
Joanna Wange8523912023-01-21 02:05:40 +00001010])
1011
1012
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001013class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001014 """Changelist works with one changelist in local branch.
1015
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001016 Notes:
1017 * Not safe for concurrent multi-{thread,process} use.
1018 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001019 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001020 """
1021
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00001022 def __init__(self,
1023 branchref=None,
1024 issue=None,
1025 codereview_host=None,
1026 commit_date=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001027 """Create a new ChangeList instance.
1028
Edward Lemurf38bc172019-09-03 21:02:13 +00001029 **kwargs will be passed directly to Gerrit implementation.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001030 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001032 global settings
1033 if not settings:
1034 # Happens when git_cl.py is used as a utility library.
1035 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001036
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001037 self.branchref = branchref
1038 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001039 assert branchref.startswith('refs/heads/')
Edward Lemur85153282020-02-14 22:06:29 +00001040 self.branch = scm.GIT.ShortBranchName(self.branchref)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001041 else:
1042 self.branch = None
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00001043 self.commit_date = commit_date
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001044 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001045 self.lookedup_issue = False
1046 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001047 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001048 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001049 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001050 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001051 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001052 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001053 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001054
Edward Lemur125d60a2019-09-13 18:25:41 +00001055 # Lazily cached values.
1056 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1057 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Edward Lesmese1576912021-02-16 21:53:34 +00001058 self._owners_client = None
Edward Lemur125d60a2019-09-13 18:25:41 +00001059 # Map from change number (issue) to its detail cache.
1060 self._detail_cache = {}
1061
1062 if codereview_host is not None:
1063 assert not codereview_host.startswith('https://'), codereview_host
1064 self._gerrit_host = codereview_host
1065 self._gerrit_server = 'https://%s' % codereview_host
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001066
Edward Lesmese1576912021-02-16 21:53:34 +00001067 @property
1068 def owners_client(self):
1069 if self._owners_client is None:
1070 remote, remote_branch = self.GetRemoteBranch()
1071 branch = GetTargetRef(remote, remote_branch, None)
1072 self._owners_client = owners_client.GetCodeOwnersClient(
Edward Lesmese1576912021-02-16 21:53:34 +00001073 host=self.GetGerritHost(),
1074 project=self.GetGerritProject(),
1075 branch=branch)
1076 return self._owners_client
1077
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001078 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001079 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001080
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001081 The return value is a string suitable for passing to git cl with the --cc
1082 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001083 """
1084 if self.cc is None:
Joanna Wangc8f23e22023-01-19 21:18:10 +00001085 base_cc = settings.GetDefaultCCList()
1086 more_cc = ','.join(self.more_cc)
1087 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001088 return self.cc
1089
Daniel Cheng7227d212017-11-17 08:12:37 -08001090 def ExtendCC(self, more_cc):
1091 """Extends the list of users to cc on this CL based on the changed files."""
1092 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001093
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00001094 def GetCommitDate(self):
1095 """Returns the commit date as provided in the constructor"""
1096 return self.commit_date
1097
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001098 def GetBranch(self):
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001099 """Returns the short branch name, e.g. 'main'."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100 if not self.branch:
Edward Lemur85153282020-02-14 22:06:29 +00001101 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001102 if not branchref:
1103 return None
1104 self.branchref = branchref
Edward Lemur85153282020-02-14 22:06:29 +00001105 self.branch = scm.GIT.ShortBranchName(self.branchref)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106 return self.branch
1107
1108 def GetBranchRef(self):
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001109 """Returns the full branch name, e.g. 'refs/heads/main'."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001110 self.GetBranch() # Poke the lazy loader.
1111 return self.branchref
1112
Edward Lemur85153282020-02-14 22:06:29 +00001113 def _GitGetBranchConfigValue(self, key, default=None):
1114 return scm.GIT.GetBranchConfig(
1115 settings.GetRoot(), self.GetBranch(), key, default)
tandrii5d48c322016-08-18 16:19:37 -07001116
Edward Lemur85153282020-02-14 22:06:29 +00001117 def _GitSetBranchConfigValue(self, key, value):
1118 action = 'set %s to %r' % (key, value)
1119 if not value:
1120 action = 'unset %s' % key
1121 assert self.GetBranch(), 'a branch is needed to ' + action
1122 return scm.GIT.SetBranchConfig(
1123 settings.GetRoot(), self.GetBranch(), key, value)
tandrii5d48c322016-08-18 16:19:37 -07001124
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001125 @staticmethod
1126 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001127 """Returns a tuple containing remote and remote ref,
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001128 e.g. 'origin', 'refs/heads/main'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129 """
Edward Lemur15a9b8c2020-02-13 00:52:30 +00001130 remote, upstream_branch = scm.GIT.FetchUpstreamTuple(
1131 settings.GetRoot(), branch)
1132 if not remote or not upstream_branch:
1133 DieWithError(
1134 'Unable to determine default branch to diff against.\n'
Josip Sokcevicb038f722021-01-06 18:28:11 +00001135 'Verify this branch is set up to track another \n'
1136 '(via the --track argument to "git checkout -b ..."). \n'
1137 'or pass complete "git diff"-style arguments if supported, like\n'
1138 ' git cl upload origin/main\n')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139
1140 return remote, upstream_branch
1141
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001142 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001143 upstream_branch = self.GetUpstreamBranch()
Edward Lesmes50da7702020-03-30 19:23:43 +00001144 if not scm.GIT.IsValidRevision(settings.GetRoot(), upstream_branch):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001145 DieWithError('The upstream for the current branch (%s) does not exist '
1146 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001147 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001148 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001149
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001150 def GetUpstreamBranch(self):
1151 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001152 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Raul Tambrefe1dbe12019-05-02 04:43:57 +00001153 if remote != '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001154 upstream_branch = upstream_branch.replace('refs/heads/',
1155 'refs/remotes/%s/' % remote)
1156 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1157 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001158 self.upstream_branch = upstream_branch
1159 return self.upstream_branch
1160
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001161 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001162 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001163 remote, branch = None, self.GetBranch()
1164 seen_branches = set()
1165 while branch not in seen_branches:
1166 seen_branches.add(branch)
1167 remote, branch = self.FetchUpstreamTuple(branch)
Edward Lemur85153282020-02-14 22:06:29 +00001168 branch = scm.GIT.ShortBranchName(branch)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001169 if remote != '.' or branch.startswith('refs/remotes'):
1170 break
1171 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001172 remotes = RunGit(['remote'], error_ok=True).split()
1173 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001174 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001175 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001176 remote = 'origin'
Gavin Make6a62332020-12-04 21:57:10 +00001177 logging.warning('Could not determine which remote this change is '
1178 'associated with, so defaulting to "%s".' %
1179 self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001180 else:
Gavin Make6a62332020-12-04 21:57:10 +00001181 logging.warning('Could not determine which remote this change is '
1182 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001183 branch = 'HEAD'
1184 if branch.startswith('refs/remotes'):
1185 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001186 elif branch.startswith('refs/branch-heads/'):
1187 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001188 else:
1189 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001190 return self._remote
1191
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001192 def GetRemoteUrl(self):
1193 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1194
1195 Returns None if there is no remote.
1196 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001197 is_cached, value = self._cached_remote_url
1198 if is_cached:
1199 return value
1200
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001201 remote, _ = self.GetRemoteBranch()
Edward Lemur26964072020-02-19 19:18:51 +00001202 url = scm.GIT.GetConfig(settings.GetRoot(), 'remote.%s.url' % remote, '')
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001203
Edward Lemur298f2cf2019-02-22 21:40:39 +00001204 # Check if the remote url can be parsed as an URL.
Edward Lemur79d4f992019-11-11 23:49:02 +00001205 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001206 if host:
1207 self._cached_remote_url = (True, url)
1208 return url
1209
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001210 # If it cannot be parsed as an url, assume it is a local directory,
1211 # probably a git cache.
Edward Lemur298f2cf2019-02-22 21:40:39 +00001212 logging.warning('"%s" doesn\'t appear to point to a git host. '
1213 'Interpreting it as a local directory.', url)
1214 if not os.path.isdir(url):
1215 logging.error(
Josip906bfde2020-01-31 22:38:49 +00001216 'Remote "%(remote)s" for branch "%(branch)s" points to "%(url)s", '
1217 'but it doesn\'t exist.',
1218 {'remote': remote, 'branch': self.GetBranch(), 'url': url})
Edward Lemur298f2cf2019-02-22 21:40:39 +00001219 return None
1220
1221 cache_path = url
Edward Lemur26964072020-02-19 19:18:51 +00001222 url = scm.GIT.GetConfig(url, 'remote.%s.url' % remote, '')
Edward Lemur298f2cf2019-02-22 21:40:39 +00001223
Edward Lemur79d4f992019-11-11 23:49:02 +00001224 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001225 if not host:
1226 logging.error(
1227 'Remote "%(remote)s" for branch "%(branch)s" points to '
1228 '"%(cache_path)s", but it is misconfigured.\n'
1229 '"%(cache_path)s" must be a git repo and must have a remote named '
1230 '"%(remote)s" pointing to the git host.', {
1231 'remote': remote,
1232 'cache_path': cache_path,
1233 'branch': self.GetBranch()})
1234 return None
1235
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001236 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001237 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001238
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001239 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001240 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001241 if self.issue is None and not self.lookedup_issue:
Bruce Dawson13acea32022-05-03 22:13:08 +00001242 if self.GetBranch():
1243 self.issue = self._GitGetBranchConfigValue(ISSUE_CONFIG_KEY)
Edward Lemur85153282020-02-14 22:06:29 +00001244 if self.issue is not None:
1245 self.issue = int(self.issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001246 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247 return self.issue
1248
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00001249 def GetIssueURL(self, short=False):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001250 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001251 issue = self.GetIssue()
1252 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001253 return None
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00001254 server = self.GetCodereviewServer()
1255 if short:
1256 server = _KNOWN_GERRIT_TO_SHORT_URLS.get(server, server)
1257 return '%s/%s' % (server, issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001258
Edward Lemur6c6827c2020-02-06 21:15:18 +00001259 def FetchDescription(self, pretty=False):
1260 assert self.GetIssue(), 'issue is required to query Gerrit'
1261
Edward Lemur9aa1a962020-02-25 00:58:38 +00001262 if self.description is None:
Edward Lemur6c6827c2020-02-06 21:15:18 +00001263 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
1264 current_rev = data['current_revision']
1265 self.description = data['revisions'][current_rev]['commit']['message']
Edward Lemur6c6827c2020-02-06 21:15:18 +00001266
1267 if not pretty:
1268 return self.description
1269
1270 # Set width to 72 columns + 2 space indent.
1271 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
1272 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1273 lines = self.description.splitlines()
1274 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001275
1276 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001277 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001278 if self.patchset is None and not self.lookedup_patchset:
Bruce Dawson13acea32022-05-03 22:13:08 +00001279 if self.GetBranch():
1280 self.patchset = self._GitGetBranchConfigValue(PATCHSET_CONFIG_KEY)
Edward Lemur85153282020-02-14 22:06:29 +00001281 if self.patchset is not None:
1282 self.patchset = int(self.patchset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001283 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001284 return self.patchset
1285
Edward Lemur9aa1a962020-02-25 00:58:38 +00001286 def GetAuthor(self):
1287 return scm.GIT.GetConfig(settings.GetRoot(), 'user.email')
1288
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001289 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001290 """Set this branch's patchset. If patchset=0, clears the patchset."""
1291 assert self.GetBranch()
1292 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001293 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001294 else:
1295 self.patchset = int(patchset)
Edward Lesmes50da7702020-03-30 19:23:43 +00001296 self._GitSetBranchConfigValue(PATCHSET_CONFIG_KEY, str(self.patchset))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001297
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001298 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001299 """Set this branch's issue. If issue isn't given, clears the issue."""
1300 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001301 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001302 issue = int(issue)
Edward Lesmes50da7702020-03-30 19:23:43 +00001303 self._GitSetBranchConfigValue(ISSUE_CONFIG_KEY, str(issue))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001304 self.issue = issue
Edward Lemur125d60a2019-09-13 18:25:41 +00001305 codereview_server = self.GetCodereviewServer()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001306 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001307 self._GitSetBranchConfigValue(
Edward Lesmes50da7702020-03-30 19:23:43 +00001308 CODEREVIEW_SERVER_CONFIG_KEY, codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001309 else:
tandrii5d48c322016-08-18 16:19:37 -07001310 # Reset all of these just to be clean.
1311 reset_suffixes = [
Gavin Makbe2e9262022-11-08 23:41:55 +00001312 LAST_UPLOAD_HASH_CONFIG_KEY,
Edward Lesmes50da7702020-03-30 19:23:43 +00001313 ISSUE_CONFIG_KEY,
1314 PATCHSET_CONFIG_KEY,
1315 CODEREVIEW_SERVER_CONFIG_KEY,
Gavin Makbe2e9262022-11-08 23:41:55 +00001316 GERRIT_SQUASH_HASH_CONFIG_KEY,
Edward Lesmes50da7702020-03-30 19:23:43 +00001317 ]
tandrii5d48c322016-08-18 16:19:37 -07001318 for prop in reset_suffixes:
Edward Lemur85153282020-02-14 22:06:29 +00001319 try:
1320 self._GitSetBranchConfigValue(prop, None)
1321 except subprocess2.CalledProcessError:
1322 pass
Aaron Gableca01e2c2017-07-19 11:16:02 -07001323 msg = RunGit(['log', '-1', '--format=%B']).strip()
1324 if msg and git_footers.get_footer_change_id(msg):
1325 print('WARNING: The change patched into this branch has a Change-Id. '
1326 'Removing it.')
1327 RunGit(['commit', '--amend', '-m',
1328 git_footers.remove_footer(msg, 'Change-Id')])
Edward Lemurf38bc172019-09-03 21:02:13 +00001329 self.lookedup_issue = True
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001330 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001331 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001332
Joanna Wangb46232e2023-01-21 01:58:46 +00001333 def GetAffectedFiles(self, upstream, end_commit=None):
1334 # type: (str, Optional[str]) -> Sequence[str]
1335 """Returns the list of affected files for the given commit range."""
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001336 try:
Joanna Wangb46232e2023-01-21 01:58:46 +00001337 return [
1338 f for _, f in scm.GIT.CaptureStatus(
1339 settings.GetRoot(), upstream, end_commit=end_commit)
1340 ]
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001341 except subprocess2.CalledProcessError:
1342 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001343 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001344 'This branch probably doesn\'t exist anymore. To reset the\n'
1345 'tracking branch, please run\n'
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001346 ' git branch --set-upstream-to origin/main %s\n'
1347 'or replace origin/main with the relevant branch') %
Edward Lemur2c62b332020-03-12 22:12:33 +00001348 (upstream, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001349
dsansomee2d6fd92016-09-08 00:10:47 -07001350 def UpdateDescription(self, description, force=False):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001351 assert self.GetIssue(), 'issue is required to update description'
1352
1353 if gerrit_util.HasPendingChangeEdit(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001354 self.GetGerritHost(), self._GerritChangeIdentifier()):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001355 if not force:
1356 confirm_or_exit(
1357 'The description cannot be modified while the issue has a pending '
1358 'unpublished edit. Either publish the edit in the Gerrit web UI '
1359 'or delete it.\n\n', action='delete the unpublished edit')
1360
1361 gerrit_util.DeletePendingChangeEdit(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001362 self.GetGerritHost(), self._GerritChangeIdentifier())
Edward Lemur6c6827c2020-02-06 21:15:18 +00001363 gerrit_util.SetCommitMessage(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001364 self.GetGerritHost(), self._GerritChangeIdentifier(),
Edward Lemur6c6827c2020-02-06 21:15:18 +00001365 description, notify='NONE')
1366
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001367 self.description = description
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001368
Edward Lemur75526302020-02-27 22:31:05 +00001369 def _GetCommonPresubmitArgs(self, verbose, upstream):
Edward Lemur227d5102020-02-25 23:45:35 +00001370 args = [
Edward Lemur227d5102020-02-25 23:45:35 +00001371 '--root', settings.GetRoot(),
1372 '--upstream', upstream,
1373 ]
1374
1375 args.extend(['--verbose'] * verbose)
1376
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001377 remote, remote_branch = self.GetRemoteBranch()
1378 target_ref = GetTargetRef(remote, remote_branch, None)
Aleksey Khoroshilov35ef5ad2022-06-03 18:29:25 +00001379 if settings.GetIsGerrit():
1380 args.extend(['--gerrit_url', self.GetCodereviewServer()])
1381 args.extend(['--gerrit_project', self.GetGerritProject()])
1382 args.extend(['--gerrit_branch', target_ref])
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001383
Edward Lemur99df04e2020-03-05 19:39:43 +00001384 author = self.GetAuthor()
Edward Lemur227d5102020-02-25 23:45:35 +00001385 issue = self.GetIssue()
1386 patchset = self.GetPatchset()
Edward Lemur99df04e2020-03-05 19:39:43 +00001387 if author:
1388 args.extend(['--author', author])
Edward Lemur227d5102020-02-25 23:45:35 +00001389 if issue:
1390 args.extend(['--issue', str(issue)])
1391 if patchset:
1392 args.extend(['--patchset', str(patchset)])
Edward Lemur227d5102020-02-25 23:45:35 +00001393
Edward Lemur75526302020-02-27 22:31:05 +00001394 return args
1395
Josip Sokcevic017544d2022-03-31 23:47:53 +00001396 def RunHook(self,
1397 committing,
1398 may_prompt,
1399 verbose,
1400 parallel,
1401 upstream,
1402 description,
1403 all_files,
1404 files=None,
1405 resultdb=False,
1406 realm=None):
Edward Lemur75526302020-02-27 22:31:05 +00001407 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1408 args = self._GetCommonPresubmitArgs(verbose, upstream)
1409 args.append('--commit' if committing else '--upload')
Edward Lemur227d5102020-02-25 23:45:35 +00001410 if may_prompt:
1411 args.append('--may_prompt')
1412 if parallel:
1413 args.append('--parallel')
1414 if all_files:
1415 args.append('--all_files')
Josip Sokcevic017544d2022-03-31 23:47:53 +00001416 if files:
1417 args.extend(files.split(';'))
1418 args.append('--source_controlled_only')
Bruce Dawson09c0c072022-05-26 20:28:58 +00001419 if files or all_files:
1420 args.append('--no_diffs')
Edward Lemur227d5102020-02-25 23:45:35 +00001421
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001422 if resultdb and not realm:
1423 # TODO (crbug.com/1113463): store realm somewhere and look it up so
1424 # it is not required to pass the realm flag
1425 print('Note: ResultDB reporting will NOT be performed because --realm'
1426 ' was not specified. To enable ResultDB, please run the command'
1427 ' again with the --realm argument to specify the LUCI realm.')
1428
Robert Iannucci3d6d2d22023-05-11 17:25:05 +00001429 return self._RunPresubmit(args,
1430 description,
1431 resultdb=resultdb,
1432 realm=realm)
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001433
Joanna Wanga8db0cb2023-01-24 15:43:17 +00001434 def _RunPresubmit(self,
1435 args,
1436 description,
Joanna Wanga8db0cb2023-01-24 15:43:17 +00001437 resultdb=None,
1438 realm=None):
1439 # type: (Sequence[str], str, bool, Optional[bool], Optional[str]
1440 # ) -> Mapping[str, Any]
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001441 args = args[:]
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001442
Edward Lemur227d5102020-02-25 23:45:35 +00001443 with gclient_utils.temporary_file() as description_file:
1444 with gclient_utils.temporary_file() as json_output:
Edward Lemur1a83da12020-03-04 21:18:36 +00001445 gclient_utils.FileWrite(description_file, description)
Edward Lemur227d5102020-02-25 23:45:35 +00001446 args.extend(['--json_output', json_output])
1447 args.extend(['--description_file', description_file])
Edward Lemur227d5102020-02-25 23:45:35 +00001448 start = time_time()
Robert Iannucci3d6d2d22023-05-11 17:25:05 +00001449 cmd = ['vpython3', PRESUBMIT_SUPPORT] + args
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001450 if resultdb and realm:
1451 cmd = ['rdb', 'stream', '-new', '-realm', realm, '--'] + cmd
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001452
1453 p = subprocess2.Popen(cmd)
Edward Lemur227d5102020-02-25 23:45:35 +00001454 exit_code = p.wait()
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001455
Edward Lemur227d5102020-02-25 23:45:35 +00001456 metrics.collector.add_repeated('sub_commands', {
1457 'command': 'presubmit',
1458 'execution_time': time_time() - start,
1459 'exit_code': exit_code,
1460 })
1461
1462 if exit_code:
1463 sys.exit(exit_code)
1464
1465 json_results = gclient_utils.FileRead(json_output)
1466 return json.loads(json_results)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001467
Robert Iannucci3d6d2d22023-05-11 17:25:05 +00001468 def RunPostUploadHook(self, verbose, upstream, description):
Edward Lemur75526302020-02-27 22:31:05 +00001469 args = self._GetCommonPresubmitArgs(verbose, upstream)
1470 args.append('--post_upload')
1471
1472 with gclient_utils.temporary_file() as description_file:
Edward Lemur1a83da12020-03-04 21:18:36 +00001473 gclient_utils.FileWrite(description_file, description)
Edward Lemur75526302020-02-27 22:31:05 +00001474 args.extend(['--description_file', description_file])
Robert Iannucci3d6d2d22023-05-11 17:25:05 +00001475 subprocess2.Popen(['vpython3', PRESUBMIT_SUPPORT] + args).wait()
Edward Lemur75526302020-02-27 22:31:05 +00001476
Edward Lemur5a644f82020-03-18 16:44:57 +00001477 def _GetDescriptionForUpload(self, options, git_diff_args, files):
Joanna Wangb46232e2023-01-21 01:58:46 +00001478 # type: (optparse.Values, Sequence[str], Sequence[str]
1479 # ) -> ChangeDescription
1480 """Get description message for upload."""
Edward Lemur5a644f82020-03-18 16:44:57 +00001481 if self.GetIssue():
1482 description = self.FetchDescription()
1483 elif options.message:
1484 description = options.message
1485 else:
1486 description = _create_description_from_log(git_diff_args)
1487 if options.title and options.squash:
Edward Lesmes0dd54822020-03-26 18:24:25 +00001488 description = options.title + '\n\n' + description
Edward Lemur5a644f82020-03-18 16:44:57 +00001489
Edward Lemur5a644f82020-03-18 16:44:57 +00001490 bug = options.bug
1491 fixed = options.fixed
Josip Sokcevic340edc32021-07-08 17:01:46 +00001492 if not self.GetIssue():
1493 # Extract bug number from branch name, but only if issue is being created.
1494 # It must start with bug or fix, followed by _ or - and number.
1495 # Optionally, it may contain _ or - after number with arbitrary text.
1496 # Examples:
1497 # bug-123
1498 # bug_123
1499 # fix-123
1500 # fix-123-some-description
mlcui7a0b4cb2023-01-23 23:14:55 +00001501 branch = self.GetBranch()
1502 if branch is not None:
1503 match = re.match(
1504 r'^(?P<type>bug|fix(?:e[sd])?)[_-]?(?P<bugnum>\d+)([-_]|$)', branch)
1505 if not bug and not fixed and match:
1506 if match.group('type') == 'bug':
1507 bug = match.group('bugnum')
1508 else:
1509 fixed = match.group('bugnum')
Edward Lemur5a644f82020-03-18 16:44:57 +00001510
1511 change_description = ChangeDescription(description, bug, fixed)
1512
Joanna Wang39811b12023-01-20 23:09:48 +00001513 # Fill gaps in OWNERS coverage to reviewers if requested.
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001514 if options.add_owners_to:
Joanna Wang39811b12023-01-20 23:09:48 +00001515 assert options.add_owners_to in ('R'), options.add_owners_to
Edward Lesmese1576912021-02-16 21:53:34 +00001516 status = self.owners_client.GetFilesApprovalStatus(
Joanna Wang39811b12023-01-20 23:09:48 +00001517 files, [], options.reviewers)
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001518 missing_files = [
1519 f for f in files
Edward Lesmese1576912021-02-16 21:53:34 +00001520 if status[f] == self._owners_client.INSUFFICIENT_REVIEWERS
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001521 ]
Edward Lesmese1576912021-02-16 21:53:34 +00001522 owners = self.owners_client.SuggestOwners(
1523 missing_files, exclude=[self.GetAuthor()])
Joanna Wang39811b12023-01-20 23:09:48 +00001524 assert isinstance(options.reviewers, list), options.reviewers
1525 options.reviewers.extend(owners)
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001526
Edward Lemur5a644f82020-03-18 16:44:57 +00001527 # Set the reviewer list now so that presubmit checks can access it.
Joanna Wang39811b12023-01-20 23:09:48 +00001528 if options.reviewers:
1529 change_description.update_reviewers(options.reviewers)
Edward Lemur5a644f82020-03-18 16:44:57 +00001530
1531 return change_description
1532
Joanna Wanga1abbed2023-01-24 01:41:05 +00001533 def _GetTitleForUpload(self, options, multi_change_upload=False):
1534 # type: (optparse.Values, Optional[bool]) -> str
1535
1536 # Getting titles for multipl commits is not supported so we return the
1537 # default.
1538 if not options.squash or multi_change_upload or options.title:
Edward Lemur5a644f82020-03-18 16:44:57 +00001539 return options.title
1540
1541 # On first upload, patchset title is always this string, while options.title
1542 # gets converted to first line of message.
1543 if not self.GetIssue():
1544 return 'Initial upload'
1545
1546 # When uploading subsequent patchsets, options.message is taken as the title
1547 # if options.title is not provided.
Edward Lemur5a644f82020-03-18 16:44:57 +00001548 if options.message:
1549 return options.message.strip()
1550
1551 # Use the subject of the last commit as title by default.
Edward Lesmes50da7702020-03-30 19:23:43 +00001552 title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
Mario Bianuccicebfb4e2020-07-22 23:08:16 +00001553 if options.force or options.skip_title:
Edward Lemur5a644f82020-03-18 16:44:57 +00001554 return title
Edward Lesmesae3586b2020-03-23 21:21:14 +00001555 user_title = gclient_utils.AskForData('Title for patchset [%s]: ' % title)
mlcui3da91712021-05-05 10:00:30 +00001556
1557 # Use the default title if the user confirms the default with a 'y'.
1558 if user_title.lower() == 'y':
1559 return title
Edward Lesmesae3586b2020-03-23 21:21:14 +00001560 return user_title or title
Edward Lemur5a644f82020-03-18 16:44:57 +00001561
Joanna Wang562481d2023-01-26 21:57:14 +00001562 def _GetRefSpecOptions(self,
1563 options,
1564 change_desc,
1565 multi_change_upload=False,
1566 dogfood_path=False):
1567 # type: (optparse.Values, Sequence[Changelist], Optional[bool],
1568 # Optional[bool]) -> Sequence[str]
Joanna Wanga1abbed2023-01-24 01:41:05 +00001569
1570 # Extra options that can be specified at push time. Doc:
1571 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
1572 refspec_opts = []
1573
1574 # By default, new changes are started in WIP mode, and subsequent patchsets
1575 # don't send email. At any time, passing --send-mail or --send-email will
1576 # mark the change ready and send email for that particular patch.
1577 if options.send_mail:
1578 refspec_opts.append('ready')
1579 refspec_opts.append('notify=ALL')
Joanna Wang562481d2023-01-26 21:57:14 +00001580 elif (not self.GetIssue() and options.squash and not dogfood_path):
Joanna Wanga1abbed2023-01-24 01:41:05 +00001581 refspec_opts.append('wip')
1582 else:
1583 refspec_opts.append('notify=NONE')
1584
1585 # TODO(tandrii): options.message should be posted as a comment if
1586 # --send-mail or --send-email is set on non-initial upload as Rietveld used
1587 # to do it.
1588
1589 # Set options.title in case user was prompted in _GetTitleForUpload and
1590 # _CMDUploadChange needs to be called again.
1591 options.title = self._GetTitleForUpload(
1592 options, multi_change_upload=multi_change_upload)
1593
1594 if options.title:
1595 # Punctuation and whitespace in |title| must be percent-encoded.
1596 refspec_opts.append('m=' +
1597 gerrit_util.PercentEncodeForGitRef(options.title))
1598
1599 if options.private:
1600 refspec_opts.append('private')
1601
1602 if options.topic:
1603 # Documentation on Gerrit topics is here:
1604 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
1605 refspec_opts.append('topic=%s' % options.topic)
1606
1607 if options.enable_auto_submit:
1608 refspec_opts.append('l=Auto-Submit+1')
1609 if options.set_bot_commit:
1610 refspec_opts.append('l=Bot-Commit+1')
1611 if options.use_commit_queue:
1612 refspec_opts.append('l=Commit-Queue+2')
1613 elif options.cq_dry_run:
1614 refspec_opts.append('l=Commit-Queue+1')
1615 elif options.cq_quick_run:
1616 refspec_opts.append('l=Commit-Queue+1')
1617 refspec_opts.append('l=Quick-Run+1')
1618
1619 if change_desc.get_reviewers(tbr_only=True):
1620 score = gerrit_util.GetCodeReviewTbrScore(self.GetGerritHost(),
1621 self.GetGerritProject())
1622 refspec_opts.append('l=Code-Review+%s' % score)
1623
Joanna Wang40497912023-01-24 21:18:16 +00001624 # Gerrit sorts hashtags, so order is not important.
1625 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
1626 # We check GetIssue because we only add hashtags from the
1627 # description on the first upload.
Joanna Wang562481d2023-01-26 21:57:14 +00001628 # TODO(b/265929888): When we fully launch the new path:
1629 # 1) remove fetching hashtags from description alltogether
1630 # 2) Or use descrtiption hashtags for:
1631 # `not (self.GetIssue() and multi_change_upload)`
1632 # 3) Or enabled change description tags for multi and single changes
1633 # by adding them post `git push`.
1634 if not (self.GetIssue() and dogfood_path):
Joanna Wang40497912023-01-24 21:18:16 +00001635 hashtags.update(change_desc.get_hash_tags())
1636 refspec_opts.extend(['hashtag=%s' % t for t in hashtags])
Joanna Wang40497912023-01-24 21:18:16 +00001637
1638 # Note: Reviewers, and ccs are handled individually for each
Joanna Wanga1abbed2023-01-24 01:41:05 +00001639 # branch/change.
1640 return refspec_opts
1641
Joanna Wang05b60342023-03-29 20:25:57 +00001642 def PrepareSquashedCommit(self,
1643 options,
1644 parent,
1645 orig_parent,
1646 end_commit=None):
1647 # type: (optparse.Values, str, str, Optional[str]) -> _NewUpload()
1648 """Create a squashed commit to upload.
1649
1650
1651 Args:
1652 parent: The commit to use as the parent for the new squashed.
1653 orig_parent: The commit that is an actual ancestor of `end_commit`. It
1654 is part of the same original tree as end_commit, which does not
1655 contain squashed commits. This is used to create the change
1656 description for the new squashed commit with:
1657 `git log orig_parent..end_commit`.
1658 end_commit: The commit to use as the end of the new squashed commit.
1659 """
Joanna Wangb88a4342023-01-24 01:28:22 +00001660
1661 if end_commit is None:
1662 end_commit = RunGit(['rev-parse', self.branchref]).strip()
1663
Joanna Wang05b60342023-03-29 20:25:57 +00001664 reviewers, ccs, change_desc = self._PrepareChange(options, orig_parent,
Joanna Wangb88a4342023-01-24 01:28:22 +00001665 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,
Robert Iannucci3d6d2d22023-05-11 17:25:05 +00001801 new_upload.change_desc.description)
Joanna Wang40497912023-01-24 21:18:16 +00001802
1803 if new_upload.reviewers or new_upload.ccs:
1804 gerrit_util.AddReviewers(self.GetGerritHost(),
1805 self._GerritChangeIdentifier(),
1806 reviewers=new_upload.reviewers,
1807 ccs=new_upload.ccs,
1808 notify=bool(options.send_mail))
1809
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001810 def CMDUpload(self, options, git_diff_args, orig_args):
1811 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001812 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001813 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001814 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001815 else:
1816 if self.GetBranch() is None:
1817 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1818
1819 # Default to diffing against common ancestor of upstream branch
1820 base_branch = self.GetCommonAncestorWithUpstream()
1821 git_diff_args = [base_branch, 'HEAD']
1822
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001823 # Fast best-effort checks to abort before running potentially expensive
1824 # hooks if uploading is likely to fail anyway. Passing these checks does
1825 # not guarantee that uploading will not fail.
Edward Lemur125d60a2019-09-13 18:25:41 +00001826 self.EnsureAuthenticated(force=options.force)
1827 self.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001828
1829 # Apply watchlists on upload.
Edward Lemur2c62b332020-03-12 22:12:33 +00001830 watchlist = watchlists.Watchlists(settings.GetRoot())
1831 files = self.GetAffectedFiles(base_branch)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001832 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001833 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001834
Edward Lemur5a644f82020-03-18 16:44:57 +00001835 change_desc = self._GetDescriptionForUpload(options, git_diff_args, files)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001836 if not options.bypass_hooks:
Joanna Wanga8db0cb2023-01-24 15:43:17 +00001837 hook_results = self.RunHook(committing=False,
1838 may_prompt=not options.force,
1839 verbose=options.verbose,
1840 parallel=options.parallel,
1841 upstream=base_branch,
1842 description=change_desc.description,
1843 all_files=False)
Edward Lemur227d5102020-02-25 23:45:35 +00001844 self.ExtendCC(hook_results['more_cc'])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001845
Aaron Gable13101a62018-02-09 13:20:41 -08001846 print_stats(git_diff_args)
Edward Lemura12175c2020-03-09 16:58:26 +00001847 ret = self.CMDUploadChange(
Edward Lemur5a644f82020-03-18 16:44:57 +00001848 options, git_diff_args, custom_cl_base, change_desc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001849 if not ret:
mlcui7a0b4cb2023-01-23 23:14:55 +00001850 if self.GetBranch() is not None:
1851 self._GitSetBranchConfigValue(
1852 LAST_UPLOAD_HASH_CONFIG_KEY,
1853 scm.GIT.ResolveCommit(settings.GetRoot(), 'HEAD'))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001854 # Run post upload hooks, if specified.
1855 if settings.GetRunPostUploadHook():
Brian Sheedy7326ca22022-11-02 18:36:17 +00001856 self.RunPostUploadHook(options.verbose, base_branch,
Robert Iannucci3d6d2d22023-05-11 17:25:05 +00001857 change_desc.description)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001858
1859 # Upload all dependencies if specified.
1860 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001861 print()
1862 print('--dependencies has been specified.')
1863 print('All dependent local branches will be re-uploaded.')
1864 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001865 # Remove the dependencies flag from args so that we do not end up in a
1866 # loop.
1867 orig_args.remove('--dependencies')
Jose Lopes3863fc52020-04-07 17:00:25 +00001868 ret = upload_branch_deps(self, orig_args, options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001869 return ret
1870
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001871 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001872 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001873
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00001874 Issue must have been already uploaded and known. Optionally allows for
1875 updating Quick-Run (QR) state.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001876 """
1877 assert new_state in _CQState.ALL_STATES
1878 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001879 try:
Edward Lemur125d60a2019-09-13 18:25:41 +00001880 vote_map = {
1881 _CQState.NONE: 0,
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00001882 _CQState.QUICK_RUN: 1,
Edward Lemur125d60a2019-09-13 18:25:41 +00001883 _CQState.DRY_RUN: 1,
1884 _CQState.COMMIT: 2,
1885 }
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00001886 if new_state == _CQState.QUICK_RUN:
1887 labels = {
1888 'Commit-Queue': vote_map[_CQState.DRY_RUN],
1889 'Quick-Run': vote_map[_CQState.QUICK_RUN],
1890 }
1891 else:
1892 labels = {'Commit-Queue': vote_map[new_state]}
Edward Lemur125d60a2019-09-13 18:25:41 +00001893 notify = False if new_state == _CQState.DRY_RUN else None
1894 gerrit_util.SetReview(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001895 self.GetGerritHost(), self._GerritChangeIdentifier(),
Edward Lemur125d60a2019-09-13 18:25:41 +00001896 labels=labels, notify=notify)
qyearsley1fdfcb62016-10-24 13:22:03 -07001897 return 0
1898 except KeyboardInterrupt:
1899 raise
1900 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001901 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001902 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001903 ' * Your project has no CQ,\n'
1904 ' * You don\'t have permission to change the CQ state,\n'
1905 ' * There\'s a bug in this code (see stack trace below).\n'
1906 'Consider specifying which bots to trigger manually or asking your '
1907 'project owners for permissions or contacting Chrome Infra at:\n'
1908 'https://www.chromium.org/infra\n\n' %
1909 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001910 # Still raise exception so that stack trace is printed.
1911 raise
1912
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001913 def GetGerritHost(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001914 # Lazy load of configs.
1915 self.GetCodereviewServer()
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00001916
tandriie32e3ea2016-06-22 02:52:48 -07001917 if self._gerrit_host and '.' not in self._gerrit_host:
1918 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
Edward Lemur79d4f992019-11-11 23:49:02 +00001919 parsed = urllib.parse.urlparse(self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001920 if parsed.scheme == 'sso':
tandriie32e3ea2016-06-22 02:52:48 -07001921 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1922 self._gerrit_server = 'https://%s' % self._gerrit_host
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00001923
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001924 return self._gerrit_host
1925
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001926 def _GetGitHost(self):
1927 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001928 remote_url = self.GetRemoteUrl()
1929 if not remote_url:
1930 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001931 return urllib.parse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001932
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001933 def GetCodereviewServer(self):
1934 if not self._gerrit_server:
1935 # If we're on a branch then get the server potentially associated
1936 # with that branch.
Edward Lemur85153282020-02-14 22:06:29 +00001937 if self.GetIssue() and self.GetBranch():
tandrii5d48c322016-08-18 16:19:37 -07001938 self._gerrit_server = self._GitGetBranchConfigValue(
Edward Lesmes50da7702020-03-30 19:23:43 +00001939 CODEREVIEW_SERVER_CONFIG_KEY)
tandrii5d48c322016-08-18 16:19:37 -07001940 if self._gerrit_server:
Edward Lemur79d4f992019-11-11 23:49:02 +00001941 self._gerrit_host = urllib.parse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001942 if not self._gerrit_server:
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00001943 url = urllib.parse.urlparse(self.GetRemoteUrl())
1944 parts = url.netloc.split('.')
1945
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001946 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1947 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001948 parts[0] = parts[0] + '-review'
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00001949
1950 if url.scheme == 'sso' and len(parts) == 1:
1951 # sso:// uses abbreivated hosts, eg. sso://chromium instead of
1952 # chromium.googlesource.com. Hence, for code review server, they need
1953 # to be expanded.
1954 parts[0] += '.googlesource.com'
1955
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001956 self._gerrit_host = '.'.join(parts)
1957 self._gerrit_server = 'https://%s' % self._gerrit_host
1958 return self._gerrit_server
1959
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001960 def GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001961 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001962 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001963 if remote_url is None:
Josip906bfde2020-01-31 22:38:49 +00001964 logging.warning('can\'t detect Gerrit project.')
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001965 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001966 project = urllib.parse.urlparse(remote_url).path.strip('/')
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001967 if project.endswith('.git'):
1968 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001969 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1970 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1971 # gitiles/git-over-https protocol. E.g.,
1972 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1973 # as
1974 # https://chromium.googlesource.com/v8/v8
1975 if project.startswith('a/'):
1976 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001977 return project
1978
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001979 def _GerritChangeIdentifier(self):
1980 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1981
1982 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001983 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001984 """
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001985 project = self.GetGerritProject()
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001986 if project:
1987 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1988 # Fall back on still unique, but less efficient change number.
1989 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001990
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001991 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001992 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00001993 if settings.GetGerritSkipEnsureAuthenticated():
1994 # For projects with unusual authentication schemes.
1995 # See http://crbug.com/603378.
1996 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001997
1998 # Check presence of cookies only if using cookies-based auth method.
1999 cookie_auth = gerrit_util.Authenticator.get()
2000 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002001 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002002
Florian Mayerae510e82020-01-30 21:04:48 +00002003 remote_url = self.GetRemoteUrl()
2004 if remote_url is None:
Josip906bfde2020-01-31 22:38:49 +00002005 logging.warning('invalid remote')
Florian Mayerae510e82020-01-30 21:04:48 +00002006 return
Joanna Wang46ffd1b2022-09-16 20:44:44 +00002007 if urllib.parse.urlparse(remote_url).scheme not in ['https', 'sso']:
2008 logging.warning(
2009 'Ignoring branch %(branch)s with non-https/sso remote '
2010 '%(remote)s', {
2011 'branch': self.branch,
2012 'remote': self.GetRemoteUrl()
2013 })
Daniel Chengcf6269b2019-05-18 01:02:12 +00002014 return
2015
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002016 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002017 self.GetCodereviewServer()
2018 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00002019 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002020
2021 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2022 git_auth = cookie_auth.get_auth_header(git_host)
2023 if gerrit_auth and git_auth:
2024 if gerrit_auth == git_auth:
2025 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002026 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
Raul Tambre80ee78e2019-05-06 22:41:05 +00002027 print(
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002028 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002029 ' %s\n'
2030 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002031 ' Consider running the following command:\n'
2032 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002033 ' %s\n'
Raul Tambre80ee78e2019-05-06 22:41:05 +00002034 ' %s' %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002035 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002036 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002037 cookie_auth.get_new_password_message(git_host)))
2038 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002039 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002040 return
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002041
2042 missing = (
2043 ([] if gerrit_auth else [self._gerrit_host]) +
2044 ([] if git_auth else [git_host]))
2045 DieWithError('Credentials for the following hosts are required:\n'
2046 ' %s\n'
2047 'These are read from %s (or legacy %s)\n'
2048 '%s' % (
2049 '\n '.join(missing),
2050 cookie_auth.get_gitcookies_path(),
2051 cookie_auth.get_netrc_path(),
2052 cookie_auth.get_new_password_message(git_host)))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002053
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002054 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002055 if not self.GetIssue():
2056 return
2057
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002058 status = self._GetChangeDetail()['status']
Joanna Wang583ca662022-04-27 21:17:17 +00002059 if status == 'ABANDONED':
Bruce Dawsoneb8426e2022-08-05 23:58:15 +00002060 DieWithError(
2061 'Change %s has been abandoned, new uploads are not allowed' %
2062 (self.GetIssueURL()))
Joanna Wang583ca662022-04-27 21:17:17 +00002063 if status == 'MERGED':
2064 answer = gclient_utils.AskForData(
2065 'Change %s has been submitted, new uploads are not allowed. '
2066 'Would you like to start a new change (Y/n)?' % self.GetIssueURL()
2067 ).lower()
2068 if answer not in ('y', ''):
2069 DieWithError('New uploads are not allowed.')
2070 self.SetIssue()
2071 return
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002072
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002073 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2074 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2075 # Apparently this check is not very important? Otherwise get_auth_email
2076 # could have been added to other implementations of Authenticator.
2077 cookies_auth = gerrit_util.Authenticator.get()
2078 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002079 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002080
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002081 cookies_user = cookies_auth.get_auth_email(self.GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002082 if self.GetIssueOwner() == cookies_user:
2083 return
2084 logging.debug('change %s owner is %s, cookies user is %s',
2085 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002086 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002087 # so ask what Gerrit thinks of this user.
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002088 details = gerrit_util.GetAccountDetails(self.GetGerritHost(), 'self')
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002089 if details['email'] == self.GetIssueOwner():
2090 return
2091 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002092 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002093 'as %s.\n'
2094 'Uploading may fail due to lack of permissions.' %
2095 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2096 confirm_or_exit(action='upload')
2097
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002098 def GetStatus(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002099 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002100 or CQ status, assuming adherence to a common workflow.
2101
2102 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002103 * 'error' - error from review tool (including deleted issues)
2104 * 'unsent' - no reviewers added
2105 * 'waiting' - waiting for review
2106 * 'reply' - waiting for uploader to reply to review
2107 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002108 * 'dry-run' - dry-running in the CQ
2109 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07002110 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002111 """
2112 if not self.GetIssue():
2113 return None
2114
2115 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002116 data = self._GetChangeDetail([
2117 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Edward Lemur79d4f992019-11-11 23:49:02 +00002118 except GerritChangeNotExists:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002119 return 'error'
2120
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002121 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002122 return 'closed'
2123
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002124 cq_label = data['labels'].get('Commit-Queue', {})
2125 max_cq_vote = 0
2126 for vote in cq_label.get('all', []):
2127 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
2128 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002129 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002130 if max_cq_vote == 1:
2131 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002132
Aaron Gable9ab38c62017-04-06 14:36:33 -07002133 if data['labels'].get('Code-Review', {}).get('approved'):
2134 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002135
2136 if not data.get('reviewers', {}).get('REVIEWER', []):
2137 return 'unsent'
2138
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002139 owner = data['owner'].get('_account_id')
Edward Lemur79d4f992019-11-11 23:49:02 +00002140 messages = sorted(data.get('messages', []), key=lambda m: m.get('date'))
Andrii Shyshkalov8aa9d622020-03-10 19:15:35 +00002141 while messages:
2142 m = messages.pop()
Andrii Shyshkalov899785a2021-07-09 12:45:37 +00002143 if (m.get('tag', '').startswith('autogenerated:cq') or
2144 m.get('tag', '').startswith('autogenerated:cv')):
2145 # Ignore replies from LUCI CV/CQ.
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002146 continue
Andrii Shyshkalov8aa9d622020-03-10 19:15:35 +00002147 if m.get('author', {}).get('_account_id') == owner:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002148 # Most recent message was by owner.
2149 return 'waiting'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002150
2151 # Some reply from non-owner.
2152 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002153
2154 # Somehow there are no messages even though there are reviewers.
2155 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002156
Gavin Mak4e5e3992022-11-14 22:40:12 +00002157 def GetMostRecentPatchset(self, update=True):
Edward Lemur6c6827c2020-02-06 21:15:18 +00002158 if not self.GetIssue():
2159 return None
2160
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002161 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002162 patchset = data['revisions'][data['current_revision']]['_number']
Gavin Mak4e5e3992022-11-14 22:40:12 +00002163 if update:
2164 self.SetPatchset(patchset)
Aaron Gablee8856ee2017-12-07 12:41:46 -08002165 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002166
Gavin Makf35a9eb2022-11-17 18:34:36 +00002167 def _IsPatchsetRangeSignificant(self, lower, upper):
2168 """Returns True if the inclusive range of patchsets contains any reworks or
2169 rebases."""
2170 if not self.GetIssue():
2171 return False
2172
2173 data = self._GetChangeDetail(['ALL_REVISIONS'])
2174 ps_kind = {}
2175 for rev_info in data.get('revisions', {}).values():
2176 ps_kind[rev_info['_number']] = rev_info.get('kind', '')
2177
2178 for ps in range(lower, upper + 1):
2179 assert ps in ps_kind, 'expected patchset %d in change detail' % ps
2180 if ps_kind[ps] not in ('NO_CHANGE', 'NO_CODE_CHANGE'):
2181 return True
2182 return False
2183
Gavin Make61ccc52020-11-13 00:12:57 +00002184 def GetMostRecentDryRunPatchset(self):
2185 """Get patchsets equivalent to the most recent patchset and return
2186 the patchset with the latest dry run. If none have been dry run, return
2187 the latest patchset."""
2188 if not self.GetIssue():
2189 return None
2190
2191 data = self._GetChangeDetail(['ALL_REVISIONS'])
2192 patchset = data['revisions'][data['current_revision']]['_number']
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002193 dry_run = {int(m['_revision_number'])
2194 for m in data.get('messages', [])
2195 if m.get('tag', '').endswith('dry-run')}
Gavin Make61ccc52020-11-13 00:12:57 +00002196
2197 for revision_info in sorted(data.get('revisions', {}).values(),
2198 key=lambda c: c['_number'], reverse=True):
2199 if revision_info['_number'] in dry_run:
2200 patchset = revision_info['_number']
2201 break
2202 if revision_info.get('kind', '') not in \
2203 ('NO_CHANGE', 'NO_CODE_CHANGE', 'TRIVIAL_REBASE'):
2204 break
2205 self.SetPatchset(patchset)
2206 return patchset
2207
Aaron Gable636b13f2017-07-14 10:42:48 -07002208 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002209 gerrit_util.SetReview(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002210 self.GetGerritHost(), self._GerritChangeIdentifier(),
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002211 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002212
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002213 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002214 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002215 # CURRENT_REVISION is included to get the latest patchset so that
2216 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002217 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002218 options=['MESSAGES', 'DETAILED_ACCOUNTS',
2219 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002220 file_comments = gerrit_util.GetChangeComments(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002221 self.GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002222 robot_file_comments = gerrit_util.GetChangeRobotComments(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002223 self.GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002224
2225 # Add the robot comments onto the list of comments, but only
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +00002226 # keep those that are from the latest patchset.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002227 latest_patch_set = self.GetMostRecentPatchset()
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002228 for path, robot_comments in robot_file_comments.items():
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002229 line_comments = file_comments.setdefault(path, [])
2230 line_comments.extend(
2231 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002232
2233 # Build dictionary of file comments for easy access and sorting later.
2234 # {author+date: {path: {patchset: {line: url+message}}}}
2235 comments = collections.defaultdict(
2236 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
Andrii Shyshkalova3762a92020-11-25 10:20:42 +00002237
2238 server = self.GetCodereviewServer()
2239 if server in _KNOWN_GERRIT_TO_SHORT_URLS:
2240 # /c/ is automatically added by short URL server.
2241 url_prefix = '%s/%s' % (_KNOWN_GERRIT_TO_SHORT_URLS[server],
2242 self.GetIssue())
2243 else:
2244 url_prefix = '%s/c/%s' % (server, self.GetIssue())
2245
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002246 for path, line_comments in file_comments.items():
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002247 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002248 tag = comment.get('tag', '')
2249 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002250 continue
2251 key = (comment['author']['email'], comment['updated'])
2252 if comment.get('side', 'REVISION') == 'PARENT':
2253 patchset = 'Base'
2254 else:
2255 patchset = 'PS%d' % comment['patch_set']
2256 line = comment.get('line', 0)
Andrii Shyshkalova3762a92020-11-25 10:20:42 +00002257 url = ('%s/%s/%s#%s%s' %
2258 (url_prefix, comment['patch_set'], path,
2259 'b' if comment.get('side') == 'PARENT' else '',
2260 str(line) if line else ''))
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002261 comments[key][path][patchset][line] = (url, comment['message'])
2262
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002263 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002264 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002265 summary = self._BuildCommentSummary(msg, comments, readable)
2266 if summary:
2267 summaries.append(summary)
2268 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002269
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002270 @staticmethod
2271 def _BuildCommentSummary(msg, comments, readable):
Josip Sokcevic266129c2021-11-09 00:22:00 +00002272 if 'email' not in msg['author']:
2273 # Some bot accounts may not have an email associated.
2274 return None
2275
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002276 key = (msg['author']['email'], msg['date'])
2277 # Don't bother showing autogenerated messages that don't have associated
2278 # file or line comments. this will filter out most autogenerated
2279 # messages, but will keep robot comments like those from Tricium.
2280 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
2281 if is_autogenerated and not comments.get(key):
2282 return None
2283 message = msg['message']
2284 # Gerrit spits out nanoseconds.
2285 assert len(msg['date'].split('.')[-1]) == 9
2286 date = datetime.datetime.strptime(msg['date'][:-3],
2287 '%Y-%m-%d %H:%M:%S.%f')
2288 if key in comments:
2289 message += '\n'
2290 for path, patchsets in sorted(comments.get(key, {}).items()):
2291 if readable:
2292 message += '\n%s' % path
2293 for patchset, lines in sorted(patchsets.items()):
2294 for line, (url, content) in sorted(lines.items()):
2295 if line:
2296 line_str = 'Line %d' % line
2297 path_str = '%s:%d:' % (path, line)
2298 else:
2299 line_str = 'File comment'
2300 path_str = '%s:0:' % path
2301 if readable:
2302 message += '\n %s, %s: %s' % (patchset, line_str, url)
2303 message += '\n %s\n' % content
2304 else:
2305 message += '\n%s ' % path_str
2306 message += '\n%s\n' % content
2307
2308 return _CommentSummary(
2309 date=date,
2310 message=message,
2311 sender=msg['author']['email'],
2312 autogenerated=is_autogenerated,
2313 # These could be inferred from the text messages and correlated with
2314 # Code-Review label maximum, however this is not reliable.
2315 # Leaving as is until the need arises.
2316 approval=False,
2317 disapproval=False,
2318 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002319
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002320 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002321 gerrit_util.AbandonChange(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002322 self.GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002323
Xinan Lin1bd4ffa2021-07-28 00:54:22 +00002324 def SubmitIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002325 gerrit_util.SubmitChange(
Xinan Lin1bd4ffa2021-07-28 00:54:22 +00002326 self.GetGerritHost(), self._GerritChangeIdentifier())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002327
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002328 def _GetChangeDetail(self, options=None):
2329 """Returns details of associated Gerrit change and caching results."""
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002330 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002331 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002332
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002333 # Optimization to avoid multiple RPCs:
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002334 if 'CURRENT_REVISION' in options or 'ALL_REVISIONS' in options:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002335 options.append('CURRENT_COMMIT')
2336
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002337 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002338 cache_key = str(self.GetIssue())
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002339 options_set = frozenset(o.upper() for o in options)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002340
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002341 for cached_options_set, data in self._detail_cache.get(cache_key, []):
2342 # Assumption: data fetched before with extra options is suitable
2343 # for return for a smaller set of options.
2344 # For example, if we cached data for
2345 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2346 # and request is for options=[CURRENT_REVISION],
2347 # THEN we can return prior cached data.
2348 if options_set.issubset(cached_options_set):
2349 return data
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002350
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002351 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002352 data = gerrit_util.GetChangeDetail(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002353 self.GetGerritHost(), self._GerritChangeIdentifier(), options_set)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002354 except gerrit_util.GerritError as e:
2355 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002356 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002357 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002358
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002359 self._detail_cache.setdefault(cache_key, []).append((options_set, data))
tandriic2405f52016-10-10 08:13:15 -07002360 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002361
Gavin Mak4e5e3992022-11-14 22:40:12 +00002362 def _GetChangeCommit(self, revision='current'):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002363 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002364 try:
Gavin Mak4e5e3992022-11-14 22:40:12 +00002365 data = gerrit_util.GetChangeCommit(self.GetGerritHost(),
2366 self._GerritChangeIdentifier(),
2367 revision)
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002368 except gerrit_util.GerritError as e:
2369 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002370 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002371 raise
agable32978d92016-11-01 12:55:02 -07002372 return data
2373
Karen Qian40c19422019-03-13 21:28:29 +00002374 def _IsCqConfigured(self):
2375 detail = self._GetChangeDetail(['LABELS'])
Andrii Shyshkalov8effa4d2020-01-21 13:23:36 +00002376 return u'Commit-Queue' in detail.get('labels', {})
Karen Qian40c19422019-03-13 21:28:29 +00002377
Saagar Sanghavi03b15132020-08-10 16:43:41 +00002378 def CMDLand(self, force, bypass_hooks, verbose, parallel, resultdb, realm):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002379 if git_common.is_dirty_git_tree('land'):
2380 return 1
Karen Qian40c19422019-03-13 21:28:29 +00002381
tandriid60367b2016-06-22 05:25:12 -07002382 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00002383 if not force and self._IsCqConfigured():
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002384 confirm_or_exit('\nIt seems this repository has a CQ, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002385 'which can test and land changes for you. '
2386 'Are you sure you wish to bypass it?\n',
2387 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002388 differs = True
Gavin Makbe2e9262022-11-08 23:41:55 +00002389 last_upload = self._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002390 # Note: git diff outputs nothing if there is no diff.
2391 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002392 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002393 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002394 if detail['current_revision'] == last_upload:
2395 differs = False
2396 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002397 print('WARNING: Local branch contents differ from latest uploaded '
2398 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002399 if differs:
2400 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002401 confirm_or_exit(
2402 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2403 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002404 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002405 elif not bypass_hooks:
Edward Lemur227d5102020-02-25 23:45:35 +00002406 upstream = self.GetCommonAncestorWithUpstream()
2407 if self.GetIssue():
2408 description = self.FetchDescription()
2409 else:
Edward Lemura12175c2020-03-09 16:58:26 +00002410 description = _create_description_from_log([upstream])
Edward Lemur227d5102020-02-25 23:45:35 +00002411 self.RunHook(
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002412 committing=True,
2413 may_prompt=not force,
2414 verbose=verbose,
Edward Lemur227d5102020-02-25 23:45:35 +00002415 parallel=parallel,
2416 upstream=upstream,
2417 description=description,
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00002418 all_files=False,
Saagar Sanghavi03b15132020-08-10 16:43:41 +00002419 resultdb=resultdb,
2420 realm=realm)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002421
Xinan Lin1bd4ffa2021-07-28 00:54:22 +00002422 self.SubmitIssue()
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002423 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002424 links = self._GetChangeCommit().get('web_links', [])
2425 for link in links:
Michael Mosse371c642021-09-29 16:41:04 +00002426 if link.get('name') in ['gitiles', 'browse'] and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002427 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002428 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002429 return 0
2430
Bruce Dawsonf362f6f2021-02-18 23:15:17 +00002431 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force,
2432 newbranch):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002433 assert parsed_issue_arg.valid
2434
Edward Lemur125d60a2019-09-13 18:25:41 +00002435 self.issue = parsed_issue_arg.issue
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002436
2437 if parsed_issue_arg.hostname:
2438 self._gerrit_host = parsed_issue_arg.hostname
2439 self._gerrit_server = 'https://%s' % self._gerrit_host
2440
tandriic2405f52016-10-10 08:13:15 -07002441 try:
2442 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002443 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002444 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002445
2446 if not parsed_issue_arg.patchset:
2447 # Use current revision by default.
2448 revision_info = detail['revisions'][detail['current_revision']]
2449 patchset = int(revision_info['_number'])
2450 else:
2451 patchset = parsed_issue_arg.patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002452 for revision_info in detail['revisions'].values():
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002453 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2454 break
2455 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002456 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002457 (parsed_issue_arg.patchset, self.GetIssue()))
2458
Edward Lemur125d60a2019-09-13 18:25:41 +00002459 remote_url = self.GetRemoteUrl()
Aaron Gable697a91b2018-01-19 15:20:15 -08002460 if remote_url.endswith('.git'):
2461 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002462 remote_url = remote_url.rstrip('/')
2463
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002464 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002465 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002466
2467 if remote_url != fetch_info['url']:
2468 DieWithError('Trying to patch a change from %s but this repo appears '
2469 'to be %s.' % (fetch_info['url'], remote_url))
2470
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002471 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002472
Joanna Wangc023a632023-01-26 17:59:25 +00002473 # Set issue immediately in case the cherry-pick fails, which happens
2474 # when resolving conflicts.
2475 if self.GetBranch():
Bruce Dawsonf362f6f2021-02-18 23:15:17 +00002476 self.SetIssue(parsed_issue_arg.issue)
2477
Aaron Gable62619a32017-06-16 08:22:09 -07002478 if force:
2479 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2480 print('Checked out commit for change %i patchset %i locally' %
2481 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002482 elif nocommit:
2483 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2484 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002485 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002486 RunGit(['cherry-pick', 'FETCH_HEAD'])
2487 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002488 (parsed_issue_arg.issue, patchset))
2489 print('Note: this created a local commit which does not have '
2490 'the same hash as the one uploaded for review. This will make '
2491 'uploading changes based on top of this branch difficult.\n'
2492 'If you want to do that, use "git cl patch --force" instead.')
2493
Stefan Zagerd08043c2017-10-12 12:07:02 -07002494 if self.GetBranch():
Stefan Zagerd08043c2017-10-12 12:07:02 -07002495 self.SetPatchset(patchset)
Edward Lesmes50da7702020-03-30 19:23:43 +00002496 fetched_hash = scm.GIT.ResolveCommit(settings.GetRoot(), 'FETCH_HEAD')
Gavin Makbe2e9262022-11-08 23:41:55 +00002497 self._GitSetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY, fetched_hash)
2498 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY, fetched_hash)
Stefan Zagerd08043c2017-10-12 12:07:02 -07002499 else:
2500 print('WARNING: You are in detached HEAD state.\n'
2501 'The patch has been applied to your checkout, but you will not be '
2502 'able to upload a new patch set to the gerrit issue.\n'
2503 'Try using the \'-b\' option if you would like to work on a '
2504 'branch and/or upload a new patch set.')
2505
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002506 return 0
2507
Joanna Wang18de1f62023-01-21 01:24:24 +00002508 @staticmethod
2509 def _GerritCommitMsgHookCheck(offer_removal):
2510 # type: (bool) -> None
2511 """Checks for the gerrit's commit-msg hook and removes it if necessary."""
tandrii16e0b4e2016-06-07 10:34:28 -07002512 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2513 if not os.path.exists(hook):
2514 return
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002515 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
2516 # custom developer-made one.
tandrii16e0b4e2016-06-07 10:34:28 -07002517 data = gclient_utils.FileRead(hook)
2518 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2519 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002520 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002521 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002522 'and may interfere with it in subtle ways.\n'
2523 'We recommend you remove the commit-msg hook.')
2524 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002525 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002526 gclient_utils.rm_file_or_tree(hook)
2527 print('Gerrit commit-msg hook removed.')
2528 else:
2529 print('OK, will keep Gerrit commit-msg hook in place.')
2530
Edward Lemur1b52d872019-05-09 21:12:12 +00002531 def _CleanUpOldTraces(self):
2532 """Keep only the last |MAX_TRACES| traces."""
2533 try:
2534 traces = sorted([
2535 os.path.join(TRACES_DIR, f)
2536 for f in os.listdir(TRACES_DIR)
2537 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2538 and not f.startswith('tmp'))
2539 ])
2540 traces_to_delete = traces[:-MAX_TRACES]
2541 for trace in traces_to_delete:
Daniel Chengcf6269b2019-05-18 01:02:12 +00002542 os.remove(trace)
Edward Lemur1b52d872019-05-09 21:12:12 +00002543 except OSError:
2544 print('WARNING: Failed to remove old git traces from\n'
2545 ' %s'
2546 'Consider removing them manually.' % TRACES_DIR)
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002547
Edward Lemur5737f022019-05-17 01:24:00 +00002548 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
Edward Lemur1b52d872019-05-09 21:12:12 +00002549 """Zip and write the git push traces stored in traces_dir."""
2550 gclient_utils.safe_makedirs(TRACES_DIR)
Edward Lemur1b52d872019-05-09 21:12:12 +00002551 traces_zip = trace_name + '-traces'
2552 traces_readme = trace_name + '-README'
Michael Mosse7f0b4c2019-05-08 04:36:24 +00002553 # Create a temporary dir to store git config and gitcookies in. It will be
2554 # compressed and stored next to the traces.
2555 git_info_dir = tempfile.mkdtemp()
Edward Lemur1b52d872019-05-09 21:12:12 +00002556 git_info_zip = trace_name + '-git-info'
2557
Josip Sokcevic5e18b602020-04-23 21:47:00 +00002558 git_push_metadata['now'] = datetime_now().strftime('%Y-%m-%dT%H:%M:%S.%f')
sangwoo.ko7a614332019-05-22 02:46:19 +00002559
Edward Lemur1b52d872019-05-09 21:12:12 +00002560 git_push_metadata['trace_name'] = trace_name
2561 gclient_utils.FileWrite(
2562 traces_readme, TRACES_README_FORMAT % git_push_metadata)
2563
2564 # Keep only the first 6 characters of the git hashes on the packet
2565 # trace. This greatly decreases size after compression.
2566 packet_traces = os.path.join(traces_dir, 'trace-packet')
2567 if os.path.isfile(packet_traces):
2568 contents = gclient_utils.FileRead(packet_traces)
2569 gclient_utils.FileWrite(
2570 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2571 shutil.make_archive(traces_zip, 'zip', traces_dir)
2572
2573 # Collect and compress the git config and gitcookies.
2574 git_config = RunGit(['config', '-l'])
2575 gclient_utils.FileWrite(
2576 os.path.join(git_info_dir, 'git-config'),
2577 git_config)
2578
2579 cookie_auth = gerrit_util.Authenticator.get()
2580 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2581 gitcookies_path = cookie_auth.get_gitcookies_path()
2582 if os.path.isfile(gitcookies_path):
2583 gitcookies = gclient_utils.FileRead(gitcookies_path)
2584 gclient_utils.FileWrite(
2585 os.path.join(git_info_dir, 'gitcookies'),
2586 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2587 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2588
Edward Lemur1b52d872019-05-09 21:12:12 +00002589 gclient_utils.rmtree(git_info_dir)
2590
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002591 def _RunGitPushWithTraces(self,
2592 refspec,
2593 refspec_opts,
2594 git_push_metadata,
2595 git_push_options=None):
Edward Lemur1b52d872019-05-09 21:12:12 +00002596 """Run git push and collect the traces resulting from the execution."""
2597 # Create a temporary directory to store traces in. Traces will be compressed
2598 # and stored in a 'traces' dir inside depot_tools.
2599 traces_dir = tempfile.mkdtemp()
Edward Lemur5737f022019-05-17 01:24:00 +00002600 trace_name = os.path.join(
2601 TRACES_DIR, datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
Edward Lemur0f58ae42019-04-30 17:24:12 +00002602
2603 env = os.environ.copy()
2604 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2605 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Jonathan Nieder9779b142019-05-29 23:19:29 +00002606 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002607 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2608 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2609 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2610
2611 try:
2612 push_returncode = 0
Edward Lemur1b52d872019-05-09 21:12:12 +00002613 remote_url = self.GetRemoteUrl()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002614 before_push = time_time()
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002615 push_cmd = ['git', 'push', remote_url, refspec]
2616 if git_push_options:
2617 for opt in git_push_options:
2618 push_cmd.extend(['-o', opt])
2619
Edward Lemur0f58ae42019-04-30 17:24:12 +00002620 push_stdout = gclient_utils.CheckCallAndFilter(
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002621 push_cmd,
Edward Lemur0f58ae42019-04-30 17:24:12 +00002622 env=env,
2623 print_stdout=True,
2624 # Flush after every line: useful for seeing progress when running as
2625 # recipe.
2626 filter_fn=lambda _: sys.stdout.flush())
Edward Lemur79d4f992019-11-11 23:49:02 +00002627 push_stdout = push_stdout.decode('utf-8', 'replace')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002628 except subprocess2.CalledProcessError as e:
2629 push_returncode = e.returncode
Aravind Vasudevanc9508582022-10-18 03:07:41 +00002630 if 'blocked keyword' in str(e.stdout) or 'banned word' in str(e.stdout):
Josip Sokcevic740825e2021-05-12 18:28:34 +00002631 raise GitPushError(
2632 'Failed to create a change, very likely due to blocked keyword. '
2633 'Please examine output above for the reason of the failure.\n'
2634 'If this is a false positive, you can try to bypass blocked '
2635 'keyword by using push option '
Aravind Vasudevana9a050c2023-03-10 23:09:55 +00002636 '-o banned-words~skip, e.g.:\n'
2637 'git cl upload -o banned-words~skip\n\n'
Josip Sokcevic740825e2021-05-12 18:28:34 +00002638 'If git-cl is not working correctly, file a bug under the '
2639 'Infra>SDK component.')
Josip Sokcevic54e30e72022-02-10 22:32:24 +00002640 if 'git push -o nokeycheck' in str(e.stdout):
2641 raise GitPushError(
2642 'Failed to create a change, very likely due to a private key being '
2643 'detected. Please examine output above for the reason of the '
2644 'failure.\n'
2645 'If this is a false positive, you can try to bypass private key '
2646 'detection by using push option '
2647 '-o nokeycheck, e.g.:\n'
2648 'git cl upload -o nokeycheck\n\n'
2649 'If git-cl is not working correctly, file a bug under the '
2650 'Infra>SDK component.')
Josip Sokcevic740825e2021-05-12 18:28:34 +00002651
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002652 raise GitPushError(
2653 'Failed to create a change. Please examine output above for the '
2654 'reason of the failure.\n'
Josip Sokcevic7386a1e2021-02-12 19:00:34 +00002655 'For emergencies, Googlers can escalate to '
2656 'go/gob-support or go/notify#gob\n'
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002657 'Hint: run command below to diagnose common Git/Gerrit '
2658 'credential problems:\n'
2659 ' git cl creds-check\n'
2660 '\n'
2661 'If git-cl is not working correctly, file a bug under the Infra>SDK '
2662 'component including the files below.\n'
2663 'Review the files before upload, since they might contain sensitive '
2664 'information.\n'
2665 'Set the Restrict-View-Google label so that they are not publicly '
2666 'accessible.\n' + TRACES_MESSAGE % {'trace_name': trace_name})
Edward Lemur0f58ae42019-04-30 17:24:12 +00002667 finally:
2668 execution_time = time_time() - before_push
2669 metrics.collector.add_repeated('sub_commands', {
2670 'command': 'git push',
2671 'execution_time': execution_time,
2672 'exit_code': push_returncode,
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002673 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
Edward Lemur0f58ae42019-04-30 17:24:12 +00002674 })
2675
Edward Lemur1b52d872019-05-09 21:12:12 +00002676 git_push_metadata['execution_time'] = execution_time
2677 git_push_metadata['exit_code'] = push_returncode
Edward Lemur5737f022019-05-17 01:24:00 +00002678 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002679
Edward Lemur1b52d872019-05-09 21:12:12 +00002680 self._CleanUpOldTraces()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002681 gclient_utils.rmtree(traces_dir)
2682
2683 return push_stdout
2684
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002685 def CMDUploadChange(self, options, git_diff_args, custom_cl_base,
2686 change_desc):
2687 """Upload the current branch to Gerrit, retry if new remote HEAD is
2688 found. options and change_desc may be mutated."""
Josip Sokcevicb631a882021-01-06 18:18:10 +00002689 remote, remote_branch = self.GetRemoteBranch()
2690 branch = GetTargetRef(remote, remote_branch, options.target_branch)
2691
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002692 try:
2693 return self._CMDUploadChange(options, git_diff_args, custom_cl_base,
Josip Sokcevicb631a882021-01-06 18:18:10 +00002694 change_desc, branch)
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002695 except GitPushError as e:
Josip Sokcevicb631a882021-01-06 18:18:10 +00002696 # Repository might be in the middle of transition to main branch as
2697 # default, and uploads to old default might be blocked.
2698 if remote_branch not in [DEFAULT_OLD_BRANCH, DEFAULT_NEW_BRANCH]:
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002699 DieWithError(str(e), change_desc)
2700
Josip Sokcevicb631a882021-01-06 18:18:10 +00002701 project_head = gerrit_util.GetProjectHead(self._gerrit_host,
2702 self.GetGerritProject())
2703 if project_head == branch:
2704 DieWithError(str(e), change_desc)
2705 branch = project_head
2706
2707 print("WARNING: Fetching remote state and retrying upload to default "
2708 "branch...")
2709 RunGit(['fetch', '--prune', remote])
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002710 options.edit_description = False
2711 options.force = True
2712 try:
Josip Sokcevicb631a882021-01-06 18:18:10 +00002713 self._CMDUploadChange(options, git_diff_args, custom_cl_base,
2714 change_desc, branch)
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002715 except GitPushError as e:
2716 DieWithError(str(e), change_desc)
2717
2718 def _CMDUploadChange(self, options, git_diff_args, custom_cl_base,
Josip Sokcevicb631a882021-01-06 18:18:10 +00002719 change_desc, branch):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002720 """Upload the current branch to Gerrit."""
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002721 if options.squash:
Joanna Wangc4ac3022023-01-31 21:19:57 +00002722 Changelist._GerritCommitMsgHookCheck(offer_removal=not options.force)
Gavin Mak4e5e3992022-11-14 22:40:12 +00002723 external_parent = None
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002724 if self.GetIssue():
Josipe827b0f2020-01-30 00:07:20 +00002725 # User requested to change description
2726 if options.edit_description:
Josipe827b0f2020-01-30 00:07:20 +00002727 change_desc.prompt()
Gavin Mak4e5e3992022-11-14 22:40:12 +00002728 change_detail = self._GetChangeDetail(['CURRENT_REVISION'])
2729 change_id = change_detail['change_id']
Edward Lemur5a644f82020-03-18 16:44:57 +00002730 change_desc.ensure_change_id(change_id)
Gavin Mak4e5e3992022-11-14 22:40:12 +00002731
2732 # Check if changes outside of this workspace have been uploaded.
2733 current_rev = change_detail['current_revision']
2734 last_uploaded_rev = self._GitGetBranchConfigValue(
2735 GERRIT_SQUASH_HASH_CONFIG_KEY)
2736 if last_uploaded_rev and current_rev != last_uploaded_rev:
2737 external_parent = self._UpdateWithExternalChanges()
Aaron Gableb56ad332017-01-06 15:24:31 -08002738 else: # if not self.GetIssue()
Gavin Mak68e6cf32021-01-25 18:24:08 +00002739 if not options.force and not options.message_file:
Anthony Polito8b955342019-09-24 19:01:36 +00002740 change_desc.prompt()
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002741 change_ids = git_footers.get_footer_change_id(change_desc.description)
Edward Lemur5a644f82020-03-18 16:44:57 +00002742 if len(change_ids) == 1:
2743 change_id = change_ids[0]
2744 else:
2745 change_id = GenerateGerritChangeId(change_desc.description)
2746 change_desc.ensure_change_id(change_id)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002747
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002748 if options.preserve_tryjobs:
2749 change_desc.set_preserve_tryjobs()
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002750
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002751 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Gavin Mak4e5e3992022-11-14 22:40:12 +00002752 parent = external_parent or self._ComputeParent(
Edward Lemur5a644f82020-03-18 16:44:57 +00002753 remote, upstream_branch, custom_cl_base, options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002754 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Edward Lemur1773f372020-02-22 00:27:14 +00002755 with gclient_utils.temporary_file() as desc_tempfile:
2756 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
2757 ref_to_push = RunGit(
2758 ['commit-tree', tree, '-p', parent, '-F', desc_tempfile]).strip()
Anthony Polito8b955342019-09-24 19:01:36 +00002759 else: # if not options.squash
Gregory Nisbet48d9e1e2021-04-15 23:35:54 +00002760 if options.no_add_changeid:
2761 pass
2762 else: # adding Change-Ids is okay.
2763 if not git_footers.get_footer_change_id(change_desc.description):
2764 DownloadGerritHook(False)
2765 change_desc.set_description(
2766 self._AddChangeIdToCommitMessage(change_desc.description,
2767 git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002768 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002769 # For no-squash mode, we assume the remote called "origin" is the one we
2770 # want. It is not worthwhile to support different workflows for
2771 # no-squash mode.
2772 parent = 'origin/%s' % branch
Gregory Nisbet48d9e1e2021-04-15 23:35:54 +00002773 # attempt to extract the changeid from the current description
2774 # fail informatively if not possible.
2775 change_id_candidates = git_footers.get_footer_change_id(
2776 change_desc.description)
2777 if not change_id_candidates:
2778 DieWithError("Unable to extract change-id from message.")
2779 change_id = change_id_candidates[0]
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002780
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002781 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002782 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2783 ref_to_push)]).splitlines()
2784 if len(commits) > 1:
2785 print('WARNING: This will upload %d commits. Run the following command '
2786 'to see which commits will be uploaded: ' % len(commits))
2787 print('git log %s..%s' % (parent, ref_to_push))
2788 print('You can also use `git squash-branch` to squash these into a '
2789 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002790 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002791
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002792 reviewers = sorted(change_desc.get_reviewers())
Edward Lemur4508b422019-10-03 21:56:35 +00002793 cc = []
Joanna Wangc4ac3022023-01-31 21:19:57 +00002794 # Add default, watchlist, presubmit ccs if this is the initial upload
2795 # and CL is not private and auto-ccing has not been disabled.
2796 if not options.private and not options.no_autocc and not self.GetIssue():
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002797 cc = self.GetCCList().split(',')
Gavin Makb1c08f62021-04-01 18:05:58 +00002798 if len(cc) > 100:
2799 lsc = ('https://chromium.googlesource.com/chromium/src/+/HEAD/docs/'
2800 'process/lsc/lsc_workflow.md')
2801 print('WARNING: This will auto-CC %s users.' % len(cc))
2802 print('LSC may be more appropriate: %s' % lsc)
2803 print('You can also use the --no-autocc flag to disable auto-CC.')
2804 confirm_or_exit(action='continue')
Edward Lemur4508b422019-10-03 21:56:35 +00002805 # Add cc's from the --cc flag.
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002806 if options.cc:
2807 cc.extend(options.cc)
Edward Lemur79d4f992019-11-11 23:49:02 +00002808 cc = [email.strip() for email in cc if email.strip()]
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002809 if change_desc.get_cced():
2810 cc.extend(change_desc.get_cced())
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002811 if self.GetGerritHost() == 'chromium-review.googlesource.com':
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002812 valid_accounts = set(reviewers + cc)
2813 # TODO(crbug/877717): relax this for all hosts.
2814 else:
2815 valid_accounts = gerrit_util.ValidAccounts(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002816 self.GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002817 logging.info('accounts %s are recognized, %s invalid',
2818 sorted(valid_accounts),
2819 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002820
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002821 # Extra options that can be specified at push time. Doc:
2822 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Joanna Wanga1abbed2023-01-24 01:41:05 +00002823 refspec_opts = self._GetRefSpecOptions(options, change_desc)
agablec6787972016-09-09 16:13:34 -07002824
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002825 for r in sorted(reviewers):
2826 if r in valid_accounts:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002827 refspec_opts.append('r=%s' % r)
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002828 reviewers.remove(r)
2829 else:
2830 # TODO(tandrii): this should probably be a hard failure.
2831 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2832 % r)
2833 for c in sorted(cc):
2834 # refspec option will be rejected if cc doesn't correspond to an
2835 # account, even though REST call to add such arbitrary cc may succeed.
2836 if c in valid_accounts:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002837 refspec_opts.append('cc=%s' % c)
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002838 cc.remove(c)
2839
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002840 refspec_suffix = ''
2841 if refspec_opts:
2842 refspec_suffix = '%' + ','.join(refspec_opts)
2843 assert ' ' not in refspec_suffix, (
2844 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2845 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002846
Edward Lemur1b52d872019-05-09 21:12:12 +00002847 git_push_metadata = {
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002848 'gerrit_host': self.GetGerritHost(),
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002849 'title': options.title or '<untitled>',
Edward Lemur1b52d872019-05-09 21:12:12 +00002850 'change_id': change_id,
2851 'description': change_desc.description,
2852 }
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002853
Gavin Mak4e5e3992022-11-14 22:40:12 +00002854 # Gerrit may or may not update fast enough to return the correct patchset
2855 # number after we push. Get the pre-upload patchset and increment later.
2856 latest_ps = self.GetMostRecentPatchset(update=False) or 0
2857
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002858 push_stdout = self._RunGitPushWithTraces(refspec, refspec_opts,
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002859 git_push_metadata,
2860 options.push_options)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002861
2862 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002863 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002864 change_numbers = [m.group(1)
2865 for m in map(regex.match, push_stdout.splitlines())
2866 if m]
2867 if len(change_numbers) != 1:
2868 DieWithError(
2869 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002870 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002871 self.SetIssue(change_numbers[0])
Gavin Mak4e5e3992022-11-14 22:40:12 +00002872 self.SetPatchset(latest_ps + 1)
Gavin Makbe2e9262022-11-08 23:41:55 +00002873 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY, ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002874
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002875 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002876 # GetIssue() is not set in case of non-squash uploads according to tests.
Aaron Gable6e7ddb62020-05-27 22:23:29 +00002877 # TODO(crbug.com/751901): non-squash uploads in git cl should be removed.
Thiago Perrottab0fb8d52022-08-30 21:26:19 +00002878 gerrit_util.AddReviewers(self.GetGerritHost(),
2879 self._GerritChangeIdentifier(),
2880 reviewers,
2881 cc,
2882 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002883
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002884 return 0
2885
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002886 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2887 change_desc):
2888 """Computes parent of the generated commit to be uploaded to Gerrit.
2889
2890 Returns revision or a ref name.
2891 """
2892 if custom_cl_base:
2893 # Try to avoid creating additional unintended CLs when uploading, unless
2894 # user wants to take this risk.
2895 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2896 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2897 local_ref_of_target_remote])
2898 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002899 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002900 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2901 'If you proceed with upload, more than 1 CL may be created by '
2902 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2903 'If you are certain that specified base `%s` has already been '
2904 'uploaded to Gerrit as another CL, you may proceed.\n' %
2905 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2906 if not force:
2907 confirm_or_exit(
2908 'Do you take responsibility for cleaning up potential mess '
2909 'resulting from proceeding with upload?',
2910 action='upload')
2911 return custom_cl_base
2912
Aaron Gablef97e33d2017-03-30 15:44:27 -07002913 if remote != '.':
2914 return self.GetCommonAncestorWithUpstream()
2915
2916 # If our upstream branch is local, we base our squashed commit on its
2917 # squashed version.
2918 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2919
Aaron Gablef97e33d2017-03-30 15:44:27 -07002920 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002921 return self.GetCommonAncestorWithUpstream()
Glen Robertson7d98e222020-08-27 17:53:11 +00002922 if upstream_branch_name == 'main':
2923 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002924
2925 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002926 # TODO(tandrii): consider checking parent change in Gerrit and using its
2927 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2928 # the tree hash of the parent branch. The upside is less likely bogus
2929 # requests to reupload parent change just because it's uploadhash is
2930 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Gavin Makbe2e9262022-11-08 23:41:55 +00002931 parent = scm.GIT.GetBranchConfig(settings.GetRoot(), upstream_branch_name,
2932 GERRIT_SQUASH_HASH_CONFIG_KEY)
Aaron Gablef97e33d2017-03-30 15:44:27 -07002933 # Verify that the upstream branch has been uploaded too, otherwise
2934 # Gerrit will create additional CLs when uploading.
2935 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2936 RunGitSilent(['rev-parse', parent + ':'])):
2937 DieWithError(
2938 '\nUpload upstream branch %s first.\n'
2939 'It is likely that this branch has been rebased since its last '
2940 'upload, so you just need to upload it again.\n'
2941 '(If you uploaded it with --no-squash, then branch dependencies '
2942 'are not supported, and you should reupload with --squash.)'
2943 % upstream_branch_name,
2944 change_desc)
2945 return parent
2946
Gavin Mak4e5e3992022-11-14 22:40:12 +00002947 def _UpdateWithExternalChanges(self):
2948 """Updates workspace with external changes.
2949
2950 Returns the commit hash that should be used as the merge base on upload.
2951 """
2952 local_ps = self.GetPatchset()
2953 if local_ps is None:
2954 return
2955
2956 external_ps = self.GetMostRecentPatchset(update=False)
Gavin Makf35a9eb2022-11-17 18:34:36 +00002957 if external_ps is None or local_ps == external_ps or \
2958 not self._IsPatchsetRangeSignificant(local_ps + 1, external_ps):
Gavin Mak4e5e3992022-11-14 22:40:12 +00002959 return
2960
2961 num_changes = external_ps - local_ps
Gavin Mak6f905472023-01-06 21:01:36 +00002962 if num_changes > 1:
2963 change_words = 'changes were'
2964 else:
2965 change_words = 'change was'
2966 print('\n%d external %s published to %s:\n' %
2967 (num_changes, change_words, self.GetIssueURL(short=True)))
2968
2969 # Print an overview of external changes.
2970 ps_to_commit = {}
2971 ps_to_info = {}
2972 revisions = self._GetChangeDetail(['ALL_REVISIONS'])
2973 for commit_id, revision_info in revisions.get('revisions', {}).items():
2974 ps_num = revision_info['_number']
2975 ps_to_commit[ps_num] = commit_id
2976 ps_to_info[ps_num] = revision_info
2977
2978 for ps in range(external_ps, local_ps, -1):
2979 commit = ps_to_commit[ps][:8]
2980 desc = ps_to_info[ps].get('description', '')
2981 print('Patchset %d [%s] %s' % (ps, commit, desc))
2982
Josip Sokcevic43ceaf02023-05-25 15:56:00 +00002983 print('\nSee diff at: %s/%d..%d' %
2984 (self.GetIssueURL(short=True), local_ps, external_ps))
2985 print('\nUploading without applying patches will override them.')
2986
2987 if not ask_for_explicit_yes('Get the latest changes and apply on top?'):
Gavin Mak4e5e3992022-11-14 22:40:12 +00002988 return
2989
2990 # Get latest Gerrit merge base. Use the first parent even if multiple exist.
2991 external_parent = self._GetChangeCommit(revision=external_ps)['parents'][0]
2992 external_base = external_parent['commit']
2993
2994 branch = git_common.current_branch()
2995 local_base = self.GetCommonAncestorWithUpstream()
2996 if local_base != external_base:
2997 print('\nLocal merge base %s is different from Gerrit %s.\n' %
2998 (local_base, external_base))
2999 if git_common.upstream(branch):
mark a. foltzbcb95772023-05-05 17:28:26 +00003000 confirm_or_exit('Can\'t apply the latest changes from Gerrit.\n'
3001 'Continue with upload and override the latest changes?')
3002 return
3003 print('No upstream branch set. Continuing upload with Gerrit merge base.')
Gavin Mak4e5e3992022-11-14 22:40:12 +00003004
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003005 external_parent_last_uploaded = self._GetChangeCommit(
3006 revision=local_ps)['parents'][0]
3007 external_base_last_uploaded = external_parent_last_uploaded['commit']
3008
3009 if external_base != external_base_last_uploaded:
3010 print('\nPatch set merge bases are different (%s, %s).\n' %
3011 (external_base_last_uploaded, external_base))
3012 confirm_or_exit('Can\'t apply the latest changes from Gerrit.\n'
3013 'Continue with upload and override the latest changes?')
3014 return
3015
Gavin Mak4e5e3992022-11-14 22:40:12 +00003016 # Fetch Gerrit's CL base if it doesn't exist locally.
3017 remote, _ = self.GetRemoteBranch()
3018 if not scm.GIT.IsValidRevision(settings.GetRoot(), external_base):
3019 RunGitSilent(['fetch', remote, external_base])
3020
3021 # Get the diff between local_ps and external_ps.
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003022 print('Fetching changes...')
Gavin Mak4e5e3992022-11-14 22:40:12 +00003023 issue = self.GetIssue()
Gavin Mak591ebaf2022-12-06 18:05:07 +00003024 changes_ref = 'refs/changes/%02d/%d/' % (issue % 100, issue)
Gavin Mak4e5e3992022-11-14 22:40:12 +00003025 RunGitSilent(['fetch', remote, changes_ref + str(local_ps)])
3026 last_uploaded = RunGitSilent(['rev-parse', 'FETCH_HEAD']).strip()
3027 RunGitSilent(['fetch', remote, changes_ref + str(external_ps)])
3028 latest_external = RunGitSilent(['rev-parse', 'FETCH_HEAD']).strip()
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003029
3030 # If the commit parents are different, don't apply the diff as it very
3031 # likely contains many more changes not relevant to this CL.
3032 parents = RunGitSilent(
3033 ['rev-parse',
3034 '%s~1' % (last_uploaded),
3035 '%s~1' % (latest_external)]).strip().split()
3036 assert len(parents) == 2, 'Expected two parents.'
3037 if parents[0] != parents[1]:
3038 confirm_or_exit(
3039 'Can\'t apply the latest changes from Gerrit (parent mismatch '
3040 'between PS).\n'
3041 'Continue with upload and override the latest changes?')
3042 return
3043
Gavin Mak4e5e3992022-11-14 22:40:12 +00003044 diff = RunGitSilent(['diff', '%s..%s' % (last_uploaded, latest_external)])
3045
3046 # Diff can be empty in the case of trivial rebases.
3047 if not diff:
3048 return external_base
3049
3050 # Apply the diff.
3051 with gclient_utils.temporary_file() as diff_tempfile:
3052 gclient_utils.FileWrite(diff_tempfile, diff)
3053 clean_patch = RunGitWithCode(['apply', '--check', diff_tempfile])[0] == 0
3054 RunGitSilent(['apply', '-3', '--intent-to-add', diff_tempfile])
3055 if not clean_patch:
3056 # Normally patchset is set after upload. But because we exit, that never
3057 # happens. Updating here makes sure that subsequent uploads don't need
3058 # to fetch/apply the same diff again.
3059 self.SetPatchset(external_ps)
3060 DieWithError('\nPatch did not apply cleanly. Please resolve any '
3061 'conflicts and reupload.')
3062
3063 message = 'Incorporate external changes from '
3064 if num_changes == 1:
3065 message += 'patchset %d' % external_ps
3066 else:
3067 message += 'patchsets %d to %d' % (local_ps + 1, external_ps)
3068 RunGitSilent(['commit', '-am', message])
3069 # TODO(crbug.com/1382528): Use the previous commit's message as a default
3070 # patchset title instead of this 'Incorporate' message.
3071 return external_base
3072
Edward Lemura12175c2020-03-09 16:58:26 +00003073 def _AddChangeIdToCommitMessage(self, log_desc, args):
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003074 """Re-commits using the current message, assumes the commit hook is in
3075 place.
3076 """
Edward Lemura12175c2020-03-09 16:58:26 +00003077 RunGit(['commit', '--amend', '-m', log_desc])
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00003078 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003079 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003080 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003081 return new_log_desc
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003082
3083 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003084
tandriie113dfd2016-10-11 10:20:12 -07003085 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003086 try:
3087 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003088 except GerritChangeNotExists:
3089 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003090
3091 if data['status'] in ('ABANDONED', 'MERGED'):
3092 return 'CL %s is closed' % self.GetIssue()
3093
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003094 def GetGerritChange(self, patchset=None):
3095 """Returns a buildbucket.v2.GerritChange message for the current issue."""
Edward Lemur79d4f992019-11-11 23:49:02 +00003096 host = urllib.parse.urlparse(self.GetCodereviewServer()).hostname
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003097 issue = self.GetIssue()
Edward Lemur2c210a42019-09-16 23:58:35 +00003098 patchset = int(patchset or self.GetPatchset())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003099 data = self._GetChangeDetail(['ALL_REVISIONS'])
3100
3101 assert host and issue and patchset, 'CL must be uploaded first'
3102
3103 has_patchset = any(
3104 int(revision_data['_number']) == patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003105 for revision_data in data['revisions'].values())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003106 if not has_patchset:
Aaron Gablea45ee112016-11-22 15:14:38 -08003107 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003108 (patchset, self.GetIssue()))
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003109
tandrii8c5a3532016-11-04 07:52:02 -07003110 return {
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003111 'host': host,
3112 'change': issue,
3113 'project': data['project'],
3114 'patchset': patchset,
tandrii8c5a3532016-11-04 07:52:02 -07003115 }
tandriie113dfd2016-10-11 10:20:12 -07003116
tandriide281ae2016-10-12 06:02:30 -07003117 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003118 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003119
Edward Lemur707d70b2018-02-07 00:50:14 +01003120 def GetReviewers(self):
3121 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00003122 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01003123
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003124
Lei Zhang8a0efc12020-08-05 19:58:45 +00003125def _get_bug_line_values(default_project_prefix, bugs):
3126 """Given default_project_prefix and comma separated list of bugs, yields bug
3127 line values.
tandriif9aefb72016-07-01 09:06:51 -07003128
3129 Each bug can be either:
Lei Zhang8a0efc12020-08-05 19:58:45 +00003130 * a number, which is combined with default_project_prefix
tandriif9aefb72016-07-01 09:06:51 -07003131 * string, which is left as is.
3132
3133 This function may produce more than one line, because bugdroid expects one
3134 project per line.
3135
Lei Zhang8a0efc12020-08-05 19:58:45 +00003136 >>> list(_get_bug_line_values('v8:', '123,chromium:789'))
tandriif9aefb72016-07-01 09:06:51 -07003137 ['v8:123', 'chromium:789']
3138 """
3139 default_bugs = []
3140 others = []
3141 for bug in bugs.split(','):
3142 bug = bug.strip()
3143 if bug:
3144 try:
3145 default_bugs.append(int(bug))
3146 except ValueError:
3147 others.append(bug)
3148
3149 if default_bugs:
3150 default_bugs = ','.join(map(str, default_bugs))
Lei Zhang8a0efc12020-08-05 19:58:45 +00003151 if default_project_prefix:
3152 if not default_project_prefix.endswith(':'):
3153 default_project_prefix += ':'
3154 yield '%s%s' % (default_project_prefix, default_bugs)
tandriif9aefb72016-07-01 09:06:51 -07003155 else:
3156 yield default_bugs
3157 for other in sorted(others):
3158 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3159 yield other
3160
3161
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003162class ChangeDescription(object):
3163 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003164 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003165 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003166 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Dan Beamd8b04ca2019-10-10 21:23:26 +00003167 FIXED_LINE = r'^[ \t]*Fixed[ \t]*:[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003168 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003169 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3170 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
Anthony Polito02b5af32019-12-02 19:49:47 +00003171 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):($|[^:])'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003172 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003173
Dan Beamd8b04ca2019-10-10 21:23:26 +00003174 def __init__(self, description, bug=None, fixed=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003175 self._description_lines = (description or '').strip().splitlines()
Anthony Polito8b955342019-09-24 19:01:36 +00003176 if bug:
3177 regexp = re.compile(self.BUG_LINE)
3178 prefix = settings.GetBugPrefix()
3179 if not any((regexp.match(line) for line in self._description_lines)):
3180 values = list(_get_bug_line_values(prefix, bug))
3181 self.append_footer('Bug: %s' % ', '.join(values))
Dan Beamd8b04ca2019-10-10 21:23:26 +00003182 if fixed:
3183 regexp = re.compile(self.FIXED_LINE)
3184 prefix = settings.GetBugPrefix()
3185 if not any((regexp.match(line) for line in self._description_lines)):
3186 values = list(_get_bug_line_values(prefix, fixed))
3187 self.append_footer('Fixed: %s' % ', '.join(values))
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003188
agable@chromium.org42c20792013-09-12 17:34:49 +00003189 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003190 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003191 return '\n'.join(self._description_lines)
3192
3193 def set_description(self, desc):
3194 if isinstance(desc, basestring):
3195 lines = desc.splitlines()
3196 else:
3197 lines = [line.rstrip() for line in desc]
3198 while lines and not lines[0]:
3199 lines.pop(0)
3200 while lines and not lines[-1]:
3201 lines.pop(-1)
3202 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003203
Edward Lemur5a644f82020-03-18 16:44:57 +00003204 def ensure_change_id(self, change_id):
3205 description = self.description
3206 footer_change_ids = git_footers.get_footer_change_id(description)
3207 # Make sure that the Change-Id in the description matches the given one.
3208 if footer_change_ids != [change_id]:
3209 if footer_change_ids:
3210 # Remove any existing Change-Id footers since they don't match the
3211 # expected change_id footer.
3212 description = git_footers.remove_footer(description, 'Change-Id')
3213 print('WARNING: Change-Id has been set to %s. Use `git cl issue 0` '
3214 'if you want to set a new one.')
3215 # Add the expected Change-Id footer.
3216 description = git_footers.add_footer_change_id(description, change_id)
3217 self.set_description(description)
3218
Joanna Wang39811b12023-01-20 23:09:48 +00003219 def update_reviewers(self, reviewers):
3220 """Rewrites the R= line(s) as a single line each.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003221
3222 Args:
3223 reviewers (list(str)) - list of additional emails to use for reviewers.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003224 """
Joanna Wang39811b12023-01-20 23:09:48 +00003225 if not reviewers:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003226 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003227
3228 reviewers = set(reviewers)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003229
Joanna Wang39811b12023-01-20 23:09:48 +00003230 # Get the set of R= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003231 regexp = re.compile(self.R_LINE)
3232 matches = [regexp.match(line) for line in self._description_lines]
3233 new_desc = [l for i, l in enumerate(self._description_lines)
3234 if not matches[i]]
3235 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003236
Joanna Wang39811b12023-01-20 23:09:48 +00003237 # Construct new unified R= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003238
Joanna Wang39811b12023-01-20 23:09:48 +00003239 # First, update reviewers with names from the R= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003240 for match in matches:
3241 if not match:
3242 continue
Joanna Wang39811b12023-01-20 23:09:48 +00003243 reviewers.update(cleanup_list([match.group(2).strip()]))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003244
Joanna Wang39811b12023-01-20 23:09:48 +00003245 new_r_line = 'R=' + ', '.join(sorted(reviewers))
agable@chromium.org42c20792013-09-12 17:34:49 +00003246
3247 # Put the new lines in the description where the old first R= line was.
3248 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3249 if 0 <= line_loc < len(self._description_lines):
Joanna Wang39811b12023-01-20 23:09:48 +00003250 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003251 else:
Joanna Wang39811b12023-01-20 23:09:48 +00003252 self.append_footer(new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003253
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00003254 def set_preserve_tryjobs(self):
3255 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
3256 footers = git_footers.parse_footers(self.description)
3257 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
3258 if v.lower() == 'true':
3259 return
3260 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
3261
Anthony Polito8b955342019-09-24 19:01:36 +00003262 def prompt(self):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003263 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003264 self.set_description([
3265 '# Enter a description of the change.',
3266 '# This will be displayed on the codereview site.',
3267 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003268 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003269 '--------------------',
3270 ] + self._description_lines)
Dan Beamd8b04ca2019-10-10 21:23:26 +00003271 bug_regexp = re.compile(self.BUG_LINE)
3272 fixed_regexp = re.compile(self.FIXED_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00003273 prefix = settings.GetBugPrefix()
Sigurd Schneider8630bb12020-11-11 14:02:49 +00003274 has_issue = lambda l: bug_regexp.match(l) or fixed_regexp.match(l)
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00003275
Dan Beamd8b04ca2019-10-10 21:23:26 +00003276 if not any((has_issue(line) for line in self._description_lines)):
Anthony Polito8b955342019-09-24 19:01:36 +00003277 self.append_footer('Bug: %s' % prefix)
tandriif9aefb72016-07-01 09:06:51 -07003278
Bruce Dawsonfc487042020-10-27 19:11:37 +00003279 print('Waiting for editor...')
agable@chromium.org42c20792013-09-12 17:34:49 +00003280 content = gclient_utils.RunEditor(self.description, True,
Edward Lemur79d4f992019-11-11 23:49:02 +00003281 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003282 if not content:
3283 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003284 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003285
Bruce Dawson2377b012018-01-11 16:46:49 -08003286 # Strip off comments and default inserted "Bug:" line.
3287 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00003288 (line.startswith('#') or
3289 line.rstrip() == "Bug:" or
3290 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00003291 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003292 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003293 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003294
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003295 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003296 """Adds a footer line to the description.
3297
3298 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3299 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3300 that Gerrit footers are always at the end.
3301 """
3302 parsed_footer_line = git_footers.parse_footer(line)
3303 if parsed_footer_line:
3304 # Line is a gerrit footer in the form: Footer-Key: any value.
3305 # Thus, must be appended observing Gerrit footer rules.
3306 self.set_description(
3307 git_footers.add_footer(self.description,
3308 key=parsed_footer_line[0],
3309 value=parsed_footer_line[1]))
3310 return
3311
3312 if not self._description_lines:
3313 self._description_lines.append(line)
3314 return
3315
3316 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3317 if gerrit_footers:
3318 # git_footers.split_footers ensures that there is an empty line before
3319 # actual (gerrit) footers, if any. We have to keep it that way.
3320 assert top_lines and top_lines[-1] == ''
3321 top_lines, separator = top_lines[:-1], top_lines[-1:]
3322 else:
3323 separator = [] # No need for separator if there are no gerrit_footers.
3324
3325 prev_line = top_lines[-1] if top_lines else ''
Josip Sokcevic7958e302023-03-01 23:02:21 +00003326 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3327 not presubmit_support.Change.TAG_LINE_RE.match(line)):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003328 top_lines.append('')
3329 top_lines.append(line)
3330 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003331
tandrii99a72f22016-08-17 14:33:24 -07003332 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003333 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003334 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003335 reviewers = [match.group(2).strip()
3336 for match in matches
3337 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003338 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003339
bradnelsond975b302016-10-23 12:20:23 -07003340 def get_cced(self):
3341 """Retrieves the list of reviewers."""
3342 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3343 cced = [match.group(2).strip() for match in matches if match]
3344 return cleanup_list(cced)
3345
Nodir Turakulov23b82142017-11-16 11:04:25 -08003346 def get_hash_tags(self):
3347 """Extracts and sanitizes a list of Gerrit hashtags."""
3348 subject = (self._description_lines or ('',))[0]
3349 subject = re.sub(
3350 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3351
3352 tags = []
3353 start = 0
3354 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3355 while True:
3356 m = bracket_exp.match(subject, start)
3357 if not m:
3358 break
3359 tags.append(self.sanitize_hash_tag(m.group(1)))
3360 start = m.end()
3361
3362 if not tags:
3363 # Try "Tag: " prefix.
3364 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3365 if m:
3366 tags.append(self.sanitize_hash_tag(m.group(1)))
3367 return tags
3368
3369 @classmethod
3370 def sanitize_hash_tag(cls, tag):
3371 """Returns a sanitized Gerrit hash tag.
3372
3373 A sanitized hashtag can be used as a git push refspec parameter value.
3374 """
3375 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3376
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003377
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003378def FindCodereviewSettingsFile(filename='codereview.settings'):
3379 """Finds the given file starting in the cwd and going up.
3380
3381 Only looks up to the top of the repository unless an
3382 'inherit-review-settings-ok' file exists in the root of the repository.
3383 """
3384 inherit_ok_file = 'inherit-review-settings-ok'
3385 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003386 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003387 if os.path.isfile(os.path.join(root, inherit_ok_file)):
Aleksey Khoroshilov2a229712022-06-02 16:24:11 +00003388 root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003389 while True:
Aleksey Khoroshilov2a229712022-06-02 16:24:11 +00003390 if os.path.isfile(os.path.join(cwd, filename)):
3391 return open(os.path.join(cwd, filename))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003392 if cwd == root:
3393 break
Aleksey Khoroshilov2a229712022-06-02 16:24:11 +00003394 parent_dir = os.path.dirname(cwd)
3395 if parent_dir == cwd:
3396 # We hit the system root directory.
3397 break
3398 cwd = parent_dir
3399 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003400
3401
3402def LoadCodereviewSettingsFromFile(fileobj):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003403 """Parses a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003404 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003405
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003406 def SetProperty(name, setting, unset_error_ok=False):
3407 fullname = 'rietveld.' + name
3408 if setting in keyvals:
3409 RunGit(['config', fullname, keyvals[setting]])
3410 else:
3411 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3412
tandrii48df5812016-10-17 03:55:37 -07003413 if not keyvals.get('GERRIT_HOST', False):
3414 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003415 # Only server setting is required. Other settings can be absent.
3416 # In that case, we ignore errors raised during option deletion attempt.
Joanna Wangc8f23e22023-01-19 21:18:10 +00003417 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003418 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3419 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003420 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003421 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3422 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003423 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3424 unset_error_ok=True)
Jamie Madilldc4d19e2019-10-24 21:50:02 +00003425 SetProperty(
3426 'format-full-by-default', 'FORMAT_FULL_BY_DEFAULT', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003427
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003428 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003429 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003430
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003431 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
Edward Lesmes4de54132020-05-05 19:41:33 +00003432 RunGit(['config', 'gerrit.squash-uploads',
3433 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003434
tandrii@chromium.org28253532016-04-14 13:46:56 +00003435 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003436 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003437 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3438
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003439 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003440 # should be of the form
3441 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3442 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003443 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3444 keyvals['ORIGIN_URL_CONFIG']])
3445
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003446
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003447def urlretrieve(source, destination):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003448 """Downloads a network object to a local file, like urllib.urlretrieve.
3449
3450 This is necessary because urllib is broken for SSL connections via a proxy.
3451 """
Vadim Shtayuraf7b8f8f2021-11-15 19:10:05 +00003452 with open(destination, 'wb') as f:
Edward Lemur79d4f992019-11-11 23:49:02 +00003453 f.write(urllib.request.urlopen(source).read())
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003454
3455
ukai@chromium.org712d6102013-11-27 00:52:58 +00003456def hasSheBang(fname):
3457 """Checks fname is a #! script."""
3458 with open(fname) as f:
3459 return f.read(2).startswith('#!')
3460
3461
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003462def DownloadGerritHook(force):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003463 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003464
3465 Args:
3466 force: True to update hooks. False to install hooks if not present.
3467 """
ukai@chromium.org712d6102013-11-27 00:52:58 +00003468 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003469 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3470 if not os.access(dst, os.X_OK):
3471 if os.path.exists(dst):
3472 if not force:
3473 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003474 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003475 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003476 if not hasSheBang(dst):
3477 DieWithError('Not a script: %s\n'
3478 'You need to download from\n%s\n'
3479 'into .git/hooks/commit-msg and '
3480 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003481 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3482 except Exception:
3483 if os.path.exists(dst):
3484 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003485 DieWithError('\nFailed to download hooks.\n'
3486 'You need to download from\n%s\n'
3487 'into .git/hooks/commit-msg and '
3488 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003489
3490
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003491class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003492 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003493
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003494 def __init__(self):
3495 # Cached list of [host, identity, source], where source is either
3496 # .gitcookies or .netrc.
3497 self._all_hosts = None
3498
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003499 def ensure_configured_gitcookies(self):
3500 """Runs checks and suggests fixes to make git use .gitcookies from default
3501 path."""
3502 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3503 configured_path = RunGitSilent(
3504 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003505 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003506 if configured_path:
3507 self._ensure_default_gitcookies_path(configured_path, default)
3508 else:
3509 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003510
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003511 @staticmethod
3512 def _ensure_default_gitcookies_path(configured_path, default_path):
3513 assert configured_path
3514 if configured_path == default_path:
3515 print('git is already configured to use your .gitcookies from %s' %
3516 configured_path)
3517 return
3518
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003519 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003520 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3521 (configured_path, default_path))
3522
3523 if not os.path.exists(configured_path):
3524 print('However, your configured .gitcookies file is missing.')
3525 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3526 action='reconfigure')
3527 RunGit(['config', '--global', 'http.cookiefile', default_path])
3528 return
3529
3530 if os.path.exists(default_path):
3531 print('WARNING: default .gitcookies file already exists %s' %
3532 default_path)
3533 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3534 default_path)
3535
3536 confirm_or_exit('Move existing .gitcookies to default location?',
3537 action='move')
3538 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003539 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003540 print('Moved and reconfigured git to use .gitcookies from %s' %
3541 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003542
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003543 @staticmethod
3544 def _configure_gitcookies_path(default_path):
3545 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3546 if os.path.exists(netrc_path):
3547 print('You seem to be using outdated .netrc for git credentials: %s' %
3548 netrc_path)
3549 print('This tool will guide you through setting up recommended '
3550 '.gitcookies store for git credentials.\n'
3551 '\n'
3552 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3553 ' git config --global --unset http.cookiefile\n'
3554 ' mv %s %s.backup\n\n' % (default_path, default_path))
3555 confirm_or_exit(action='setup .gitcookies')
3556 RunGit(['config', '--global', 'http.cookiefile', default_path])
3557 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003558
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003559 def get_hosts_with_creds(self, include_netrc=False):
3560 if self._all_hosts is None:
3561 a = gerrit_util.CookiesAuthenticator()
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003562 self._all_hosts = [(h, u, s) for h, u, s in itertools.chain((
3563 (h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.items()), (
3564 (h, u, '.gitcookies') for h, (u, _) in a.gitcookies.items()))
3565 if h.endswith(_GOOGLESOURCE)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003566
3567 if include_netrc:
3568 return self._all_hosts
3569 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3570
3571 def print_current_creds(self, include_netrc=False):
3572 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3573 if not hosts:
3574 print('No Git/Gerrit credentials found')
3575 return
Edward Lemur79d4f992019-11-11 23:49:02 +00003576 lengths = [max(map(len, (row[i] for row in hosts))) for i in range(3)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003577 header = [('Host', 'User', 'Which file'),
3578 ['=' * l for l in lengths]]
3579 for row in (header + hosts):
3580 print('\t'.join((('%%+%ds' % l) % s)
3581 for l, s in zip(lengths, row)))
3582
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003583 @staticmethod
3584 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003585 """Parses identity "git-<username>.domain" into <username> and domain."""
3586 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003587 # distinguishable from sub-domains. But we do know typical domains:
3588 if identity.endswith('.chromium.org'):
3589 domain = 'chromium.org'
3590 username = identity[:-len('.chromium.org')]
3591 else:
3592 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003593 if username.startswith('git-'):
3594 username = username[len('git-'):]
3595 return username, domain
3596
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003597 def has_generic_host(self):
3598 """Returns whether generic .googlesource.com has been configured.
3599
3600 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3601 """
3602 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003603 if host == '.' + _GOOGLESOURCE:
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003604 return True
3605 return False
3606
3607 def _get_git_gerrit_identity_pairs(self):
3608 """Returns map from canonic host to pair of identities (Git, Gerrit).
3609
3610 One of identities might be None, meaning not configured.
3611 """
3612 host_to_identity_pairs = {}
3613 for host, identity, _ in self.get_hosts_with_creds():
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003614 canonical = _canonical_git_googlesource_host(host)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003615 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3616 idx = 0 if canonical == host else 1
3617 pair[idx] = identity
3618 return host_to_identity_pairs
3619
3620 def get_partially_configured_hosts(self):
3621 return set(
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003622 (host if i1 else _canonical_gerrit_googlesource_host(host))
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003623 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003624 if None in (i1, i2) and host != '.' + _GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003625
3626 def get_conflicting_hosts(self):
3627 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003628 host
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003629 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003630 if None not in (i1, i2) and i1 != i2)
3631
3632 def get_duplicated_hosts(self):
3633 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003634 return set(host for host, count in counters.items() if count > 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003635
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003636
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003637 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003638 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003639 hosts = sorted(hosts)
3640 assert hosts
3641 if extra_column_func is None:
3642 extras = [''] * len(hosts)
3643 else:
3644 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003645 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3646 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003647 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003648 lines.append(tmpl % he)
3649 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003650
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003651 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003652 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003653 yield ('.googlesource.com wildcard record detected',
3654 ['Chrome Infrastructure team recommends to list full host names '
3655 'explicitly.'],
3656 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003657
3658 dups = self.get_duplicated_hosts()
3659 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003660 yield ('The following hosts were defined twice',
3661 self._format_hosts(dups),
3662 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003663
3664 partial = self.get_partially_configured_hosts()
3665 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003666 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3667 'These hosts are missing',
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003668 self._format_hosts(
3669 partial, lambda host: 'but %s defined' % _get_counterpart_host(
3670 host)), partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003671
3672 conflicting = self.get_conflicting_hosts()
3673 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003674 yield ('The following Git hosts have differing credentials from their '
3675 'Gerrit counterparts',
3676 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3677 tuple(self._get_git_gerrit_identity_pairs()[host])),
3678 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003679
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003680 def find_and_report_problems(self):
3681 """Returns True if there was at least one problem, else False."""
3682 found = False
3683 bad_hosts = set()
3684 for title, sublines, hosts in self._find_problems():
3685 if not found:
3686 found = True
3687 print('\n\n.gitcookies problem report:\n')
3688 bad_hosts.update(hosts or [])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003689 print(' %s%s' % (title, (':' if sublines else '')))
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003690 if sublines:
3691 print()
3692 print(' %s' % '\n '.join(sublines))
3693 print()
3694
3695 if bad_hosts:
3696 assert found
3697 print(' You can manually remove corresponding lines in your %s file and '
3698 'visit the following URLs with correct account to generate '
3699 'correct credential lines:\n' %
3700 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003701 print(' %s' % '\n '.join(
3702 sorted(
3703 set(gerrit_util.CookiesAuthenticator().get_new_password_url(
3704 _canonical_git_googlesource_host(host))
3705 for host in bad_hosts))))
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003706 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003707
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003708
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003709@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003710def CMDcreds_check(parser, args):
3711 """Checks credentials and suggests changes."""
3712 _, _ = parser.parse_args(args)
3713
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003714 # Code below checks .gitcookies. Abort if using something else.
3715 authn = gerrit_util.Authenticator.get()
3716 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
Edward Lemur57d47422020-03-06 20:43:07 +00003717 message = (
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003718 'This command is not designed for bot environment. It checks '
3719 '~/.gitcookies file not generally used on bots.')
Edward Lemur57d47422020-03-06 20:43:07 +00003720 # TODO(crbug.com/1059384): Automatically detect when running on cloudtop.
3721 if isinstance(authn, gerrit_util.GceAuthenticator):
3722 message += (
3723 '\n'
3724 'If you need to run this on GCE or a cloudtop instance, '
3725 'export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
3726 DieWithError(message)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003727
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003728 checker = _GitCookiesChecker()
3729 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003730
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003731 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003732 checker.print_current_creds(include_netrc=True)
3733
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003734 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003735 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003736 return 0
3737 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003738
3739
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003740@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003741def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003742 """Gets or sets base-url for this branch."""
Thiago Perrotta16d08f02022-07-20 18:18:50 +00003743 _, args = parser.parse_args(args)
Edward Lesmes50da7702020-03-30 19:23:43 +00003744 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
Edward Lemur85153282020-02-14 22:06:29 +00003745 branch = scm.GIT.ShortBranchName(branchref)
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003746 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003747 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003748 return RunGit(['config', 'branch.%s.base-url' % branch],
3749 error_ok=False).strip()
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003750
3751 print('Setting base-url to %s' % args[0])
3752 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3753 error_ok=False).strip()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003754
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003755
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003756def color_for_status(status):
3757 """Maps a Changelist status to color, for CMDstatus and other tools."""
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00003758 BOLD = '\033[1m'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003759 return {
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00003760 'unsent': BOLD + Fore.YELLOW,
3761 'waiting': BOLD + Fore.RED,
3762 'reply': BOLD + Fore.YELLOW,
3763 'not lgtm': BOLD + Fore.RED,
3764 'lgtm': BOLD + Fore.GREEN,
3765 'commit': BOLD + Fore.MAGENTA,
3766 'closed': BOLD + Fore.CYAN,
3767 'error': BOLD + Fore.WHITE,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003768 }.get(status, Fore.WHITE)
3769
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003770
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003771def get_cl_statuses(changes, fine_grained, max_processes=None):
3772 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003773
3774 If fine_grained is true, this will fetch CL statuses from the server.
3775 Otherwise, simply indicate if there's a matching url for the given branches.
3776
3777 If max_processes is specified, it is used as the maximum number of processes
3778 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3779 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003780
3781 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003782 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003783 if not changes:
Edward Lemur61bf4172020-02-24 23:22:37 +00003784 return
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003785
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003786 if not fine_grained:
3787 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003788 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003789 for cl in changes:
3790 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003791 return
3792
3793 # First, sort out authentication issues.
3794 logging.debug('ensuring credentials exist')
3795 for cl in changes:
3796 cl.EnsureAuthenticated(force=False, refresh=True)
3797
3798 def fetch(cl):
3799 try:
3800 return (cl, cl.GetStatus())
3801 except:
3802 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003803 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003804 raise
3805
3806 threads_count = len(changes)
3807 if max_processes:
3808 threads_count = max(1, min(threads_count, max_processes))
3809 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3810
Edward Lemur61bf4172020-02-24 23:22:37 +00003811 pool = multiprocessing.pool.ThreadPool(threads_count)
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003812 fetched_cls = set()
3813 try:
3814 it = pool.imap_unordered(fetch, changes).__iter__()
3815 while True:
3816 try:
3817 cl, status = it.next(timeout=5)
Edward Lemur61bf4172020-02-24 23:22:37 +00003818 except (multiprocessing.TimeoutError, StopIteration):
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003819 break
3820 fetched_cls.add(cl)
3821 yield cl, status
3822 finally:
3823 pool.close()
3824
3825 # Add any branches that failed to fetch.
3826 for cl in set(changes) - fetched_cls:
3827 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003828
rmistry@google.com2dd99862015-06-22 12:22:18 +00003829
Jose Lopes3863fc52020-04-07 17:00:25 +00003830def upload_branch_deps(cl, args, force=False):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003831 """Uploads CLs of local branches that are dependents of the current branch.
3832
3833 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003834
3835 test1 -> test2.1 -> test3.1
3836 -> test3.2
3837 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003838
3839 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3840 run on the dependent branches in this order:
3841 test2.1, test3.1, test3.2, test2.2, test3.3
3842
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003843 Note: This function does not rebase your local dependent branches. Use it
3844 when you make a change to the parent branch that will not conflict
3845 with its dependent branches, and you would like their dependencies
3846 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003847 """
3848 if git_common.is_dirty_git_tree('upload-branch-deps'):
3849 return 1
3850
3851 root_branch = cl.GetBranch()
3852 if root_branch is None:
3853 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3854 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003855 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003856 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3857 'patchset dependencies without an uploaded CL.')
3858
3859 branches = RunGit(['for-each-ref',
3860 '--format=%(refname:short) %(upstream:short)',
3861 'refs/heads'])
3862 if not branches:
3863 print('No local branches found.')
3864 return 0
3865
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003866 # Create a dictionary of all local branches to the branches that are
3867 # dependent on it.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003868 tracked_to_dependents = collections.defaultdict(list)
3869 for b in branches.splitlines():
3870 tokens = b.split()
3871 if len(tokens) == 2:
3872 branch_name, tracked = tokens
3873 tracked_to_dependents[tracked].append(branch_name)
3874
vapiera7fbd5a2016-06-16 09:17:49 -07003875 print()
3876 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003877 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003878
rmistry@google.com2dd99862015-06-22 12:22:18 +00003879 def traverse_dependents_preorder(branch, padding=''):
3880 dependents_to_process = tracked_to_dependents.get(branch, [])
3881 padding += ' '
3882 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003883 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003884 dependents.append(dependent)
3885 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003886
rmistry@google.com2dd99862015-06-22 12:22:18 +00003887 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003888 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003889
3890 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003891 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003892 return 0
3893
rmistry@google.com2dd99862015-06-22 12:22:18 +00003894 # Record all dependents that failed to upload.
3895 failures = {}
3896 # Go through all dependents, checkout the branch and upload.
3897 try:
3898 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003899 print()
3900 print('--------------------------------------')
3901 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003902 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003903 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003904 try:
3905 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003906 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003907 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003908 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003909 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003910 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003911 finally:
3912 # Swap back to the original root branch.
3913 RunGit(['checkout', '-q', root_branch])
3914
vapiera7fbd5a2016-06-16 09:17:49 -07003915 print()
3916 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003917 for dependent_branch in dependents:
3918 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003919 print(' %s : %s' % (dependent_branch, upload_status))
3920 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003921
3922 return 0
3923
3924
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003925def GetArchiveTagForBranch(issue_num, branch_name, existing_tags, pattern):
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003926 """Given a proposed tag name, returns a tag name that is guaranteed to be
3927 unique. If 'foo' is proposed but already exists, then 'foo-2' is used,
3928 or 'foo-3', and so on."""
3929
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003930 proposed_tag = pattern.format(**{'issue': issue_num, 'branch': branch_name})
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003931 for suffix_num in itertools.count(1):
3932 if suffix_num == 1:
3933 to_check = proposed_tag
3934 else:
3935 to_check = '%s-%d' % (proposed_tag, suffix_num)
3936
3937 if to_check not in existing_tags:
3938 return to_check
3939
3940
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003941@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003942def CMDarchive(parser, args):
3943 """Archives and deletes branches associated with closed changelists."""
3944 parser.add_option(
3945 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003946 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003947 parser.add_option(
3948 '-f', '--force', action='store_true',
3949 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003950 parser.add_option(
3951 '-d', '--dry-run', action='store_true',
3952 help='Skip the branch tagging and removal steps.')
3953 parser.add_option(
3954 '-t', '--notags', action='store_true',
3955 help='Do not tag archived branches. '
3956 'Note: local commit history may be lost.')
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003957 parser.add_option(
3958 '-p',
3959 '--pattern',
3960 default='git-cl-archived-{issue}-{branch}',
3961 help='Format string for archive tags. '
3962 'E.g. \'archived-{issue}-{branch}\'.')
kmarshall3bff56b2016-06-06 18:31:47 -07003963
kmarshall3bff56b2016-06-06 18:31:47 -07003964 options, args = parser.parse_args(args)
3965 if args:
3966 parser.error('Unsupported args: %s' % ' '.join(args))
kmarshall3bff56b2016-06-06 18:31:47 -07003967
3968 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3969 if not branches:
3970 return 0
3971
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003972 tags = RunGit(['for-each-ref', '--format=%(refname)',
3973 'refs/tags']).splitlines() or []
3974 tags = [t.split('/')[-1] for t in tags]
3975
vapiera7fbd5a2016-06-16 09:17:49 -07003976 print('Finding all branches associated with closed issues...')
Edward Lemur934836a2019-09-09 20:16:54 +00003977 changes = [Changelist(branchref=b)
3978 for b in branches.splitlines()]
kmarshall3bff56b2016-06-06 18:31:47 -07003979 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3980 statuses = get_cl_statuses(changes,
3981 fine_grained=True,
3982 max_processes=options.maxjobs)
3983 proposal = [(cl.GetBranch(),
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003984 GetArchiveTagForBranch(cl.GetIssue(), cl.GetBranch(), tags,
3985 options.pattern))
kmarshall3bff56b2016-06-06 18:31:47 -07003986 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00003987 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07003988 proposal.sort()
3989
3990 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003991 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003992 return 0
3993
Edward Lemur85153282020-02-14 22:06:29 +00003994 current_branch = scm.GIT.GetBranch(settings.GetRoot())
kmarshall3bff56b2016-06-06 18:31:47 -07003995
vapiera7fbd5a2016-06-16 09:17:49 -07003996 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003997 if options.notags:
3998 for next_item in proposal:
3999 print(' ' + next_item[0])
4000 else:
4001 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4002 for next_item in proposal:
4003 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004004
kmarshall9249e012016-08-23 12:02:16 -07004005 # Quit now on precondition failure or if instructed by the user, either
4006 # via an interactive prompt or by command line flags.
4007 if options.dry_run:
4008 print('\nNo changes were made (dry run).\n')
4009 return 0
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00004010
4011 if any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004012 print('You are currently on a branch \'%s\' which is associated with a '
4013 'closed codereview issue, so archive cannot proceed. Please '
4014 'checkout another branch and run this command again.' %
4015 current_branch)
4016 return 1
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00004017
4018 if not options.force:
Edward Lesmesae3586b2020-03-23 21:21:14 +00004019 answer = gclient_utils.AskForData('\nProceed with deletion (Y/n)? ').lower()
sergiyb4a5ecbe2016-06-20 09:46:00 -07004020 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004021 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004022 return 1
4023
4024 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004025 if not options.notags:
4026 RunGit(['tag', tagname, branch])
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00004027
4028 if RunGitWithCode(['branch', '-D', branch])[0] != 0:
4029 # Clean up the tag if we failed to delete the branch.
4030 RunGit(['tag', '-d', tagname])
kmarshall9249e012016-08-23 12:02:16 -07004031
vapiera7fbd5a2016-06-16 09:17:49 -07004032 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004033
4034 return 0
4035
4036
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004037@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004038def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004039 """Show status of changelists.
4040
4041 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004042 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004043 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004044 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004045 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004046 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004047 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004048 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004049
4050 Also see 'git cl comments'.
4051 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00004052 parser.add_option(
4053 '--no-branch-color',
4054 action='store_true',
4055 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004056 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004057 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004058 parser.add_option('-f', '--fast', action='store_true',
4059 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004060 parser.add_option(
4061 '-j', '--maxjobs', action='store', type=int,
4062 help='The maximum number of jobs to use when retrieving review status')
Edward Lemur52969c92020-02-06 18:15:28 +00004063 parser.add_option(
4064 '-i', '--issue', type=int,
4065 help='Operate on this issue instead of the current branch\'s implicit '
4066 'issue. Requires --field to be set.')
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00004067 parser.add_option('-d',
4068 '--date-order',
4069 action='store_true',
4070 help='Order branches by committer date.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004071 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004072 if args:
4073 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004074
iannuccie53c9352016-08-17 14:40:40 -07004075 if options.issue is not None and not options.field:
Edward Lemur6c6827c2020-02-06 21:15:18 +00004076 parser.error('--field must be given when --issue is set.')
iannucci3c972b92016-08-17 13:24:10 -07004077
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004078 if options.field:
Edward Lemur934836a2019-09-09 20:16:54 +00004079 cl = Changelist(issue=options.issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004080 if options.field.startswith('desc'):
Edward Lemur6c6827c2020-02-06 21:15:18 +00004081 if cl.GetIssue():
4082 print(cl.FetchDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004083 elif options.field == 'id':
4084 issueid = cl.GetIssue()
4085 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004086 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004087 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004088 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004089 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004090 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004091 elif options.field == 'status':
4092 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004093 elif options.field == 'url':
4094 url = cl.GetIssueURL()
4095 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004096 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004097 return 0
4098
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00004099 branches = RunGit([
4100 'for-each-ref', '--format=%(refname) %(committerdate:unix)', 'refs/heads'
4101 ])
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004102 if not branches:
4103 print('No local branch found.')
4104 return 0
4105
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004106 changes = [
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00004107 Changelist(branchref=b, commit_date=ct)
4108 for b, ct in map(lambda line: line.split(' '), branches.splitlines())
4109 ]
vapiera7fbd5a2016-06-16 09:17:49 -07004110 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004111 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004112 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004113 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004114
Edward Lemur85153282020-02-14 22:06:29 +00004115 current_branch = scm.GIT.GetBranch(settings.GetRoot())
Daniel McArdlea23bf592019-02-12 00:25:12 +00004116
4117 def FormatBranchName(branch, colorize=False):
4118 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
4119 an asterisk when it is the current branch."""
4120
4121 asterisk = ""
4122 color = Fore.RESET
4123 if branch == current_branch:
4124 asterisk = "* "
4125 color = Fore.GREEN
Edward Lemur85153282020-02-14 22:06:29 +00004126 branch_name = scm.GIT.ShortBranchName(branch)
Daniel McArdlea23bf592019-02-12 00:25:12 +00004127
4128 if colorize:
4129 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00004130 return asterisk + branch_name
4131
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004132 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00004133
4134 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +00004135
4136 if options.date_order or settings.IsStatusCommitOrderByDate():
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00004137 sorted_changes = sorted(changes,
4138 key=lambda c: c.GetCommitDate(),
4139 reverse=True)
4140 else:
4141 sorted_changes = sorted(changes, key=lambda c: c.GetBranch())
4142 for cl in sorted_changes:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004143 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004144 while branch not in branch_statuses:
Edward Lemur79d4f992019-11-11 23:49:02 +00004145 c, status = next(output)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004146 branch_statuses[c.GetBranch()] = status
4147 status = branch_statuses.pop(branch)
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00004148 url = cl.GetIssueURL(short=True)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004149 if url and (not status or status == 'error'):
4150 # The issue probably doesn't exist anymore.
4151 url += ' (broken)'
4152
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004153 color = color_for_status(status)
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00004154 # Turn off bold as well as colors.
4155 END = '\033[0m'
4156 reset = Fore.RESET + END
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004157 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004158 color = ''
4159 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004160 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00004161
Alan Cuttera3be9a52019-03-04 18:50:33 +00004162 branch_display = FormatBranchName(branch)
4163 padding = ' ' * (alignment - len(branch_display))
4164 if not options.no_branch_color:
4165 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00004166
Alan Cuttera3be9a52019-03-04 18:50:33 +00004167 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
4168 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004169
vapiera7fbd5a2016-06-16 09:17:49 -07004170 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00004171 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004172 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00004173 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004174 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004175 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004176 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004177 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004178 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004179 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004180 print('Issue description:')
Edward Lemur6c6827c2020-02-06 21:15:18 +00004181 print(cl.FetchDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004182 return 0
4183
4184
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004185def colorize_CMDstatus_doc():
4186 """To be called once in main() to add colors to git cl status help."""
4187 colors = [i for i in dir(Fore) if i[0].isupper()]
4188
4189 def colorize_line(line):
4190 for color in colors:
4191 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004192 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004193 indent = len(line) - len(line.lstrip(' ')) + 1
4194 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4195 return line
4196
4197 lines = CMDstatus.__doc__.splitlines()
4198 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4199
4200
phajdan.jre328cf92016-08-22 04:12:17 -07004201def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004202 if path == '-':
4203 json.dump(contents, sys.stdout)
4204 else:
4205 with open(path, 'w') as f:
4206 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004207
4208
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004209@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004210@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004211def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004212 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004213
4214 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004215 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004216 parser.add_option('-r', '--reverse', action='store_true',
4217 help='Lookup the branch(es) for the specified issues. If '
4218 'no issues are specified, all branches with mapped '
4219 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004220 parser.add_option('--json',
4221 help='Path to JSON output file, or "-" for stdout.')
dnj@chromium.org406c4402015-03-03 17:22:28 +00004222 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004223
dnj@chromium.org406c4402015-03-03 17:22:28 +00004224 if options.reverse:
Arthur Milchior801a9752023-04-07 10:33:54 +00004225 branches = RunGit(['for-each-ref', 'refs/heads',
4226 '--format=%(refname)']).splitlines()
4227 # Reverse issue lookup.
4228 issue_branch_map = {}
4229
4230 git_config = {}
4231 for config in RunGit(['config', '--get-regexp',
4232 r'branch\..*issue']).splitlines():
4233 name, _space, val = config.partition(' ')
4234 git_config[name] = val
4235
4236 for branch in branches:
4237 issue = git_config.get(
4238 'branch.%s.%s' % (scm.GIT.ShortBranchName(branch), ISSUE_CONFIG_KEY))
4239 if issue:
4240 issue_branch_map.setdefault(int(issue), []).append(branch)
4241 if not args:
4242 args = sorted(issue_branch_map.keys())
4243 result = {}
4244 for issue in args:
4245 try:
4246 issue_num = int(issue)
4247 except ValueError:
4248 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
4249 continue
4250 result[issue_num] = issue_branch_map.get(issue_num)
4251 print('Branch for issue number %s: %s' %
4252 (issue, ', '.join(issue_branch_map.get(issue_num) or ('None', ))))
4253 if options.json:
4254 write_json(options.json, result)
4255 return 0
Aaron Gable78753da2017-06-15 10:35:49 -07004256
4257 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00004258 issue = ParseIssueNumberArgument(args[0])
Aaron Gable78753da2017-06-15 10:35:49 -07004259 if not issue.valid:
4260 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4261 'or no argument to list it.\n'
4262 'Maybe you want to run git cl status?')
Edward Lemurf38bc172019-09-03 21:02:13 +00004263 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07004264 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004265 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00004266 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07004267 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4268 if options.json:
4269 write_json(options.json, {
Nodir Turakulov27379632021-03-17 18:53:29 +00004270 'gerrit_host': cl.GetGerritHost(),
4271 'gerrit_project': cl.GetGerritProject(),
Aaron Gable78753da2017-06-15 10:35:49 -07004272 'issue_url': cl.GetIssueURL(),
Nodir Turakulov27379632021-03-17 18:53:29 +00004273 'issue': cl.GetIssue(),
Aaron Gable78753da2017-06-15 10:35:49 -07004274 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004275 return 0
4276
4277
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004278@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004279def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004280 """Shows or posts review comments for any changelist."""
4281 parser.add_option('-a', '--add-comment', dest='comment',
4282 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004283 parser.add_option('-p', '--publish', action='store_true',
4284 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004285 parser.add_option('-i', '--issue', dest='issue',
Edward Lemurf38bc172019-09-03 21:02:13 +00004286 help='review issue id (defaults to current issue).')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004287 parser.add_option('-m', '--machine-readable', dest='readable',
4288 action='store_false', default=True,
4289 help='output comments in a format compatible with '
4290 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004291 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004292 help='File to write JSON summary to, or "-" for stdout')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004293 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004294
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004295 issue = None
4296 if options.issue:
4297 try:
4298 issue = int(options.issue)
4299 except ValueError:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004300 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004301
Edward Lemur934836a2019-09-09 20:16:54 +00004302 cl = Changelist(issue=issue)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004303
4304 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004305 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004306 return 0
4307
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004308 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4309 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004310 for comment in summary:
4311 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004312 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004313 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004314 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004315 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004316 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00004317 elif comment.autogenerated:
4318 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004319 else:
4320 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004321 print('\n%s%s %s%s\n%s' % (
4322 color,
4323 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4324 comment.sender,
4325 Fore.RESET,
4326 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4327
smut@google.comc85ac942015-09-15 16:34:43 +00004328 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004329 def pre_serialize(c):
Edward Lemur79d4f992019-11-11 23:49:02 +00004330 dct = c._asdict().copy()
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004331 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4332 return dct
Edward Lemur79d4f992019-11-11 23:49:02 +00004333 write_json(options.json_file, [pre_serialize(x) for x in summary])
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004334 return 0
4335
4336
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004337@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004338@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004339def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004340 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004341 parser.add_option('-d', '--display', action='store_true',
4342 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004343 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004344 help='New description to set for this issue (- for stdin, '
4345 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004346 parser.add_option('-f', '--force', action='store_true',
4347 help='Delete any unpublished Gerrit edits for this issue '
4348 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004349
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004350 options, args = parser.parse_args(args)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004351
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004352 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004353 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00004354 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004355 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004356 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004357
Edward Lemur934836a2019-09-09 20:16:54 +00004358 kwargs = {}
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004359 if target_issue_arg:
4360 kwargs['issue'] = target_issue_arg.issue
4361 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07004362
4363 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004364 if not cl.GetIssue():
4365 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004366
Edward Lemur678a6842019-10-03 22:25:05 +00004367 if args and not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00004368 logging.info('canonical issue/change URL: %s\n', cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004369
Edward Lemur6c6827c2020-02-06 21:15:18 +00004370 description = ChangeDescription(cl.FetchDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004371
smut@google.com34fb6b12015-07-13 20:03:26 +00004372 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004373 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004374 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004375
4376 if options.new_description:
4377 text = options.new_description
4378 if text == '-':
4379 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004380 elif text == '+':
4381 base_branch = cl.GetCommonAncestorWithUpstream()
Edward Lemura12175c2020-03-09 16:58:26 +00004382 text = _create_description_from_log([base_branch])
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004383
4384 description.set_description(text)
4385 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00004386 description.prompt()
Edward Lemur6c6827c2020-02-06 21:15:18 +00004387 if cl.FetchDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004388 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004389 return 0
4390
4391
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004392@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004393def CMDlint(parser, args):
4394 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004395 parser.add_option('--filter', action='append', metavar='-x,+y',
4396 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004397 options, args = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004398
4399 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004400 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004401 try:
4402 import cpplint
4403 import cpplint_chromium
4404 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004405 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004406 return 1
4407
4408 # Change the current working directory before calling lint so that it
4409 # shows the correct base.
4410 previous_cwd = os.getcwd()
4411 os.chdir(settings.GetRoot())
4412 try:
Edward Lemur934836a2019-09-09 20:16:54 +00004413 cl = Changelist()
Edward Lemur2c62b332020-03-12 22:12:33 +00004414 files = cl.GetAffectedFiles(cl.GetCommonAncestorWithUpstream())
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004415 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004416 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004417 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004418
Lei Zhangb8c62cf2020-07-15 20:09:37 +00004419 # Process cpplint arguments, if any.
4420 filters = presubmit_canned_checks.GetCppLintFilters(options.filter)
4421 command = ['--filter=' + ','.join(filters)] + args + files
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004422 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004423
Lei Zhang379d1ad2020-07-15 19:40:06 +00004424 include_regex = re.compile(settings.GetLintRegex())
4425 ignore_regex = re.compile(settings.GetLintIgnoreRegex())
thestig@chromium.org44202a22014-03-11 19:22:18 +00004426 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4427 for filename in filenames:
Lei Zhang379d1ad2020-07-15 19:40:06 +00004428 if not include_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004429 print('Skipping file %s' % filename)
Lei Zhang379d1ad2020-07-15 19:40:06 +00004430 continue
4431
4432 if ignore_regex.match(filename):
4433 print('Ignoring file %s' % filename)
4434 continue
4435
4436 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4437 extra_check_functions)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004438 finally:
4439 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004440 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004441 if cpplint._cpplint_state.error_count != 0:
4442 return 1
4443 return 0
4444
4445
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004446@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004447def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004448 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004449 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004450 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004451 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004452 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004453 parser.add_option('--all', action='store_true',
4454 help='Run checks against all files, not just modified ones')
Josip Sokcevic017544d2022-03-31 23:47:53 +00004455 parser.add_option('--files',
4456 nargs=1,
4457 help='Semicolon-separated list of files to be marked as '
4458 'modified when executing presubmit or post-upload hooks. '
4459 'fnmatch wildcards can also be used.')
Edward Lesmes8e282792018-04-03 18:50:29 -04004460 parser.add_option('--parallel', action='store_true',
4461 help='Run all tests specified by input_api.RunTests in all '
4462 'PRESUBMIT files in parallel.')
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00004463 parser.add_option('--resultdb', action='store_true',
4464 help='Run presubmit checks in the ResultSink environment '
4465 'and send results to the ResultDB database.')
Saagar Sanghavi03b15132020-08-10 16:43:41 +00004466 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004467 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004468
sbc@chromium.org71437c02015-04-09 19:29:40 +00004469 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004470 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004471 return 1
4472
Edward Lemur934836a2019-09-09 20:16:54 +00004473 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004474 if args:
4475 base_branch = args[0]
4476 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004477 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004478 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004479
Bruce Dawsoneb8426e2022-08-05 23:58:15 +00004480 start = time.time()
4481 try:
4482 if not 'PRESUBMIT_SKIP_NETWORK' in os.environ and cl.GetIssue():
4483 description = cl.FetchDescription()
4484 else:
4485 description = _create_description_from_log([base_branch])
4486 except Exception as e:
4487 print('Failed to fetch CL description - %s' % str(e))
Edward Lemura12175c2020-03-09 16:58:26 +00004488 description = _create_description_from_log([base_branch])
Bruce Dawsoneb8426e2022-08-05 23:58:15 +00004489 elapsed = time.time() - start
4490 if elapsed > 5:
4491 print('%.1f s to get CL description.' % elapsed)
Aaron Gable8076c282017-11-29 14:39:41 -08004492
Bruce Dawson13acea32022-05-03 22:13:08 +00004493 if not base_branch:
4494 if not options.force:
4495 print('use --force to check even when not on a branch.')
4496 return 1
4497 base_branch = 'HEAD'
4498
Josip Sokcevic017544d2022-03-31 23:47:53 +00004499 cl.RunHook(committing=not options.upload,
4500 may_prompt=False,
4501 verbose=options.verbose,
4502 parallel=options.parallel,
4503 upstream=base_branch,
4504 description=description,
4505 all_files=options.all,
4506 files=options.files,
4507 resultdb=options.resultdb,
4508 realm=options.realm)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004509 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004510
4511
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004512def GenerateGerritChangeId(message):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004513 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004514
4515 Works the same way as
4516 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4517 but can be called on demand on all platforms.
4518
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004519 The basic idea is to generate git hash of a state of the tree, original
4520 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004521 """
4522 lines = []
4523 tree_hash = RunGitSilent(['write-tree'])
4524 lines.append('tree %s' % tree_hash.strip())
4525 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4526 if code == 0:
4527 lines.append('parent %s' % parent.strip())
4528 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4529 lines.append('author %s' % author.strip())
4530 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4531 lines.append('committer %s' % committer.strip())
4532 lines.append('')
4533 # Note: Gerrit's commit-hook actually cleans message of some lines and
4534 # whitespace. This code is not doing this, but it clearly won't decrease
4535 # entropy.
4536 lines.append(message)
4537 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00004538 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004539 return 'I%s' % change_hash.strip()
4540
4541
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004542def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004543 """Computes the remote branch ref to use for the CL.
4544
4545 Args:
4546 remote (str): The git remote for the CL.
4547 remote_branch (str): The git remote branch for the CL.
4548 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004549 """
4550 if not (remote and remote_branch):
4551 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004552
wittman@chromium.org455dc922015-01-26 20:15:50 +00004553 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004554 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004555 # refs, which are then translated into the remote full symbolic refs
4556 # below.
4557 if '/' not in target_branch:
4558 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4559 else:
4560 prefix_replacements = (
4561 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4562 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4563 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4564 )
4565 match = None
4566 for regex, replacement in prefix_replacements:
4567 match = re.search(regex, target_branch)
4568 if match:
4569 remote_branch = target_branch.replace(match.group(0), replacement)
4570 break
4571 if not match:
4572 # This is a branch path but not one we recognize; use as-is.
4573 remote_branch = target_branch
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00004574 # pylint: disable=consider-using-get
rmistry@google.comc68112d2015-03-03 12:48:06 +00004575 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00004576 # pylint: enable=consider-using-get
rmistry@google.comc68112d2015-03-03 12:48:06 +00004577 # Handle the refs that need to land in different refs.
4578 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004579
wittman@chromium.org455dc922015-01-26 20:15:50 +00004580 # Create the true path to the remote branch.
4581 # Does the following translation:
4582 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
Josip Sokcevicc39ab992020-09-24 20:09:15 +00004583 # * refs/remotes/origin/main -> refs/heads/main
wittman@chromium.org455dc922015-01-26 20:15:50 +00004584 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4585 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4586 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4587 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4588 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4589 'refs/heads/')
4590 elif remote_branch.startswith('refs/remotes/branch-heads'):
4591 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004592
wittman@chromium.org455dc922015-01-26 20:15:50 +00004593 return remote_branch
4594
4595
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004596def cleanup_list(l):
4597 """Fixes a list so that comma separated items are put as individual items.
4598
4599 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4600 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4601 """
4602 items = sum((i.split(',') for i in l), [])
4603 stripped_items = (i.strip() for i in items)
4604 return sorted(filter(None, stripped_items))
4605
4606
Aaron Gable4db38df2017-11-03 14:59:07 -07004607@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004608@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004609def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004610 """Uploads the current changelist to codereview.
4611
4612 Can skip dependency patchset uploads for a branch by running:
4613 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004614 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00004615 git config --unset branch.branch_name.skip-deps-uploads
4616 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004617
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004618 If the name of the checked out branch starts with "bug-" or "fix-" followed
4619 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02004620 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004621
4622 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004623 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004624 [git-cl] add support for hashtags
4625 Foo bar: implement foo
4626 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004627 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004628 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4629 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004630 parser.add_option('--bypass-watchlists', action='store_true',
4631 dest='bypass_watchlists',
4632 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004633 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004634 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004635 parser.add_option('--message', '-m', dest='message',
4636 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004637 parser.add_option('-b', '--bug',
4638 help='pre-populate the bug number(s) for this issue. '
4639 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004640 parser.add_option('--message-file', dest='message_file',
4641 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004642 parser.add_option('--title', '-t', dest='title',
4643 help='title for patchset')
Mario Bianuccicebfb4e2020-07-22 23:08:16 +00004644 parser.add_option('-T', '--skip-title', action='store_true',
4645 dest='skip_title',
4646 help='Use the most recent commit message as the title of '
4647 'the patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004648 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004649 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004650 help='reviewer email addresses')
4651 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004652 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004653 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004654 parser.add_option('--hashtag', dest='hashtags',
4655 action='append', default=[],
4656 help=('Gerrit hashtag for new CL; '
4657 'can be applied multiple times'))
Thiago Perrottab0fb8d52022-08-30 21:26:19 +00004658 parser.add_option('-s',
4659 '--send-mail',
4660 '--send-email',
4661 dest='send_mail',
4662 action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004663 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004664 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004665 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004666 metavar='TARGET',
4667 help='Apply CL to remote ref TARGET. ' +
Josip Sokcevicc39ab992020-09-24 20:09:15 +00004668 'Default: remote branch head, or main')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004669 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004670 help='Squash multiple commits into one')
Mike Frysingera989d552019-08-14 20:51:23 +00004671 parser.add_option('--no-squash', action='store_false', dest='squash',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004672 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004673 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004674 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004675 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4676 const='R', help='add a set of OWNERS to R')
Thiago Perrottab0fb8d52022-08-30 21:26:19 +00004677 parser.add_option('-c',
4678 '--use-commit-queue',
4679 action='store_true',
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004680 default=False,
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004681 help='tell the CQ to commit this patchset; '
Thiago Perrottab0fb8d52022-08-30 21:26:19 +00004682 'implies --send-mail')
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004683 parser.add_option('-d', '--cq-dry-run',
4684 action='store_true', default=False,
rmistry@google.comef966222015-04-07 11:15:01 +00004685 help='Send the patchset to do a CQ dry run right after '
4686 'upload.')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004687 parser.add_option(
4688 '-q',
4689 '--cq-quick-run',
4690 action='store_true',
4691 default=False,
4692 help='Send the patchset to do a CQ quick run right after '
4693 'upload (https://source.chromium.org/chromium/chromium/src/+/main:do'
4694 'cs/cq_quick_run.md) (chromium only).')
Edward Lesmes10c3dd62021-02-08 21:13:57 +00004695 parser.add_option('--set-bot-commit', action='store_true',
4696 help=optparse.SUPPRESS_HELP)
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00004697 parser.add_option('--preserve-tryjobs', action='store_true',
4698 help='instruct the CQ to let tryjobs running even after '
4699 'new patchsets are uploaded instead of canceling '
4700 'prior patchset\' tryjobs')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004701 parser.add_option('--dependencies', action='store_true',
4702 help='Uploads CLs of all the local branches that depend on '
4703 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004704 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4705 help='Sends your change to the CQ after an approval. Only '
4706 'works on repos that have the Auto-Submit label '
4707 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004708 parser.add_option('--parallel', action='store_true',
4709 help='Run all tests specified by input_api.RunTests in all '
4710 'PRESUBMIT files in parallel.')
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004711 parser.add_option('--no-autocc', action='store_true',
4712 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004713 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004714 help='Set the review private. This implies --no-autocc.')
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004715 parser.add_option('-R', '--retry-failed', action='store_true',
4716 help='Retry failed tryjobs from old patchset immediately '
4717 'after uploading new patchset. Cannot be used with '
4718 '--use-commit-queue or --cq-dry-run.')
Dan Beamd8b04ca2019-10-10 21:23:26 +00004719 parser.add_option('--fixed', '-x',
4720 help='List of bugs that will be commented on and marked '
4721 'fixed (pre-populates "Fixed:" tag). Same format as '
4722 '-b option / "Bug:" tag. If fixing several issues, '
4723 'separate with commas.')
Josipe827b0f2020-01-30 00:07:20 +00004724 parser.add_option('--edit-description', action='store_true', default=False,
4725 help='Modify description before upload. Cannot be used '
4726 'with --force. It is a noop when --no-squash is set '
4727 'or a new commit is created.')
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00004728 parser.add_option('--git-completion-helper', action="store_true",
4729 help=optparse.SUPPRESS_HELP)
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00004730 parser.add_option('-o',
4731 '--push-options',
4732 action='append',
4733 default=[],
4734 help='Transmit the given string to the server when '
4735 'performing git push (pass-through). See git-push '
4736 'documentation for more details.')
Gregory Nisbet48d9e1e2021-04-15 23:35:54 +00004737 parser.add_option('--no-add-changeid',
4738 action='store_true',
4739 dest='no_add_changeid',
4740 help='Do not add change-ids to messages.')
Joanna Wangd75fc882023-03-01 21:53:34 +00004741 parser.add_option('--cherry-pick-stacked',
4742 '--cp',
4743 dest='cherry_pick_stacked',
4744 action='store_true',
4745 help='If parent branch has un-uploaded updates, '
4746 'automatically skip parent branches and just upload '
4747 'the current branch cherry-pick on its parent\'s last '
4748 'uploaded commit. Allows users to skip the potential '
4749 'interactive confirmation step.')
Joanna Wanga1abbed2023-01-24 01:41:05 +00004750 # TODO(b/265929888): Add --wip option of --cl-status option.
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004751
rmistry@google.com2dd99862015-06-22 12:22:18 +00004752 orig_args = args
ukai@chromium.orge8077812012-02-03 03:41:46 +00004753 (options, args) = parser.parse_args(args)
4754
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00004755 if options.git_completion_helper:
Edward Lesmesb7db1832020-06-22 20:22:27 +00004756 print(' '.join(opt.get_opt_string() for opt in parser.option_list
4757 if opt.help != optparse.SUPPRESS_HELP))
4758 return
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00004759
sbc@chromium.org71437c02015-04-09 19:29:40 +00004760 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004761 return 1
4762
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004763 options.reviewers = cleanup_list(options.reviewers)
4764 options.cc = cleanup_list(options.cc)
4765
Josipe827b0f2020-01-30 00:07:20 +00004766 if options.edit_description and options.force:
4767 parser.error('Only one of --force and --edit-description allowed')
4768
tandriib80458a2016-06-23 12:20:07 -07004769 if options.message_file:
4770 if options.message:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004771 parser.error('Only one of --message and --message-file allowed.')
tandriib80458a2016-06-23 12:20:07 -07004772 options.message = gclient_utils.FileRead(options.message_file)
tandriib80458a2016-06-23 12:20:07 -07004773
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004774 if ([options.cq_dry_run,
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004775 options.cq_quick_run,
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004776 options.use_commit_queue,
4777 options.retry_failed].count(True) > 1):
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004778 parser.error('Only one of --use-commit-queue, --cq-dry-run, --cq-quick-run '
4779 'or --retry-failed is allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07004780
Mario Bianuccicebfb4e2020-07-22 23:08:16 +00004781 if options.skip_title and options.title:
4782 parser.error('Only one of --title and --skip-title allowed.')
4783
Aaron Gableedbc4132017-09-11 13:22:28 -07004784 if options.use_commit_queue:
4785 options.send_mail = True
4786
Edward Lesmes0dd54822020-03-26 18:24:25 +00004787 if options.squash is None:
4788 # Load default for user, repo, squash=true, in this order.
4789 options.squash = settings.GetSquashGerritUploads()
4790
Joanna Wang5051ffe2023-03-01 22:24:07 +00004791 cl = Changelist(branchref=options.target_branch)
4792
4793 # Warm change details cache now to avoid RPCs later, reducing latency for
4794 # developers.
4795 if cl.GetIssue():
4796 cl._GetChangeDetail(
4797 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
4798
4799 if options.retry_failed and not cl.GetIssue():
4800 print('No previous patchsets, so --retry-failed has no effect.')
4801 options.retry_failed = False
4802
Joanna Wang5051ffe2023-03-01 22:24:07 +00004803
Joanna Wang4786a412023-05-16 18:23:08 +00004804 disable_dogfood_stacked_changes = os.environ.get(
4805 DOGFOOD_STACKED_CHANGES_VAR) == '0'
4806 dogfood_stacked_changes = os.environ.get(DOGFOOD_STACKED_CHANGES_VAR) == '1'
4807
4808 # Only print message for folks who don't have DOGFOOD_STACKED_CHANGES set
4809 # to an expected value.
4810 if (options.squash and not dogfood_stacked_changes
4811 and not disable_dogfood_stacked_changes):
Joanna Wang1398e4f2023-05-01 18:49:13 +00004812 print(
4813 'This repo has been enrolled in the stacked changes dogfood.\n'
4814 '`git cl upload` now uploads the current branch and all upstream '
4815 'branches that have un-uploaded updates.\n'
4816 'Patches can now be reapplied with --force:\n'
4817 '`git cl patch --reapply --force`.\n'
4818 'Googlers may visit go/stacked-changes-dogfood for more information.\n'
Joanna Wang4786a412023-05-16 18:23:08 +00004819 '\n'
4820 'Depot Tools no longer sets new uploads to "WIP". Please update the\n'
4821 '"Set new changes to "work in progress" by default" checkbox at\n'
4822 'https://<host>-review.googlesource.com/settings/\n'
4823 '\n'
4824 'To opt-out use `export DOGFOOD_STACKED_CHANGES=0`.\n'
4825 'To hide this message use `export DOGFOOD_STACKED_CHANGES=1`.\n'
Joanna Wang1398e4f2023-05-01 18:49:13 +00004826 'File bugs at https://bit.ly/3Y6opoI\n')
Joanna Wang5051ffe2023-03-01 22:24:07 +00004827
Joanna Wang4786a412023-05-16 18:23:08 +00004828 if options.squash and not disable_dogfood_stacked_changes:
Joanna Wangdd12deb2023-01-26 20:43:28 +00004829 if options.dependencies:
Joanna Wang1398e4f2023-05-01 18:49:13 +00004830 parser.error('--dependencies is not available for this dogfood workflow.')
Joanna Wang18de1f62023-01-21 01:24:24 +00004831
Joanna Wangd75fc882023-03-01 21:53:34 +00004832 if options.cherry_pick_stacked:
4833 try:
4834 orig_args.remove('--cherry-pick-stacked')
4835 except ValueError:
4836 orig_args.remove('--cp')
Joanna Wang18de1f62023-01-21 01:24:24 +00004837 UploadAllSquashed(options, orig_args)
4838 return 0
4839
Joanna Wangd75fc882023-03-01 21:53:34 +00004840 if options.cherry_pick_stacked:
4841 parser.error('--cherry-pick-stacked is not available for this workflow.')
4842
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004843 # cl.GetMostRecentPatchset uses cached information, and can return the last
4844 # patchset before upload. Calling it here makes it clear that it's the
4845 # last patchset before upload. Note that GetMostRecentPatchset will fail
4846 # if no CL has been uploaded yet.
4847 if options.retry_failed:
4848 patchset = cl.GetMostRecentPatchset()
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004849
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004850 ret = cl.CMDUpload(options, args, orig_args)
4851
4852 if options.retry_failed:
4853 if ret != 0:
4854 print('Upload failed, so --retry-failed has no effect.')
4855 return ret
Joanna Wanga8db0cb2023-01-24 15:43:17 +00004856 builds, _ = _fetch_latest_builds(cl,
4857 DEFAULT_BUILDBUCKET_HOST,
4858 latest_patchset=patchset)
Edward Lemur45768512020-03-02 19:03:14 +00004859 jobs = _filter_failed_for_retry(builds)
4860 if len(jobs) == 0:
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004861 print('No failed tryjobs, so --retry-failed has no effect.')
4862 return ret
Quinten Yearsley777660f2020-03-04 23:37:06 +00004863 _trigger_tryjobs(cl, jobs, options, patchset + 1)
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004864
4865 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00004866
4867
Joanna Wang18de1f62023-01-21 01:24:24 +00004868def UploadAllSquashed(options, orig_args):
4869 # type: (optparse.Values, Sequence[str]) -> Tuple[Sequence[Changelist], bool]
4870 """Uploads the current and upstream branches (if necessary)."""
Joanna Wangc710e2d2023-01-25 14:53:22 +00004871 cls, cherry_pick_current = _UploadAllPrecheck(options, orig_args)
Joanna Wang18de1f62023-01-21 01:24:24 +00004872
Joanna Wangc710e2d2023-01-25 14:53:22 +00004873 # Create commits.
4874 uploads_by_cl = [] #type: Sequence[Tuple[Changelist, _NewUpload]]
4875 if cherry_pick_current:
4876 parent = cls[1]._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
4877 new_upload = cls[0].PrepareCherryPickSquashedCommit(options, parent)
4878 uploads_by_cl.append((cls[0], new_upload))
4879 else:
Joanna Wangc710e2d2023-01-25 14:53:22 +00004880 ordered_cls = list(reversed(cls))
4881
Joanna Wang6215dd02023-02-07 15:58:03 +00004882 cl = ordered_cls[0]
Joanna Wang7603f042023-03-01 22:17:36 +00004883 # We can only support external changes when we're only uploading one
4884 # branch.
4885 parent = cl._UpdateWithExternalChanges() if len(ordered_cls) == 1 else None
Joanna Wang05b60342023-03-29 20:25:57 +00004886 orig_parent = None
Joanna Wang7603f042023-03-01 22:17:36 +00004887 if parent is None:
4888 origin = '.'
4889 branch = cl.GetBranch()
Joanna Wang74c53b62023-03-01 22:00:22 +00004890
Joanna Wang7603f042023-03-01 22:17:36 +00004891 while origin == '.':
4892 # Search for cl's closest ancestor with a gerrit hash.
4893 origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(branch)
4894 if origin == '.':
4895 upstream_branch = scm.GIT.ShortBranchName(upstream_branch_ref)
4896
4897 # Support the `git merge` and `git pull` workflow.
4898 if upstream_branch in ['master', 'main']:
4899 parent = cl.GetCommonAncestorWithUpstream()
4900 else:
Joanna Wang05b60342023-03-29 20:25:57 +00004901 orig_parent = scm.GIT.GetBranchConfig(settings.GetRoot(),
4902 upstream_branch,
4903 LAST_UPLOAD_HASH_CONFIG_KEY)
Joanna Wang7603f042023-03-01 22:17:36 +00004904 parent = scm.GIT.GetBranchConfig(settings.GetRoot(),
4905 upstream_branch,
4906 GERRIT_SQUASH_HASH_CONFIG_KEY)
4907 if parent:
4908 break
4909 branch = upstream_branch
4910 else:
4911 # Either the root of the tree is the cl's direct parent and the while
4912 # loop above only found empty branches between cl and the root of the
4913 # tree.
4914 parent = cl.GetCommonAncestorWithUpstream()
Joanna Wang6215dd02023-02-07 15:58:03 +00004915
Joanna Wang05b60342023-03-29 20:25:57 +00004916 if orig_parent is None:
4917 orig_parent = parent
Joanna Wangc710e2d2023-01-25 14:53:22 +00004918 for i, cl in enumerate(ordered_cls):
4919 # If we're in the middle of the stack, set end_commit to downstream's
4920 # direct ancestor.
4921 if i + 1 < len(ordered_cls):
4922 child_base_commit = ordered_cls[i + 1].GetCommonAncestorWithUpstream()
4923 else:
4924 child_base_commit = None
4925 new_upload = cl.PrepareSquashedCommit(options,
Joanna Wang6215dd02023-02-07 15:58:03 +00004926 parent,
Joanna Wang05b60342023-03-29 20:25:57 +00004927 orig_parent,
Joanna Wangc710e2d2023-01-25 14:53:22 +00004928 end_commit=child_base_commit)
4929 uploads_by_cl.append((cl, new_upload))
Joanna Wangc710e2d2023-01-25 14:53:22 +00004930 parent = new_upload.commit_to_push
Joanna Wang05b60342023-03-29 20:25:57 +00004931 orig_parent = child_base_commit
Joanna Wangc710e2d2023-01-25 14:53:22 +00004932
4933 # Create refspec options
4934 cl, new_upload = uploads_by_cl[-1]
4935 refspec_opts = cl._GetRefSpecOptions(
4936 options,
4937 new_upload.change_desc,
Joanna Wang562481d2023-01-26 21:57:14 +00004938 multi_change_upload=len(uploads_by_cl) > 1,
4939 dogfood_path=True)
Joanna Wangc710e2d2023-01-25 14:53:22 +00004940 refspec_suffix = ''
4941 if refspec_opts:
4942 refspec_suffix = '%' + ','.join(refspec_opts)
4943 assert ' ' not in refspec_suffix, ('spaces not allowed in refspec: "%s"' %
4944 refspec_suffix)
4945
4946 remote, remote_branch = cl.GetRemoteBranch()
4947 branch = GetTargetRef(remote, remote_branch, options.target_branch)
4948 refspec = '%s:refs/for/%s%s' % (new_upload.commit_to_push, branch,
4949 refspec_suffix)
4950
4951 # Git push
4952 git_push_metadata = {
4953 'gerrit_host':
4954 cl.GetGerritHost(),
4955 'title':
4956 options.title or '<untitled>',
4957 'change_id':
4958 git_footers.get_footer_change_id(new_upload.change_desc.description),
4959 'description':
4960 new_upload.change_desc.description,
4961 }
4962 push_stdout = cl._RunGitPushWithTraces(refspec, refspec_opts,
Joanna Wang34086522023-03-14 22:01:57 +00004963 git_push_metadata,
4964 options.push_options)
Joanna Wangc710e2d2023-01-25 14:53:22 +00004965
4966 # Post push updates
4967 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
4968 change_numbers = [
4969 m.group(1) for m in map(regex.match, push_stdout.splitlines()) if m
4970 ]
4971
4972 for i, (cl, new_upload) in enumerate(uploads_by_cl):
4973 cl.PostUploadUpdates(options, new_upload, change_numbers[i])
4974
4975 return 0
Joanna Wang18de1f62023-01-21 01:24:24 +00004976
4977
4978def _UploadAllPrecheck(options, orig_args):
4979 # type: (optparse.Values, Sequence[str]) -> Tuple[Sequence[Changelist], bool]
4980 """Checks the state of the tree and gives the user uploading options
4981
4982 Returns: A tuple of the ordered list of changes that have new commits
4983 since their last upload and a boolean of whether the user wants to
4984 cherry-pick and upload the current branch instead of uploading all cls.
4985 """
Joanna Wang6b98cdc2023-02-16 00:37:20 +00004986 cl = Changelist()
4987 if cl.GetBranch() is None:
4988 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
4989
Joanna Wang18de1f62023-01-21 01:24:24 +00004990 branch_ref = None
4991 cls = []
4992 must_upload_upstream = False
Joanna Wang6215dd02023-02-07 15:58:03 +00004993 first_pass = True
Joanna Wang18de1f62023-01-21 01:24:24 +00004994
4995 Changelist._GerritCommitMsgHookCheck(offer_removal=not options.force)
4996
4997 while True:
4998 if len(cls) > _MAX_STACKED_BRANCHES_UPLOAD:
4999 DieWithError(
5000 'More than %s branches in the stack have not been uploaded.\n'
5001 'Are your branches in a misconfigured state?\n'
5002 'If not, please upload some upstream changes first.' %
5003 (_MAX_STACKED_BRANCHES_UPLOAD))
5004
5005 cl = Changelist(branchref=branch_ref)
Joanna Wang18de1f62023-01-21 01:24:24 +00005006
Joanna Wang6215dd02023-02-07 15:58:03 +00005007 # Only add CL if it has anything to commit.
5008 base_commit = cl.GetCommonAncestorWithUpstream()
5009 end_commit = RunGit(['rev-parse', cl.GetBranchRef()]).strip()
5010
5011 diff = RunGitSilent(['diff', '%s..%s' % (base_commit, end_commit)])
5012 if diff:
5013 cls.append(cl)
5014 if (not first_pass and
5015 cl._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY) is None):
5016 # We are mid-stack and the user must upload their upstream branches.
5017 must_upload_upstream = True
5018 elif first_pass: # The current branch has nothing to commit. Exit.
5019 DieWithError('Branch %s has nothing to commit' % cl.GetBranch())
5020 # Else: A mid-stack branch has nothing to commit. We do not add it to cls.
5021 first_pass = False
5022
5023 # Cases below determine if we should continue to traverse up the tree.
Joanna Wang18de1f62023-01-21 01:24:24 +00005024 origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(cl.GetBranch())
Joanna Wang18de1f62023-01-21 01:24:24 +00005025 branch_ref = upstream_branch_ref # set branch for next run.
5026
Joanna Wang6215dd02023-02-07 15:58:03 +00005027 upstream_branch = scm.GIT.ShortBranchName(upstream_branch_ref)
5028 upstream_last_upload = scm.GIT.GetBranchConfig(settings.GetRoot(),
5029 upstream_branch,
5030 LAST_UPLOAD_HASH_CONFIG_KEY)
5031
Joanna Wang18de1f62023-01-21 01:24:24 +00005032 # Case 1: We've reached the beginning of the tree.
5033 if origin != '.':
5034 break
5035
Joanna Wang18de1f62023-01-21 01:24:24 +00005036 # Case 2: If any upstream branches have never been uploaded,
Joanna Wang6215dd02023-02-07 15:58:03 +00005037 # the user MUST upload them unless they are empty. Continue to
5038 # next loop to add upstream if it is not empty.
Joanna Wang18de1f62023-01-21 01:24:24 +00005039 if not upstream_last_upload:
Joanna Wang18de1f62023-01-21 01:24:24 +00005040 continue
5041
Joanna Wang18de1f62023-01-21 01:24:24 +00005042 # Case 3: If upstream's last_upload == cl.base_commit we do
5043 # not need to upload any more upstreams from this point on.
5044 # (Even if there may be diverged branches higher up the tree)
5045 if base_commit == upstream_last_upload:
5046 break
5047
5048 # Case 4: If upstream's last_upload < cl.base_commit we are
5049 # uploading cl and upstream_cl.
5050 # Continue up the tree to check other branch relations.
Joanna Wangab9c6ba2023-01-21 01:46:36 +00005051 if scm.GIT.IsAncestor(upstream_last_upload, base_commit):
Joanna Wang18de1f62023-01-21 01:24:24 +00005052 continue
5053
5054 # Case 5: If cl.base_commit < upstream's last_upload the user
5055 # must rebase before uploading.
Joanna Wangab9c6ba2023-01-21 01:46:36 +00005056 if scm.GIT.IsAncestor(base_commit, upstream_last_upload):
Joanna Wang18de1f62023-01-21 01:24:24 +00005057 DieWithError(
5058 'At least one branch in the stack has diverged from its upstream '
5059 'branch and does not contain its upstream\'s last upload.\n'
5060 'Please rebase the stack with `git rebase-update` before uploading.')
5061
5062 # The tree went through a rebase. LAST_UPLOAD_HASH_CONFIG_KEY no longer has
5063 # any relation to commits in the tree. Continue up the tree until we hit
5064 # the root.
5065
5066 # We assume all cls in the stack have the same auth requirements and only
5067 # check this once.
5068 cls[0].EnsureAuthenticated(force=options.force)
5069
5070 cherry_pick = False
5071 if len(cls) > 1:
Joanna Wangd75fc882023-03-01 21:53:34 +00005072 opt_message = ''
Joanna Wang6215dd02023-02-07 15:58:03 +00005073 branches = ', '.join([cl.branch for cl in cls])
Joanna Wang18de1f62023-01-21 01:24:24 +00005074 if len(orig_args):
Joanna Wangd75fc882023-03-01 21:53:34 +00005075 opt_message = ('options %s will be used for all uploads.\n' % orig_args)
Joanna Wang18de1f62023-01-21 01:24:24 +00005076 if must_upload_upstream:
Joanna Wangd75fc882023-03-01 21:53:34 +00005077 msg = ('At least one parent branch in `%s` has never been uploaded '
5078 'and must be uploaded before/with `%s`.\n' %
5079 (branches, cls[1].branch))
5080 if options.cherry_pick_stacked:
5081 DieWithError(msg)
5082 if not options.force:
5083 confirm_or_exit('\n' + opt_message + msg)
Joanna Wang18de1f62023-01-21 01:24:24 +00005084 else:
Joanna Wangd75fc882023-03-01 21:53:34 +00005085 if options.cherry_pick_stacked:
5086 print('cherry-picking `%s` on %s\'s last upload' %
5087 (cls[0].branch, cls[1].branch))
Joanna Wang18de1f62023-01-21 01:24:24 +00005088 cherry_pick = True
Joanna Wangd75fc882023-03-01 21:53:34 +00005089 elif not options.force:
5090 answer = gclient_utils.AskForData(
5091 '\n' + opt_message +
5092 'Press enter to update branches %s.\nOr type `n` to upload only '
5093 '`%s` cherry-picked on %s\'s last upload:' %
5094 (branches, cls[0].branch, cls[1].branch))
5095 if answer.lower() == 'n':
5096 cherry_pick = True
Joanna Wang18de1f62023-01-21 01:24:24 +00005097 return cls, cherry_pick
5098
5099
Francois Dorayd42c6812017-05-30 15:10:20 -04005100@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005101@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04005102def CMDsplit(parser, args):
5103 """Splits a branch into smaller branches and uploads CLs.
5104
5105 Creates a branch and uploads a CL for each group of files modified in the
5106 current branch that share a common OWNERS file. In the CL description and
Edward Lemurac5c55f2020-02-29 00:17:16 +00005107 comment, the string '$directory', is replaced with the directory containing
5108 the shared OWNERS file.
Francois Dorayd42c6812017-05-30 15:10:20 -04005109 """
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005110 parser.add_option('-d', '--description', dest='description_file',
5111 help='A text file containing a CL description in which '
5112 '$directory will be replaced by each CL\'s directory.')
5113 parser.add_option('-c', '--comment', dest='comment_file',
5114 help='A text file containing a CL comment.')
5115 parser.add_option('-n', '--dry-run', dest='dry_run', action='store_true',
Chris Watkinsba28e462017-12-13 11:22:17 +11005116 default=False,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005117 help='List the files and reviewers for each CL that would '
5118 'be created, but don\'t create branches or CLs.')
5119 parser.add_option('--cq-dry-run', action='store_true',
5120 help='If set, will do a cq dry run for each uploaded CL. '
5121 'Please be careful when doing this; more than ~10 CLs '
5122 'has the potential to overload our build '
5123 'infrastructure. Try to upload these not during high '
5124 'load times (usually 11-3 Mountain View time). Email '
5125 'infra-dev@chromium.org with any questions.')
Anne Redulla072d06e2023-07-06 23:12:16 +00005126 parser.add_option('-a',
5127 '--enable-auto-submit',
5128 action='store_true',
5129 dest='enable_auto_submit',
Takuto Ikuta51eca592019-02-14 19:40:52 +00005130 default=True,
5131 help='Sends your change to the CQ after an approval. Only '
Anne Redulla072d06e2023-07-06 23:12:16 +00005132 'works on repos that have the Auto-Submit label '
5133 'enabled')
5134 parser.add_option('--disable-auto-submit',
5135 action='store_false',
5136 dest='enable_auto_submit',
5137 help='Disables automatic sending of the changes to the CQ '
5138 'after approval. Note that auto-submit only works for '
5139 'repos that have the Auto-Submit label enabled.')
Daniel Cheng403c44e2022-10-05 22:24:58 +00005140 parser.add_option('--max-depth',
5141 type='int',
5142 default=0,
5143 help='The max depth to look for OWNERS files. Useful for '
5144 'controlling the granularity of the split CLs, e.g. '
5145 '--max-depth=1 will only split by top-level '
5146 'directory. Specifying a value less than 1 means no '
5147 'limit on max depth.')
Rachael Newitt03e49122023-06-28 21:39:21 +00005148 parser.add_option('--topic',
5149 default=None,
5150 help='Topic to specify when uploading')
Francois Dorayd42c6812017-05-30 15:10:20 -04005151 options, _ = parser.parse_args(args)
5152
5153 if not options.description_file:
5154 parser.error('No --description flag specified.')
5155
5156 def WrappedCMDupload(args):
5157 return CMDupload(OptionParser(), args)
5158
Daniel Cheng403c44e2022-10-05 22:24:58 +00005159 return split_cl.SplitCl(options.description_file, options.comment_file,
5160 Changelist, WrappedCMDupload, options.dry_run,
5161 options.cq_dry_run, options.enable_auto_submit,
Rachael Newitt03e49122023-06-28 21:39:21 +00005162 options.max_depth, options.topic, settings.GetRoot())
Francois Dorayd42c6812017-05-30 15:10:20 -04005163
5164
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005165@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005166@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005167def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005168 """DEPRECATED: Used to commit the current changelist via git-svn."""
5169 message = ('git-cl no longer supports committing to SVN repositories via '
5170 'git-svn. You probably want to use `git cl land` instead.')
5171 print(message)
5172 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005173
5174
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005175@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005176@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005177def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005178 """Commits the current changelist via git.
5179
5180 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5181 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005182 """
5183 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5184 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07005185 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005186 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04005187 parser.add_option('--parallel', action='store_true',
5188 help='Run all tests specified by input_api.RunTests in all '
5189 'PRESUBMIT files in parallel.')
Saagar Sanghavi03b15132020-08-10 16:43:41 +00005190 parser.add_option('--resultdb', action='store_true',
5191 help='Run presubmit checks in the ResultSink environment '
5192 'and send results to the ResultDB database.')
5193 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005194 (options, args) = parser.parse_args(args)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005195
Edward Lemur934836a2019-09-09 20:16:54 +00005196 cl = Changelist()
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005197
Robert Iannucci2e73d432018-03-14 01:10:47 -07005198 if not cl.GetIssue():
5199 DieWithError('You must upload the change first to Gerrit.\n'
5200 ' If you would rather have `git cl land` upload '
5201 'automatically for you, see http://crbug.com/642759')
Saagar Sanghavi03b15132020-08-10 16:43:41 +00005202 return cl.CMDLand(options.force, options.bypass_hooks, options.verbose,
5203 options.parallel, options.resultdb, options.realm)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005204
5205
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005206@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005207@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005208def CMDpatch(parser, args):
Victor Hugo Vianna Silvadeff9a22023-07-11 18:07:18 +00005209 """Applies (cherry-picks) a Gerrit changelist locally."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005210 parser.add_option('-b', dest='newbranch',
5211 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005212 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005213 help='overwrite state on the current or chosen branch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005214 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
Edward Lemurf38bc172019-09-03 21:02:13 +00005215 help='don\'t commit after patch applies.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005216
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005217 group = optparse.OptionGroup(
5218 parser,
5219 'Options for continuing work on the current issue uploaded from a '
5220 'different clone (e.g. different machine). Must be used independently '
5221 'from the other options. No issue number should be specified, and the '
5222 'branch must have an issue number associated with it')
5223 group.add_option('--reapply', action='store_true', dest='reapply',
5224 help='Reset the branch and reapply the issue.\n'
5225 'CAUTION: This will undo any local changes in this '
5226 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005227
5228 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005229 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005230 parser.add_option_group(group)
5231
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005232 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005233
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005234 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005235 if options.newbranch:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005236 parser.error('--reapply works on the current branch only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005237 if len(args) > 0:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005238 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005239
Edward Lemur934836a2019-09-09 20:16:54 +00005240 cl = Changelist()
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005241 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005242 parser.error('Current branch must have an associated issue.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005243
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005244 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005245 if upstream is None:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005246 parser.error('No upstream branch specified. Cannot reset branch.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005247
5248 RunGit(['reset', '--hard', upstream])
5249 if options.pull:
5250 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005251
Edward Lemur678a6842019-10-03 22:25:05 +00005252 target_issue_arg = ParseIssueNumberArgument(cl.GetIssue())
Joanna Wang44e9bee2023-01-25 21:51:42 +00005253 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit,
5254 options.force, False)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005255
5256 if len(args) != 1 or not args[0]:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005257 parser.error('Must specify issue number or URL.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005258
Edward Lemurf38bc172019-09-03 21:02:13 +00005259 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005260 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005261 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005262
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005263 # We don't want uncommitted changes mixed up with the patch.
5264 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005265 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005266
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005267 if options.newbranch:
5268 if options.force:
5269 RunGit(['branch', '-D', options.newbranch],
5270 stderr=subprocess2.PIPE, error_ok=True)
Edward Lemur84101642020-02-21 21:40:34 +00005271 git_new_branch.create_new_branch(options.newbranch)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005272
Edward Lemur678a6842019-10-03 22:25:05 +00005273 cl = Changelist(
5274 codereview_host=target_issue_arg.hostname, issue=target_issue_arg.issue)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005275
Edward Lemur678a6842019-10-03 22:25:05 +00005276 if not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00005277 print('canonical issue/change URL: %s\n' % cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005278
Bruce Dawsonf362f6f2021-02-18 23:15:17 +00005279 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit,
5280 options.force, options.newbranch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005281
5282
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005283def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005284 """Fetches the tree status and returns either 'open', 'closed',
5285 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005286 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005287 if url:
Daniel McArdle8b4eeff2020-07-20 17:02:47 +00005288 status = str(urllib.request.urlopen(url).read().lower())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005289 if status.find('closed') != -1 or status == '0':
5290 return 'closed'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005291
5292 if status.find('open') != -1 or status == '1':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005293 return 'open'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005294
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005295 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005296 return 'unset'
5297
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005298
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005299def GetTreeStatusReason():
5300 """Fetches the tree status from a json url and returns the message
5301 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005302 url = settings.GetTreeStatusUrl()
Daniel McArdle8b4eeff2020-07-20 17:02:47 +00005303 json_url = urllib.parse.urljoin(url, '/current?format=json')
Edward Lemur79d4f992019-11-11 23:49:02 +00005304 connection = urllib.request.urlopen(json_url)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005305 status = json.loads(connection.read())
5306 connection.close()
5307 return status['message']
5308
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005309
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005310@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005311def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005312 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005313 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005314 status = GetTreeStatus()
5315 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005316 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005317 return 2
5318
vapiera7fbd5a2016-06-16 09:17:49 -07005319 print('The tree is %s' % status)
5320 print()
5321 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005322 if status != 'open':
5323 return 1
5324 return 0
5325
5326
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005327@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005328def CMDtry(parser, args):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005329 """Triggers tryjobs using either Buildbucket or CQ dry run."""
5330 group = optparse.OptionGroup(parser, 'Tryjob options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005331 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005332 '-b', '--bot', action='append',
5333 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5334 'times to specify multiple builders. ex: '
5335 '"-b win_rel -b win_layout". See '
5336 'the try server waterfall for the builders name and the tests '
5337 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005338 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005339 '-B', '--bucket', default='',
Ben Pastene08a30b22022-05-04 17:46:38 +00005340 help=('Buildbucket bucket to send the try requests. Format: '
5341 '"luci.$LUCI_PROJECT.$LUCI_BUCKET". eg: "luci.chromium.try"'))
borenet6c0efe62016-10-19 08:13:29 -07005342 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005343 '-r', '--revision',
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005344 help='Revision to use for the tryjob; default: the revision will '
tandriif7b29d42016-10-07 08:45:41 -07005345 'be determined by the try recipe that builder runs, which usually '
Josip Sokcevicc39ab992020-09-24 20:09:15 +00005346 'defaults to HEAD of origin/master or origin/main')
maruel@chromium.org15192402012-09-06 12:38:29 +00005347 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005348 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005349 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005350 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005351 group.add_option(
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005352 '-q',
5353 '--quick-run',
5354 action='store_true',
5355 default=False,
5356 help='trigger in quick run mode '
5357 '(https://source.chromium.org/chromium/chromium/src/+/main:docs/cq_q'
5358 'uick_run.md) (chromium only).')
5359 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005360 '--category', default='git_cl_try', help='Specify custom build category.')
5361 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005362 '--project',
5363 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005364 'in recipe to determine to which repository or directory to '
5365 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005366 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005367 '-p', '--property', dest='properties', action='append', default=[],
5368 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005369 'key2=value2 etc. The value will be treated as '
5370 'json if decodable, or as string otherwise. '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005371 'NOTE: using this may make your tryjob not usable for CQ, '
5372 'which will then schedule another tryjob with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005373 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005374 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5375 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005376 parser.add_option_group(group)
Quinten Yearsley983111f2019-09-26 17:18:48 +00005377 parser.add_option(
5378 '-R', '--retry-failed', action='store_true', default=False,
5379 help='Retry failed jobs from the latest set of tryjobs. '
5380 'Not allowed with --bucket and --bot options.')
Edward Lemur52969c92020-02-06 18:15:28 +00005381 parser.add_option(
5382 '-i', '--issue', type=int,
5383 help='Operate on this issue instead of the current branch\'s implicit '
5384 'issue.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005385 options, args = parser.parse_args(args)
5386
machenbach@chromium.org45453142015-09-15 08:45:22 +00005387 # Make sure that all properties are prop=value pairs.
5388 bad_params = [x for x in options.properties if '=' not in x]
5389 if bad_params:
5390 parser.error('Got properties with missing "=": %s' % bad_params)
5391
maruel@chromium.org15192402012-09-06 12:38:29 +00005392 if args:
5393 parser.error('Unknown arguments: %s' % args)
5394
Edward Lemur934836a2019-09-09 20:16:54 +00005395 cl = Changelist(issue=options.issue)
maruel@chromium.org15192402012-09-06 12:38:29 +00005396 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005397 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005398
Edward Lemurf38bc172019-09-03 21:02:13 +00005399 # HACK: warm up Gerrit change detail cache to save on RPCs.
Edward Lemur125d60a2019-09-13 18:25:41 +00005400 cl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005401
tandriie113dfd2016-10-11 10:20:12 -07005402 error_message = cl.CannotTriggerTryJobReason()
5403 if error_message:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005404 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005405
Edward Lemur45768512020-03-02 19:03:14 +00005406 if options.bot:
5407 if options.retry_failed:
5408 parser.error('--bot is not compatible with --retry-failed.')
5409 if not options.bucket:
5410 parser.error('A bucket (e.g. "chromium/try") is required.')
5411
5412 triggered = [b for b in options.bot if 'triggered' in b]
5413 if triggered:
5414 parser.error(
5415 'Cannot schedule builds on triggered bots: %s.\n'
5416 'This type of bot requires an initial job from a parent (usually a '
5417 'builder). Schedule a job on the parent instead.\n' % triggered)
5418
5419 if options.bucket.startswith('.master'):
5420 parser.error('Buildbot masters are not supported.')
5421
5422 project, bucket = _parse_bucket(options.bucket)
5423 if project is None or bucket is None:
5424 parser.error('Invalid bucket: %s.' % options.bucket)
5425 jobs = sorted((project, bucket, bot) for bot in options.bot)
5426 elif options.retry_failed:
Quinten Yearsley983111f2019-09-26 17:18:48 +00005427 print('Searching for failed tryjobs...')
Joanna Wanga8db0cb2023-01-24 15:43:17 +00005428 builds, patchset = _fetch_latest_builds(cl, DEFAULT_BUILDBUCKET_HOST)
Quinten Yearsley983111f2019-09-26 17:18:48 +00005429 if options.verbose:
5430 print('Got %d builds in patchset #%d' % (len(builds), patchset))
Edward Lemur45768512020-03-02 19:03:14 +00005431 jobs = _filter_failed_for_retry(builds)
5432 if not jobs:
Quinten Yearsley983111f2019-09-26 17:18:48 +00005433 print('There are no failed jobs in the latest set of jobs '
5434 '(patchset #%d), doing nothing.' % patchset)
5435 return 0
Edward Lemur45768512020-03-02 19:03:14 +00005436 num_builders = len(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +00005437 if num_builders > 10:
5438 confirm_or_exit('There are %d builders with failed builds.'
5439 % num_builders, action='continue')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005440 elif options.quick_run:
5441 print('Scheduling CQ quick run on: %s' % cl.GetIssueURL())
5442 return cl.SetCQState(_CQState.QUICK_RUN)
Quinten Yearsley983111f2019-09-26 17:18:48 +00005443 else:
qyearsley1fdfcb62016-10-24 13:22:03 -07005444 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005445 print('git cl try with no bots now defaults to CQ dry run.')
5446 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5447 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005448
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005449 patchset = cl.GetMostRecentPatchset()
Edward Lemur2c210a42019-09-16 23:58:35 +00005450 try:
Quinten Yearsley777660f2020-03-04 23:37:06 +00005451 _trigger_tryjobs(cl, jobs, options, patchset)
Edward Lemur2c210a42019-09-16 23:58:35 +00005452 except BuildbucketResponseException as ex:
5453 print('ERROR: %s' % ex)
5454 return 1
5455 return 0
maruel@chromium.org15192402012-09-06 12:38:29 +00005456
5457
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005458@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005459def CMDtry_results(parser, args):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005460 """Prints info about results for tryjobs associated with the current CL."""
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005461 group = optparse.OptionGroup(parser, 'Tryjob results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005462 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005463 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005464 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005465 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005466 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005467 '--color', action='store_true', default=setup_color.IS_TTY,
5468 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005469 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005470 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5471 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005472 group.add_option(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005473 '--json', help=('Path of JSON output file to write tryjob results to,'
Stefan Zager1306bd02017-06-22 19:26:46 -07005474 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005475 parser.add_option_group(group)
Edward Lemur52969c92020-02-06 18:15:28 +00005476 parser.add_option(
5477 '-i', '--issue', type=int,
5478 help='Operate on this issue instead of the current branch\'s implicit '
5479 'issue.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005480 options, args = parser.parse_args(args)
5481 if args:
5482 parser.error('Unrecognized args: %s' % ' '.join(args))
5483
Edward Lemur934836a2019-09-09 20:16:54 +00005484 cl = Changelist(issue=options.issue)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005485 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005486 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005487
tandrii221ab252016-10-06 08:12:04 -07005488 patchset = options.patchset
5489 if not patchset:
Gavin Make61ccc52020-11-13 00:12:57 +00005490 patchset = cl.GetMostRecentDryRunPatchset()
tandrii221ab252016-10-06 08:12:04 -07005491 if not patchset:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005492 parser.error('Code review host doesn\'t know about issue %s. '
tandrii221ab252016-10-06 08:12:04 -07005493 'No access to issue or wrong issue number?\n'
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005494 'Either upload first, or pass --patchset explicitly.' %
tandrii221ab252016-10-06 08:12:04 -07005495 cl.GetIssue())
5496
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005497 try:
Joanna Wanga8db0cb2023-01-24 15:43:17 +00005498 jobs = _fetch_tryjobs(cl, DEFAULT_BUILDBUCKET_HOST, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005499 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005500 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005501 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005502 if options.json:
Edward Lemurbaaf6be2019-10-09 18:00:44 +00005503 write_json(options.json, jobs)
qyearsley53f48a12016-09-01 10:45:13 -07005504 else:
Quinten Yearsley777660f2020-03-04 23:37:06 +00005505 _print_tryjobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005506 return 0
5507
5508
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005509@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005510@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005511def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005512 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005513 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005514 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005515 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005516
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005517 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005518 if args:
5519 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005520 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005521 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005522 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005523 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005524
5525 # Clear configured merge-base, if there is one.
5526 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005527 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005528 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005529 return 0
5530
5531
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005532@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005533def CMDweb(parser, args):
5534 """Opens the current CL in the web browser."""
Orr Bernstein0b960582022-12-22 20:16:18 +00005535 parser.add_option('-p',
5536 '--print-only',
5537 action='store_true',
5538 dest='print_only',
5539 help='Only print the Gerrit URL, don\'t open it in the '
5540 'browser.')
5541 (options, args) = parser.parse_args(args)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005542 if args:
5543 parser.error('Unrecognized args: %s' % ' '.join(args))
5544
5545 issue_url = Changelist().GetIssueURL()
5546 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005547 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005548 return 1
5549
Orr Bernstein0b960582022-12-22 20:16:18 +00005550 if options.print_only:
5551 print(issue_url)
5552 return 0
5553
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005554 # Redirect I/O before invoking browser to hide its output. For example, this
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005555 # allows us to hide the "Created new window in existing browser session."
5556 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005557 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005558 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005559 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005560 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005561 os.open(os.devnull, os.O_RDWR)
5562 try:
5563 webbrowser.open(issue_url)
5564 finally:
5565 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005566 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005567 return 0
5568
5569
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005570@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005571def CMDset_commit(parser, args):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00005572 """Sets the commit bit to trigger the CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005573 parser.add_option('-d', '--dry-run', action='store_true',
5574 help='trigger in dry run mode')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005575 parser.add_option(
5576 '-q',
5577 '--quick-run',
5578 action='store_true',
5579 help='trigger in quick run mode '
5580 '(https://source.chromium.org/chromium/chromium/src/+/main:docs/cq_qu'
5581 'ick_run.md) (chromium only).')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005582 parser.add_option('-c', '--clear', action='store_true',
5583 help='stop CQ run, if any')
Edward Lemur52969c92020-02-06 18:15:28 +00005584 parser.add_option(
5585 '-i', '--issue', type=int,
5586 help='Operate on this issue instead of the current branch\'s implicit '
5587 'issue.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005588 options, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005589 if args:
5590 parser.error('Unrecognized args: %s' % ' '.join(args))
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005591 if [options.dry_run, options.quick_run, options.clear].count(True) > 1:
5592 parser.error('Only one of --dry-run, --quick-run, and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005593
Edward Lemur934836a2019-09-09 20:16:54 +00005594 cl = Changelist(issue=options.issue)
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005595 if not cl.GetIssue():
5596 parser.error('Must upload the issue first.')
5597
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005598 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005599 state = _CQState.NONE
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005600 elif options.quick_run:
5601 state = _CQState.QUICK_RUN
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005602 elif options.dry_run:
5603 state = _CQState.DRY_RUN
5604 else:
5605 state = _CQState.COMMIT
tandrii9de9ec62016-07-13 03:01:59 -07005606 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005607 return 0
5608
5609
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005610@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005611def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005612 """Closes the issue."""
Edward Lemur52969c92020-02-06 18:15:28 +00005613 parser.add_option(
5614 '-i', '--issue', type=int,
5615 help='Operate on this issue instead of the current branch\'s implicit '
5616 'issue.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005617 options, args = parser.parse_args(args)
groby@chromium.org411034a2013-02-26 15:12:01 +00005618 if args:
5619 parser.error('Unrecognized args: %s' % ' '.join(args))
Edward Lemur934836a2019-09-09 20:16:54 +00005620 cl = Changelist(issue=options.issue)
groby@chromium.org411034a2013-02-26 15:12:01 +00005621 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005622 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005623 DieWithError('ERROR: No issue to close.')
groby@chromium.org411034a2013-02-26 15:12:01 +00005624 cl.CloseIssue()
5625 return 0
5626
5627
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005628@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005629def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005630 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005631 parser.add_option(
5632 '--stat',
5633 action='store_true',
5634 dest='stat',
5635 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005636 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005637 if args:
5638 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005639
Edward Lemur934836a2019-09-09 20:16:54 +00005640 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005641 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005642 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005643 if not issue:
5644 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005645
Gavin Makbe2e9262022-11-08 23:41:55 +00005646 base = cl._GitGetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY)
Aaron Gablea718c3e2017-08-28 17:47:28 -07005647 if not base:
Gavin Makbe2e9262022-11-08 23:41:55 +00005648 base = cl._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
Aaron Gablea718c3e2017-08-28 17:47:28 -07005649 if not base:
5650 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5651 revision_info = detail['revisions'][detail['current_revision']]
5652 fetch_info = revision_info['fetch']['http']
5653 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5654 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005655
Aaron Gablea718c3e2017-08-28 17:47:28 -07005656 cmd = ['git', 'diff']
5657 if options.stat:
5658 cmd.append('--stat')
5659 cmd.append(base)
5660 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005661
5662 return 0
5663
5664
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005665@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005666def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005667 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005668 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005669 '--ignore-current',
5670 action='store_true',
5671 help='Ignore the CL\'s current reviewers and start from scratch.')
5672 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005673 '--ignore-self',
5674 action='store_true',
5675 help='Do not consider CL\'s author as an owners.')
5676 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005677 '--no-color',
5678 action='store_true',
5679 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005680 parser.add_option(
5681 '--batch',
5682 action='store_true',
5683 help='Do not run interactively, just suggest some')
Yang Guo6e269a02019-06-26 11:17:02 +00005684 # TODO: Consider moving this to another command, since other
5685 # git-cl owners commands deal with owners for a given CL.
5686 parser.add_option(
5687 '--show-all',
5688 action='store_true',
5689 help='Show all owners for a particular file')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005690 options, args = parser.parse_args(args)
5691
Edward Lemur934836a2019-09-09 20:16:54 +00005692 cl = Changelist()
Edward Lesmes50da7702020-03-30 19:23:43 +00005693 author = cl.GetAuthor()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005694
Yang Guo6e269a02019-06-26 11:17:02 +00005695 if options.show_all:
Bruce Dawson97ed44a2020-05-06 17:04:03 +00005696 if len(args) == 0:
5697 print('No files specified for --show-all. Nothing to do.')
5698 return 0
Edward Lesmese1576912021-02-16 21:53:34 +00005699 owners_by_path = cl.owners_client.BatchListOwners(args)
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +00005700 for path in args:
5701 print('Owners for %s:' % path)
5702 print('\n'.join(
5703 ' - %s' % owner
5704 for owner in owners_by_path.get(path, ['No owners found'])))
Yang Guo6e269a02019-06-26 11:17:02 +00005705 return 0
5706
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005707 if args:
5708 if len(args) > 1:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005709 parser.error('Unknown args.')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005710 base_branch = args[0]
5711 else:
5712 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005713 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005714
Edward Lemur2c62b332020-03-12 22:12:33 +00005715 affected_files = cl.GetAffectedFiles(base_branch)
Dirk Prankebf980882017-09-02 15:08:00 -07005716
5717 if options.batch:
Edward Lesmese1576912021-02-16 21:53:34 +00005718 owners = cl.owners_client.SuggestOwners(affected_files, exclude=[author])
5719 print('\n'.join(owners))
Dirk Prankebf980882017-09-02 15:08:00 -07005720 return 0
5721
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005722 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005723 affected_files,
Edward Lemur707d70b2018-02-07 00:50:14 +01005724 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005725 [] if options.ignore_current else cl.GetReviewers(),
Edward Lesmes5cd75472021-02-19 00:34:25 +00005726 cl.owners_client,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005727 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005728 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005729
5730
Aiden Bennerc08566e2018-10-03 17:52:42 +00005731def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005732 """Generates a diff command."""
5733 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005734 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5735
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005736 if allow_prefix:
5737 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5738 # case that diff.noprefix is set in the user's git config.
5739 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5740 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005741 diff_cmd += ['--no-prefix']
5742
5743 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005744
5745 if args:
5746 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005747 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005748 diff_cmd.append(arg)
5749 else:
5750 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005751
5752 return diff_cmd
5753
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005754
Jamie Madill5e96ad12020-01-13 16:08:35 +00005755def _RunClangFormatDiff(opts, clang_diff_files, top_dir, upstream_commit):
5756 """Runs clang-format-diff and sets a return value if necessary."""
5757
5758 if not clang_diff_files:
5759 return 0
5760
5761 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5762 # formatted. This is used to block during the presubmit.
5763 return_value = 0
5764
5765 # Locate the clang-format binary in the checkout
5766 try:
5767 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
5768 except clang_format.NotFoundError as e:
5769 DieWithError(e)
5770
5771 if opts.full or settings.GetFormatFullByDefault():
5772 cmd = [clang_format_tool]
5773 if not opts.dry_run and not opts.diff:
5774 cmd.append('-i')
5775 if opts.dry_run:
5776 for diff_file in clang_diff_files:
5777 with open(diff_file, 'r') as myfile:
5778 code = myfile.read().replace('\r\n', '\n')
5779 stdout = RunCommand(cmd + [diff_file], cwd=top_dir)
5780 stdout = stdout.replace('\r\n', '\n')
5781 if opts.diff:
5782 sys.stdout.write(stdout)
5783 if code != stdout:
5784 return_value = 2
5785 else:
5786 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
5787 if opts.diff:
5788 sys.stdout.write(stdout)
5789 else:
Jamie Madill5e96ad12020-01-13 16:08:35 +00005790 try:
5791 script = clang_format.FindClangFormatScriptInChromiumTree(
5792 'clang-format-diff.py')
5793 except clang_format.NotFoundError as e:
5794 DieWithError(e)
5795
Josip Sokcevic2a827fc2022-03-04 17:51:47 +00005796 cmd = ['vpython3', script, '-p0']
Jamie Madill5e96ad12020-01-13 16:08:35 +00005797 if not opts.dry_run and not opts.diff:
5798 cmd.append('-i')
5799
5800 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
Edward Lemur1a83da12020-03-04 21:18:36 +00005801 diff_output = RunGit(diff_cmd).encode('utf-8')
Jamie Madill5e96ad12020-01-13 16:08:35 +00005802
Edward Lesmes89624cd2020-04-06 17:51:56 +00005803 env = os.environ.copy()
5804 env['PATH'] = (
5805 str(os.path.dirname(clang_format_tool)) + os.pathsep + env['PATH'])
5806 stdout = RunCommand(
5807 cmd, stdin=diff_output, cwd=top_dir, env=env,
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00005808 shell=sys.platform.startswith('win32'))
Jamie Madill5e96ad12020-01-13 16:08:35 +00005809 if opts.diff:
5810 sys.stdout.write(stdout)
5811 if opts.dry_run and len(stdout) > 0:
5812 return_value = 2
5813
5814 return return_value
5815
5816
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005817def _RunRustFmt(opts, rust_diff_files, top_dir, upstream_commit):
5818 """Runs rustfmt. Just like _RunClangFormatDiff returns 2 to indicate that
5819 presubmit checks have failed (and returns 0 otherwise)."""
5820
5821 if not rust_diff_files:
5822 return 0
5823
5824 # Locate the rustfmt binary.
5825 try:
5826 rustfmt_tool = rustfmt.FindRustfmtToolInChromiumTree()
5827 except rustfmt.NotFoundError as e:
5828 DieWithError(e)
5829
Lukasz Anforowicz9696c7f2023-05-03 17:26:04 +00005830 # TODO(crbug.com/1440869): Support formatting only the changed lines
5831 # if `opts.full or settings.GetFormatFullByDefault()` is False.
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005832 cmd = [rustfmt_tool]
5833 if opts.dry_run:
5834 cmd.append('--check')
5835 cmd += rust_diff_files
5836 rustfmt_exitcode = subprocess2.call(cmd)
5837
5838 if opts.presubmit and rustfmt_exitcode != 0:
5839 return 2
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005840
5841 return 0
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005842
5843
Olivier Robin0a6b5442022-04-07 07:25:04 +00005844def _RunSwiftFormat(opts, swift_diff_files, top_dir, upstream_commit):
5845 """Runs swift-format. Just like _RunClangFormatDiff returns 2 to indicate
5846 that presubmit checks have failed (and returns 0 otherwise)."""
5847
5848 if not swift_diff_files:
5849 return 0
5850
5851 # Locate the swift-format binary.
5852 try:
5853 swift_format_tool = swift_format.FindSwiftFormatToolInChromiumTree()
5854 except swift_format.NotFoundError as e:
5855 DieWithError(e)
5856
5857 cmd = [swift_format_tool]
5858 if opts.dry_run:
Olivier Robin7f39e3d2022-04-28 08:20:49 +00005859 cmd += ['lint', '-s']
Olivier Robin0a6b5442022-04-07 07:25:04 +00005860 else:
5861 cmd += ['format', '-i']
5862 cmd += swift_diff_files
5863 swift_format_exitcode = subprocess2.call(cmd)
5864
5865 if opts.presubmit and swift_format_exitcode != 0:
5866 return 2
5867
5868 return 0
5869
5870
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005871def MatchingFileType(file_name, extensions):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005872 """Returns True if the file name ends with one of the given extensions."""
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005873 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005874
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005875
enne@chromium.org555cfe42014-01-29 18:21:39 +00005876@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005877@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005878def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005879 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005880 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005881 GN_EXTS = ['.gn', '.gni', '.typemap']
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005882 RUST_EXTS = ['.rs']
Olivier Robin0a6b5442022-04-07 07:25:04 +00005883 SWIFT_EXTS = ['.swift']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005884 parser.add_option('--full', action='store_true',
5885 help='Reformat the full content of all touched files')
Tomasz Åšniatowski58194462021-08-27 17:36:16 +00005886 parser.add_option('--upstream', help='Branch to check against')
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005887 parser.add_option('--dry-run', action='store_true',
5888 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005889 parser.add_option(
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00005890 '--no-clang-format',
5891 dest='clang_format',
5892 action='store_false',
5893 default=True,
5894 help='Disables formatting of various file types using clang-format.')
5895 parser.add_option(
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005896 '--python',
5897 action='store_true',
5898 default=None,
5899 help='Enables python formatting on all python files.')
5900 parser.add_option(
5901 '--no-python',
5902 action='store_true',
Garrett Beaty91a6f332020-01-06 16:57:24 +00005903 default=False,
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005904 help='Disables python formatting on all python files. '
Garrett Beaty91a6f332020-01-06 16:57:24 +00005905 'If neither --python or --no-python are set, python files that have a '
5906 '.style.yapf file in an ancestor directory will be formatted. '
5907 'It is an error to set both.')
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00005908 parser.add_option(
5909 '--js',
5910 action='store_true',
5911 help='Format javascript code with clang-format. '
5912 'Has no effect if --no-clang-format is set.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005913 parser.add_option('--diff', action='store_true',
5914 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005915 parser.add_option('--presubmit', action='store_true',
5916 help='Used when running the script from a presubmit.')
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005917
5918 parser.add_option('--rust-fmt',
5919 dest='use_rust_fmt',
5920 action='store_true',
5921 default=rustfmt.IsRustfmtSupported(),
5922 help='Enables formatting of Rust file types using rustfmt.')
5923 parser.add_option(
5924 '--no-rust-fmt',
5925 dest='use_rust_fmt',
5926 action='store_false',
5927 help='Disables formatting of Rust file types using rustfmt.')
5928
Olivier Robin0a6b5442022-04-07 07:25:04 +00005929 parser.add_option(
5930 '--swift-format',
5931 dest='use_swift_format',
5932 action='store_true',
Olivier Robin7f39e3d2022-04-28 08:20:49 +00005933 default=swift_format.IsSwiftFormatSupported(),
Olivier Robin0a6b5442022-04-07 07:25:04 +00005934 help='Enables formatting of Swift file types using swift-format '
5935 '(macOS host only).')
5936 parser.add_option(
5937 '--no-swift-format',
5938 dest='use_swift_format',
5939 action='store_false',
5940 help='Disables formatting of Swift file types using swift-format.')
5941
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005942 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005943
Garrett Beaty91a6f332020-01-06 16:57:24 +00005944 if opts.python is not None and opts.no_python:
5945 raise parser.error('Cannot set both --python and --no-python')
5946 if opts.no_python:
5947 opts.python = False
5948
Daniel Chengc55eecf2016-12-30 03:11:02 -08005949 # Normalize any remaining args against the current path, so paths relative to
5950 # the current directory are still resolved as expected.
5951 args = [os.path.join(os.getcwd(), arg) for arg in args]
5952
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005953 # git diff generates paths against the root of the repository. Change
5954 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005955 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005956 if rel_base_path:
5957 os.chdir(rel_base_path)
5958
digit@chromium.org29e47272013-05-17 17:01:46 +00005959 # Grab the merge-base commit, i.e. the upstream commit of the current
5960 # branch when it was created or the last time it was rebased. This is
5961 # to cover the case where the user may have called "git fetch origin",
5962 # moving the origin branch to a newer commit, but hasn't rebased yet.
5963 upstream_commit = None
Tomasz Åšniatowski58194462021-08-27 17:36:16 +00005964 upstream_branch = opts.upstream
5965 if not upstream_branch:
5966 cl = Changelist()
5967 upstream_branch = cl.GetUpstreamBranch()
digit@chromium.org29e47272013-05-17 17:01:46 +00005968 if upstream_branch:
5969 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5970 upstream_commit = upstream_commit.strip()
5971
5972 if not upstream_commit:
5973 DieWithError('Could not find base commit for this branch. '
5974 'Are you in detached state?')
5975
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005976 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5977 diff_output = RunGit(changed_files_cmd)
5978 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005979 # Filter out files deleted by this CL
5980 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005981
Andreas Haas417d89c2020-02-06 10:24:27 +00005982 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005983 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005984
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00005985 clang_diff_files = []
5986 if opts.clang_format:
5987 clang_diff_files = [
5988 x for x in diff_files if MatchingFileType(x, CLANG_EXTS)
5989 ]
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005990 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005991 rust_diff_files = [x for x in diff_files if MatchingFileType(x, RUST_EXTS)]
Olivier Robin0a6b5442022-04-07 07:25:04 +00005992 swift_diff_files = [x for x in diff_files if MatchingFileType(x, SWIFT_EXTS)]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005993 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005994
Edward Lesmes50da7702020-03-30 19:23:43 +00005995 top_dir = settings.GetRoot()
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005996
Jamie Madill5e96ad12020-01-13 16:08:35 +00005997 return_value = _RunClangFormatDiff(opts, clang_diff_files, top_dir,
5998 upstream_commit)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005999
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006000 if opts.use_rust_fmt:
6001 rust_fmt_return_value = _RunRustFmt(opts, rust_diff_files, top_dir,
6002 upstream_commit)
6003 if rust_fmt_return_value == 2:
6004 return_value = 2
6005
Olivier Robin0a6b5442022-04-07 07:25:04 +00006006 if opts.use_swift_format:
6007 if sys.platform != 'darwin':
6008 DieWithError('swift-format is only supported on macOS.')
6009 swift_format_return_value = _RunSwiftFormat(opts, swift_diff_files, top_dir,
6010 upstream_commit)
6011 if swift_format_return_value == 2:
6012 return_value = 2
6013
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006014 # Similar code to above, but using yapf on .py files rather than clang-format
6015 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00006016 py_explicitly_disabled = opts.python is not None and not opts.python
6017 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00006018 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
6019 yapf_tool = os.path.join(depot_tools_path, 'yapf')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006020
Aiden Bennerc08566e2018-10-03 17:52:42 +00006021 # Used for caching.
6022 yapf_configs = {}
6023 for f in python_diff_files:
6024 # Find the yapf style config for the current file, defaults to depot
6025 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00006026 _FindYapfConfigFile(f, yapf_configs, top_dir)
6027
6028 # Turn on python formatting by default if a yapf config is specified.
6029 # This breaks in the case of this repo though since the specified
6030 # style file is also the global default.
6031 if opts.python is None:
6032 filtered_py_files = []
6033 for f in python_diff_files:
6034 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
6035 filtered_py_files.append(f)
6036 else:
6037 filtered_py_files = python_diff_files
6038
6039 # Note: yapf still seems to fix indentation of the entire file
6040 # even if line ranges are specified.
6041 # See https://github.com/google/yapf/issues/499
6042 if not opts.full and filtered_py_files:
6043 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
6044
Brian Sheedyb4307d52019-12-02 19:18:17 +00006045 yapfignore_patterns = _GetYapfIgnorePatterns(top_dir)
6046 filtered_py_files = _FilterYapfIgnoredFiles(filtered_py_files,
6047 yapfignore_patterns)
Brian Sheedy59b06a82019-10-14 17:03:29 +00006048
Aiden Benner99b0ccb2018-11-20 19:53:31 +00006049 for f in filtered_py_files:
Andrew Grievefa40bfa2020-01-07 02:32:57 +00006050 yapf_style = _FindYapfConfigFile(f, yapf_configs, top_dir)
6051 # Default to pep8 if not .style.yapf is found.
6052 if not yapf_style:
6053 yapf_style = 'pep8'
Aiden Bennerc08566e2018-10-03 17:52:42 +00006054
Peter Wend9399922020-06-17 17:33:49 +00006055 with open(f, 'r') as py_f:
Andrew Grieveb9e694c2021-11-15 19:04:46 +00006056 if 'python2' in py_f.readline():
Peter Wend9399922020-06-17 17:33:49 +00006057 vpython_script = 'vpython'
Andrew Grieveb9e694c2021-11-15 19:04:46 +00006058 else:
6059 vpython_script = 'vpython3'
Peter Wend9399922020-06-17 17:33:49 +00006060
6061 cmd = [vpython_script, yapf_tool, '--style', yapf_style, f]
Aiden Bennerc08566e2018-10-03 17:52:42 +00006062
6063 has_formattable_lines = False
6064 if not opts.full:
6065 # Only run yapf over changed line ranges.
6066 for diff_start, diff_len in py_line_diffs[f]:
6067 diff_end = diff_start + diff_len - 1
6068 # Yapf errors out if diff_end < diff_start but this
6069 # is a valid line range diff for a removal.
6070 if diff_end >= diff_start:
6071 has_formattable_lines = True
6072 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
6073 # If all line diffs were removals we have nothing to format.
6074 if not has_formattable_lines:
6075 continue
6076
6077 if opts.diff or opts.dry_run:
6078 cmd += ['--diff']
6079 # Will return non-zero exit code if non-empty diff.
Edward Lesmesb7db1832020-06-22 20:22:27 +00006080 stdout = RunCommand(cmd,
6081 error_ok=True,
Josip Sokcevic673e8ed2021-10-27 23:46:18 +00006082 stderr=subprocess2.PIPE,
Edward Lesmesb7db1832020-06-22 20:22:27 +00006083 cwd=top_dir,
6084 shell=sys.platform.startswith('win32'))
Aiden Bennerc08566e2018-10-03 17:52:42 +00006085 if opts.diff:
6086 sys.stdout.write(stdout)
6087 elif len(stdout) > 0:
6088 return_value = 2
6089 else:
6090 cmd += ['-i']
Edward Lesmesb7db1832020-06-22 20:22:27 +00006091 RunCommand(cmd, cwd=top_dir, shell=sys.platform.startswith('win32'))
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006092
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006093 # Format GN build files. Always run on full build files for canonical form.
6094 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006095 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07006096 if opts.dry_run or opts.diff:
6097 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006098 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07006099 gn_ret = subprocess2.call(cmd + [gn_diff_file],
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00006100 shell=sys.platform.startswith('win'),
brettw4b8ed592016-08-05 16:19:12 -07006101 cwd=top_dir)
6102 if opts.dry_run and gn_ret == 2:
6103 return_value = 2 # Not formatted.
6104 elif opts.diff and gn_ret == 2:
6105 # TODO this should compute and print the actual diff.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006106 print('This change has GN build file diff for ' + gn_diff_file)
brettw4b8ed592016-08-05 16:19:12 -07006107 elif gn_ret != 0:
6108 # For non-dry run cases (and non-2 return values for dry-run), a
6109 # nonzero error code indicates a failure, probably because the file
6110 # doesn't parse.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006111 DieWithError('gn format failed on ' + gn_diff_file +
6112 '\nTry running `gn format` on this file manually.')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006113
Ilya Shermane081cbe2017-08-15 17:51:04 -07006114 # Skip the metrics formatting from the global presubmit hook. These files have
6115 # a separate presubmit hook that issues an error if the files need formatting,
6116 # whereas the top-level presubmit script merely issues a warning. Formatting
6117 # these files is somewhat slow, so it's important not to duplicate the work.
6118 if not opts.presubmit:
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006119 for diff_xml in GetDiffXMLs(diff_files):
6120 xml_dir = GetMetricsDir(diff_xml)
6121 if not xml_dir:
6122 continue
6123
Ilya Shermane081cbe2017-08-15 17:51:04 -07006124 tool_dir = os.path.join(top_dir, xml_dir)
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00006125 pretty_print_tool = os.path.join(tool_dir, 'pretty_print.py')
Fabrice de Gansecfab092022-09-15 20:59:01 +00006126 cmd = ['vpython3', pretty_print_tool, '--non-interactive']
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006127
6128 # If the XML file is histograms.xml or enums.xml, add the xml path to the
6129 # command as histograms/pretty_print.py now needs a relative path argument
6130 # after splitting the histograms into multiple directories.
6131 # For example, in tools/metrics/ukm, pretty-print could be run using:
6132 # $ python pretty_print.py
6133 # But in tools/metrics/histogrmas, pretty-print should be run with an
6134 # additional relative path argument, like:
Peter Kastingee088882021-08-03 17:57:00 +00006135 # $ python pretty_print.py metadata/UMA/histograms.xml
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006136 # $ python pretty_print.py enums.xml
6137
Weilun Shib92c4b72020-08-27 17:45:11 +00006138 if (diff_xml.endswith('histograms.xml') or diff_xml.endswith('enums.xml')
Weilun Shi4f50adb2023-01-17 20:43:17 +00006139 or diff_xml.endswith('histogram_suffixes_list.xml')):
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006140 cmd.append(diff_xml)
6141
Ilya Shermane081cbe2017-08-15 17:51:04 -07006142 if opts.dry_run or opts.diff:
6143 cmd.append('--diff')
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006144
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00006145 # TODO(isherman): Once this file runs only on Python 3.3+, drop the
6146 # `shell` param and instead replace `'vpython'` with
6147 # `shutil.which('frob')` above: https://stackoverflow.com/a/32799942
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006148 stdout = RunCommand(cmd,
6149 cwd=top_dir,
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00006150 shell=sys.platform.startswith('win32'))
Ilya Shermane081cbe2017-08-15 17:51:04 -07006151 if opts.diff:
6152 sys.stdout.write(stdout)
6153 if opts.dry_run and stdout:
6154 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05006155
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006156 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006157
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006158
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006159def GetDiffXMLs(diff_files):
6160 return [
6161 os.path.normpath(x) for x in diff_files if MatchingFileType(x, ['.xml'])
6162 ]
6163
6164
6165def GetMetricsDir(diff_xml):
Steven Holte2e664bf2017-04-21 13:10:47 -07006166 metrics_xml_dirs = [
6167 os.path.join('tools', 'metrics', 'actions'),
6168 os.path.join('tools', 'metrics', 'histograms'),
6169 os.path.join('tools', 'metrics', 'rappor'),
Ilya Shermanb67e60c2020-05-20 22:27:03 +00006170 os.path.join('tools', 'metrics', 'structured'),
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006171 os.path.join('tools', 'metrics', 'ukm'),
6172 ]
Steven Holte2e664bf2017-04-21 13:10:47 -07006173 for xml_dir in metrics_xml_dirs:
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006174 if diff_xml.startswith(xml_dir):
6175 return xml_dir
6176 return None
Steven Holte2e664bf2017-04-21 13:10:47 -07006177
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006178
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006179@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006180@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006181def CMDcheckout(parser, args):
Edward Lemurf38bc172019-09-03 21:02:13 +00006182 """Checks out a branch associated with a given Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006183 _, args = parser.parse_args(args)
6184
6185 if len(args) != 1:
6186 parser.print_help()
6187 return 1
6188
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00006189 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00006190 if not issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00006191 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006192
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00006193 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006194
Edward Lemur52969c92020-02-06 18:15:28 +00006195 output = RunGit(['config', '--local', '--get-regexp',
Edward Lesmes50da7702020-03-30 19:23:43 +00006196 r'branch\..*\.' + ISSUE_CONFIG_KEY],
Edward Lemur52969c92020-02-06 18:15:28 +00006197 error_ok=True)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006198
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006199 branches = []
Edward Lemur52969c92020-02-06 18:15:28 +00006200 for key, issue in [x.split() for x in output.splitlines()]:
6201 if issue == target_issue:
Edward Lesmes50da7702020-03-30 19:23:43 +00006202 branches.append(re.sub(r'branch\.(.*)\.' + ISSUE_CONFIG_KEY, r'\1', key))
Edward Lemur52969c92020-02-06 18:15:28 +00006203
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006204 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07006205 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006206 return 1
6207 if len(branches) == 1:
6208 RunGit(['checkout', branches[0]])
6209 else:
vapiera7fbd5a2016-06-16 09:17:49 -07006210 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006211 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07006212 print('%d: %s' % (i, branches[i]))
Edward Lesmesae3586b2020-03-23 21:21:14 +00006213 which = gclient_utils.AskForData('Choose by index: ')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006214 try:
6215 RunGit(['checkout', branches[int(which)]])
6216 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07006217 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006218 return 1
6219
6220 return 0
6221
6222
maruel@chromium.org29404b52014-09-08 22:58:00 +00006223def CMDlol(parser, args):
6224 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07006225 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00006226 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6227 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6228 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
Gavin Mak18f45d22020-12-04 21:45:10 +00006229 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')).decode('utf-8'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00006230 return 0
6231
6232
Josip Sokcevic0399e172022-03-21 23:11:51 +00006233def CMDversion(parser, args):
Josip Sokcevica9a7eec2023-03-10 03:54:52 +00006234 import utils
Josip Sokcevic0399e172022-03-21 23:11:51 +00006235 print(utils.depot_tools_version())
6236
6237
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006238class OptionParser(optparse.OptionParser):
6239 """Creates the option parse and add --verbose support."""
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00006240
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006241 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006242 optparse.OptionParser.__init__(
6243 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006244 self.add_option(
6245 '-v', '--verbose', action='count', default=0,
6246 help='Use 2 times for more debugging info')
6247
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006248 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00006249 try:
6250 return self._parse_args(args)
6251 finally:
6252 # Regardless of success or failure of args parsing, we want to report
6253 # metrics, but only after logging has been initialized (if parsing
6254 # succeeded).
6255 global settings
6256 settings = Settings()
6257
Edward Lesmes9c349062021-05-06 20:02:39 +00006258 if metrics.collector.config.should_collect_metrics:
Joanna Wangc5b38322023-03-15 20:38:46 +00006259 try:
6260 # GetViewVCUrl ultimately calls logging method.
6261 project_url = settings.GetViewVCUrl().strip('/+')
6262 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
6263 metrics.collector.add('project_urls', [project_url])
6264 except subprocess2.CalledProcessError:
6265 # Occurs when command is not executed in a git repository
6266 # We should not fail here. If the command needs to be executed
6267 # in a repo, it will be raised later.
6268 pass
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00006269
6270 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006271 # Create an optparse.Values object that will store only the actual passed
6272 # options, without the defaults.
6273 actual_options = optparse.Values()
6274 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
6275 # Create an optparse.Values object with the default options.
6276 options = optparse.Values(self.get_default_values().__dict__)
6277 # Update it with the options passed by the user.
6278 options._update_careful(actual_options.__dict__)
6279 # Store the options passed by the user in an _actual_options attribute.
6280 # We store only the keys, and not the values, since the values can contain
6281 # arbitrary information, which might be PII.
Edward Lemur79d4f992019-11-11 23:49:02 +00006282 metrics.collector.add('arguments', list(actual_options.__dict__.keys()))
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006283
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006284 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01006285 logging.basicConfig(
6286 level=levels[min(options.verbose, len(levels) - 1)],
6287 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6288 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00006289
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006290 return options, args
6291
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006292
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006293def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006294 if sys.hexversion < 0x02060000:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006295 print('\nYour Python version %s is unsupported, please upgrade.\n' %
vapiera7fbd5a2016-06-16 09:17:49 -07006296 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006297 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006298
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006299 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006300 dispatcher = subcommand.CommandDispatcher(__name__)
6301 try:
6302 return dispatcher.execute(OptionParser(), argv)
Edward Lemur5b929a42019-10-21 17:57:39 +00006303 except auth.LoginRequiredError as e:
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006304 DieWithError(str(e))
Edward Lemur79d4f992019-11-11 23:49:02 +00006305 except urllib.error.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006306 if e.code != 500:
6307 raise
6308 DieWithError(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006309 ('App Engine is misbehaving and returned HTTP %d, again. Keep faith '
Quinten Yearsleyc4202212019-07-22 23:34:40 +00006310 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006311 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006312
6313
6314if __name__ == '__main__':
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006315 # These affect sys.stdout, so do it outside of main() to simplify mocks in
6316 # the unit tests.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006317 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006318 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00006319 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00006320 sys.exit(main(sys.argv[1:]))