blob: 51a57085f85669314be03f976c6fd34a74935109 [file] [log] [blame]
Josip Sokcevic4de5dea2022-03-23 21:15:14 +00001#!/usr/bin/env vpython3
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002# Copyright (c) 2013 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00008"""A git-command for integrating reviews on Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
thakis@chromium.org3421c992014-11-02 02:20:32 +000012import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000013import collections
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010014import datetime
Brian Sheedyb4307d52019-12-02 19:18:17 +000015import fnmatch
Edward Lemur202c5592019-10-21 22:44:52 +000016import httplib2
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010017import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000018import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000020import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import optparse
22import os
23import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010024import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000025import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000026import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070027import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000029import time
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000030import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000031import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000032import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000034from third_party import colorama
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000035import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000036import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000037import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000038import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000039import gerrit_util
iannucci@chromium.org9e849272014-04-04 00:31:55 +000040import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000041import git_footers
Edward Lemur85153282020-02-14 22:06:29 +000042import git_new_branch
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000043import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000044import metrics_utils
Edward Lesmeseeca9c62020-11-20 00:00:17 +000045import owners_client
iannucci@chromium.org9e849272014-04-04 00:31:55 +000046import owners_finder
Lei Zhangb8c62cf2020-07-15 20:09:37 +000047import presubmit_canned_checks
Josip Sokcevic7958e302023-03-01 23:02:21 +000048import presubmit_support
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +000049import rustfmt
Josip Sokcevic7958e302023-03-01 23:02:21 +000050import scm
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000051import setup_color
Francois Dorayd42c6812017-05-30 15:10:20 -040052import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000053import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000054import subprocess2
Olivier Robin0a6b5442022-04-07 07:25:04 +000055import swift_format
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import watchlists
57
Josip Sokcevic7958e302023-03-01 23:02:21 +000058from lib import utils
Edward Lemur79d4f992019-11-11 23:49:02 +000059from third_party import six
60from six.moves import urllib
61
62
63if sys.version_info.major == 3:
64 basestring = (str,) # pylint: disable=redefined-builtin
65
Edward Lemurb9830242019-10-30 22:19:20 +000066
tandrii7400cf02016-06-21 08:48:07 -070067__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000068
Edward Lemur0f58ae42019-04-30 17:24:12 +000069# Traces for git push will be stored in a traces directory inside the
70# depot_tools checkout.
71DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
72TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
Edward Lemur227d5102020-02-25 23:45:35 +000073PRESUBMIT_SUPPORT = os.path.join(DEPOT_TOOLS, 'presubmit_support.py')
Edward Lemur0f58ae42019-04-30 17:24:12 +000074
75# When collecting traces, Git hashes will be reduced to 6 characters to reduce
76# the size after compression.
77GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
78# Used to redact the cookies from the gitcookies file.
79GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
80
Edward Lemurd4d1ba42019-09-20 21:46:37 +000081MAX_ATTEMPTS = 3
82
Edward Lemur1b52d872019-05-09 21:12:12 +000083# The maximum number of traces we will keep. Multiplied by 3 since we store
84# 3 files per trace.
85MAX_TRACES = 3 * 10
Edward Lemur5737f022019-05-17 01:24:00 +000086# Message to be displayed to the user to inform where to find the traces for a
87# git-cl upload execution.
Edward Lemur0f58ae42019-04-30 17:24:12 +000088TRACES_MESSAGE = (
Edward Lemur1b52d872019-05-09 21:12:12 +000089'\n'
Edward Lemur5737f022019-05-17 01:24:00 +000090'The traces of this git-cl execution have been recorded at:\n'
Edward Lemur1b52d872019-05-09 21:12:12 +000091' %(trace_name)s-traces.zip\n'
Edward Lemur5737f022019-05-17 01:24:00 +000092'Copies of your gitcookies file and git config have been recorded at:\n'
93' %(trace_name)s-git-info.zip\n')
Edward Lemur1b52d872019-05-09 21:12:12 +000094# Format of the message to be stored as part of the traces to give developers a
95# better context when they go through traces.
96TRACES_README_FORMAT = (
97'Date: %(now)s\n'
98'\n'
99'Change: https://%(gerrit_host)s/q/%(change_id)s\n'
100'Title: %(title)s\n'
101'\n'
102'%(description)s\n'
103'\n'
104'Execution time: %(execution_time)s\n'
105'Exit code: %(exit_code)s\n') + TRACES_MESSAGE
Edward Lemur0f58ae42019-04-30 17:24:12 +0000106
Aaron Gable1bc7bfe2016-12-19 10:08:14 -0800107POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
Henrique Ferreiroff249622019-11-28 23:19:29 +0000108DESCRIPTION_BACKUP_FILE = '.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +0000109REFS_THAT_ALIAS_TO_OTHER_REFS = {
Josip Sokcevic7e133ff2021-07-13 17:44:53 +0000110 'refs/remotes/origin/lkgr': 'refs/remotes/origin/main',
111 'refs/remotes/origin/lkcr': 'refs/remotes/origin/main',
rmistry@google.comc68112d2015-03-03 12:48:06 +0000112}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
Josip Sokcevicc39ab992020-09-24 20:09:15 +0000114DEFAULT_OLD_BRANCH = 'refs/remotes/origin/master'
115DEFAULT_NEW_BRANCH = 'refs/remotes/origin/main'
116
Joanna Wanga8db0cb2023-01-24 15:43:17 +0000117DEFAULT_BUILDBUCKET_HOST = 'cr-buildbucket.appspot.com'
118
thestig@chromium.org44202a22014-03-11 19:22:18 +0000119# Valid extensions for files we want to lint.
120DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
121DEFAULT_LINT_IGNORE_REGEX = r"$^"
122
Aiden Bennerc08566e2018-10-03 17:52:42 +0000123# File name for yapf style config files.
124YAPF_CONFIG_FILENAME = '.style.yapf'
125
Edward Lesmes50da7702020-03-30 19:23:43 +0000126# The issue, patchset and codereview server are stored on git config for each
127# branch under branch.<branch-name>.<config-key>.
128ISSUE_CONFIG_KEY = 'gerritissue'
129PATCHSET_CONFIG_KEY = 'gerritpatchset'
130CODEREVIEW_SERVER_CONFIG_KEY = 'gerritserver'
Gavin Makbe2e9262022-11-08 23:41:55 +0000131# When using squash workflow, _CMDUploadChange doesn't simply push the commit(s)
132# you make to Gerrit. Instead, it creates a new commit object that contains all
133# changes you've made, diffed against a parent/merge base.
134# This is the hash of the new squashed commit and you can find this on Gerrit.
135GERRIT_SQUASH_HASH_CONFIG_KEY = 'gerritsquashhash'
136# This is the latest uploaded local commit hash.
137LAST_UPLOAD_HASH_CONFIG_KEY = 'last-upload-hash'
Edward Lesmes50da7702020-03-30 19:23:43 +0000138
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000139# Shortcut since it quickly becomes repetitive.
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000140Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000141
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000142# Initialized in main()
143settings = None
144
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100145# Used by tests/git_cl_test.py to add extra logging.
146# Inside the weirdly failing test, add this:
147# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700148# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100149_IS_BEING_TESTED = False
150
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000151_GOOGLESOURCE = 'googlesource.com'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000152
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000153_KNOWN_GERRIT_TO_SHORT_URLS = {
154 'https://chrome-internal-review.googlesource.com': 'https://crrev.com/i',
155 'https://chromium-review.googlesource.com': 'https://crrev.com/c',
156}
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000157assert len(_KNOWN_GERRIT_TO_SHORT_URLS) == len(
158 set(_KNOWN_GERRIT_TO_SHORT_URLS.values())), 'must have unique values'
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000159
160
Joanna Wang18de1f62023-01-21 01:24:24 +0000161# Maximum number of branches in a stack that can be traversed and uploaded
162# at once. Picked arbitrarily.
163_MAX_STACKED_BRANCHES_UPLOAD = 20
164
165
Joanna Wang5051ffe2023-03-01 22:24:07 +0000166# Repo prefixes that are enrolled in the stacked changes dogfood.
167DOGFOOD_STACKED_CHANGES_REPOS = [
168 'chromium.googlesource.com/infra/',
169 'chrome-internal.googlesource.com/infra/'
170]
171
172
Josip Sokcevicf736cab2020-10-20 23:41:38 +0000173class GitPushError(Exception):
174 pass
175
176
Christopher Lamf732cd52017-01-24 12:40:11 +1100177def DieWithError(message, change_desc=None):
178 if change_desc:
179 SaveDescriptionBackup(change_desc)
Josip Sokcevic953278a2020-02-28 19:46:36 +0000180 print('\n ** Content of CL description **\n' +
181 '='*72 + '\n' +
182 change_desc.description + '\n' +
183 '='*72 + '\n')
Christopher Lamf732cd52017-01-24 12:40:11 +1100184
vapiera7fbd5a2016-06-16 09:17:49 -0700185 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000186 sys.exit(1)
187
188
Christopher Lamf732cd52017-01-24 12:40:11 +1100189def SaveDescriptionBackup(change_desc):
Henrique Ferreiro5ae48172019-11-29 16:14:42 +0000190 backup_path = os.path.join(DEPOT_TOOLS, DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000191 print('\nsaving CL description to %s\n' % backup_path)
sokcevic07152802021-08-18 00:06:34 +0000192 with open(backup_path, 'wb') as backup_file:
193 backup_file.write(change_desc.description.encode('utf-8'))
Christopher Lamf732cd52017-01-24 12:40:11 +1100194
195
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000196def GetNoGitPagerEnv():
197 env = os.environ.copy()
198 # 'cat' is a magical git string that disables pagers on all platforms.
199 env['GIT_PAGER'] = 'cat'
200 return env
201
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000202
bsep@chromium.org627d9002016-04-29 00:00:52 +0000203def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000204 try:
Edward Lemur79d4f992019-11-11 23:49:02 +0000205 stdout = subprocess2.check_output(args, shell=shell, **kwargs)
206 return stdout.decode('utf-8', 'replace')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000207 except subprocess2.CalledProcessError as e:
208 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000209 if not error_ok:
Alan Cutter594fd332020-07-21 23:55:27 +0000210 message = error_message or e.stdout.decode('utf-8', 'replace') or ''
211 DieWithError('Command "%s" failed.\n%s' % (' '.join(args), message))
Josip Sokcevic673e8ed2021-10-27 23:46:18 +0000212 out = e.stdout.decode('utf-8', 'replace')
213 if e.stderr:
214 out += e.stderr.decode('utf-8', 'replace')
215 return out
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000216
217
218def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000219 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000220 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000221
222
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000223def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000224 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700225 if suppress_stderr:
Edward Lesmescf06cad2020-12-14 22:03:23 +0000226 stderr = subprocess2.DEVNULL
tandrii5d48c322016-08-18 16:19:37 -0700227 else:
228 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000229 try:
tandrii5d48c322016-08-18 16:19:37 -0700230 (out, _), code = subprocess2.communicate(['git'] + args,
231 env=GetNoGitPagerEnv(),
232 stdout=subprocess2.PIPE,
233 stderr=stderr)
Edward Lemur79d4f992019-11-11 23:49:02 +0000234 return code, out.decode('utf-8', 'replace')
tandrii5d48c322016-08-18 16:19:37 -0700235 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900236 logging.debug('Failed running %s', ['git'] + args)
Edward Lemur79d4f992019-11-11 23:49:02 +0000237 return e.returncode, e.stdout.decode('utf-8', 'replace')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000238
239
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000240def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000241 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000242 return RunGitWithCode(args, suppress_stderr=True)[1]
243
244
tandrii2a16b952016-10-19 07:09:44 -0700245def time_sleep(seconds):
246 # Use this so that it can be mocked in tests without interfering with python
247 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700248 return time.sleep(seconds)
249
250
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000251def time_time():
252 # Use this so that it can be mocked in tests without interfering with python
253 # system machinery.
254 return time.time()
255
256
Edward Lemur1b52d872019-05-09 21:12:12 +0000257def datetime_now():
258 # Use this so that it can be mocked in tests without interfering with python
259 # system machinery.
260 return datetime.datetime.now()
261
262
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100263def confirm_or_exit(prefix='', action='confirm'):
264 """Asks user to press enter to continue or press Ctrl+C to abort."""
265 if not prefix or prefix.endswith('\n'):
266 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100267 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100268 mid = ' Press'
269 elif prefix.endswith(' '):
270 mid = 'press'
271 else:
272 mid = ' press'
Edward Lesmesae3586b2020-03-23 21:21:14 +0000273 gclient_utils.AskForData(
274 '%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100275
276
277def ask_for_explicit_yes(prompt):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000278 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
Edward Lesmesae3586b2020-03-23 21:21:14 +0000279 result = gclient_utils.AskForData(prompt + ' [Yes/No]: ').lower()
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100280 while True:
281 if 'yes'.startswith(result):
282 return True
283 if 'no'.startswith(result):
284 return False
Edward Lesmesae3586b2020-03-23 21:21:14 +0000285 result = gclient_utils.AskForData('Please, type yes or no: ').lower()
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100286
287
machenbach@chromium.org45453142015-09-15 08:45:22 +0000288def _get_properties_from_options(options):
Quinten Yearsleya19d3532019-09-30 21:54:39 +0000289 prop_list = getattr(options, 'properties', [])
290 properties = dict(x.split('=', 1) for x in prop_list)
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +0000291 for key, val in properties.items():
machenbach@chromium.org45453142015-09-15 08:45:22 +0000292 try:
293 properties[key] = json.loads(val)
294 except ValueError:
295 pass # If a value couldn't be evaluated, treat it as a string.
296 return properties
297
298
Edward Lemur4c707a22019-09-24 21:13:43 +0000299def _call_buildbucket(http, buildbucket_host, method, request):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000300 """Calls a buildbucket v2 method and returns the parsed json response."""
301 headers = {
302 'Accept': 'application/json',
303 'Content-Type': 'application/json',
304 }
305 request = json.dumps(request)
306 url = 'https://%s/prpc/buildbucket.v2.Builds/%s' % (buildbucket_host, method)
307
308 logging.info('POST %s with %s' % (url, request))
309
310 attempts = 1
311 time_to_sleep = 1
312 while True:
313 response, content = http.request(url, 'POST', body=request, headers=headers)
314 if response.status == 200:
315 return json.loads(content[4:])
316 if attempts >= MAX_ATTEMPTS or 400 <= response.status < 500:
317 msg = '%s error when calling POST %s with %s: %s' % (
318 response.status, url, request, content)
319 raise BuildbucketResponseException(msg)
320 logging.debug(
321 '%s error when calling POST %s with %s. '
322 'Sleeping for %d seconds and retrying...' % (
323 response.status, url, request, time_to_sleep))
324 time.sleep(time_to_sleep)
325 time_to_sleep *= 2
326 attempts += 1
327
328 assert False, 'unreachable'
329
330
Edward Lemur6215c792019-10-03 21:59:05 +0000331def _parse_bucket(raw_bucket):
332 legacy = True
333 project = bucket = None
334 if '/' in raw_bucket:
335 legacy = False
336 project, bucket = raw_bucket.split('/', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000337 # Assume luci.<project>.<bucket>.
Edward Lemur6215c792019-10-03 21:59:05 +0000338 elif raw_bucket.startswith('luci.'):
339 project, bucket = raw_bucket[len('luci.'):].split('.', 1)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000340 # Otherwise, assume prefix is also the project name.
Edward Lemur6215c792019-10-03 21:59:05 +0000341 elif '.' in raw_bucket:
342 project = raw_bucket.split('.')[0]
343 bucket = raw_bucket
344 # Legacy buckets.
Edward Lemur45768512020-03-02 19:03:14 +0000345 if legacy and project and bucket:
Edward Lemur6215c792019-10-03 21:59:05 +0000346 print('WARNING Please use %s/%s to specify the bucket.' % (project, bucket))
347 return project, bucket
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000348
349
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000350def _canonical_git_googlesource_host(host):
351 """Normalizes Gerrit hosts (with '-review') to Git host."""
352 assert host.endswith(_GOOGLESOURCE)
353 # Prefix doesn't include '.' at the end.
354 prefix = host[:-(1 + len(_GOOGLESOURCE))]
355 if prefix.endswith('-review'):
356 prefix = prefix[:-len('-review')]
357 return prefix + '.' + _GOOGLESOURCE
358
359
360def _canonical_gerrit_googlesource_host(host):
361 git_host = _canonical_git_googlesource_host(host)
362 prefix = git_host.split('.', 1)[0]
363 return prefix + '-review.' + _GOOGLESOURCE
364
365
366def _get_counterpart_host(host):
367 assert host.endswith(_GOOGLESOURCE)
368 git = _canonical_git_googlesource_host(host)
369 gerrit = _canonical_gerrit_googlesource_host(git)
370 return git if gerrit == host else gerrit
371
372
Quinten Yearsley777660f2020-03-04 23:37:06 +0000373def _trigger_tryjobs(changelist, jobs, options, patchset):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000374 """Sends a request to Buildbucket to trigger tryjobs for a changelist.
qyearsley1fdfcb62016-10-24 13:22:03 -0700375
376 Args:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000377 changelist: Changelist that the tryjobs are associated with.
Edward Lemur45768512020-03-02 19:03:14 +0000378 jobs: A list of (project, bucket, builder).
qyearsley1fdfcb62016-10-24 13:22:03 -0700379 options: Command-line options.
380 """
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000381 print('Scheduling jobs on:')
Edward Lemur45768512020-03-02 19:03:14 +0000382 for project, bucket, builder in jobs:
383 print(' %s/%s: %s' % (project, bucket, builder))
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000384 print('To see results here, run: git cl try-results')
385 print('To see results in browser, run: git cl web')
tandriide281ae2016-10-12 06:02:30 -0700386
Quinten Yearsley777660f2020-03-04 23:37:06 +0000387 requests = _make_tryjob_schedule_requests(changelist, jobs, options, patchset)
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000388 if not requests:
389 return
390
Edward Lemur5b929a42019-10-21 17:57:39 +0000391 http = auth.Authenticator().authorize(httplib2.Http())
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000392 http.force_exception_to_status_code = True
393
394 batch_request = {'requests': requests}
Joanna Wanga8db0cb2023-01-24 15:43:17 +0000395 batch_response = _call_buildbucket(http, DEFAULT_BUILDBUCKET_HOST, 'Batch',
396 batch_request)
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000397
398 errors = [
399 ' ' + response['error']['message']
400 for response in batch_response.get('responses', [])
401 if 'error' in response
402 ]
403 if errors:
404 raise BuildbucketResponseException(
405 'Failed to schedule builds for some bots:\n%s' % '\n'.join(errors))
406
407
Quinten Yearsley777660f2020-03-04 23:37:06 +0000408def _make_tryjob_schedule_requests(changelist, jobs, options, patchset):
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000409 """Constructs requests for Buildbucket to trigger tryjobs."""
Edward Lemurf0faf482019-09-25 20:40:17 +0000410 gerrit_changes = [changelist.GetGerritChange(patchset)]
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000411 shared_properties = {
412 'category': options.ensure_value('category', 'git_cl_try')
413 }
414 if options.ensure_value('clobber', False):
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000415 shared_properties['clobber'] = True
416 shared_properties.update(_get_properties_from_options(options) or {})
417
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000418 shared_tags = [{'key': 'user_agent', 'value': 'git_cl_try'}]
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000419 if options.ensure_value('retry_failed', False):
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000420 shared_tags.append({'key': 'retry_failed',
421 'value': '1'})
422
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000423 requests = []
Edward Lemur45768512020-03-02 19:03:14 +0000424 for (project, bucket, builder) in jobs:
425 properties = shared_properties.copy()
426 if 'presubmit' in builder.lower():
427 properties['dry_run'] = 'true'
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000428
Edward Lemur45768512020-03-02 19:03:14 +0000429 requests.append({
430 'scheduleBuild': {
431 'requestId': str(uuid.uuid4()),
432 'builder': {
433 'project': getattr(options, 'project', None) or project,
434 'bucket': bucket,
435 'builder': builder,
436 },
437 'gerritChanges': gerrit_changes,
438 'properties': properties,
439 'tags': [
440 {'key': 'builder', 'value': builder},
441 ] + shared_tags,
442 }
443 })
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000444
Quinten Yearsleyee8be8a2020-03-05 21:48:32 +0000445 if options.ensure_value('revision', None):
Josip Sokcevic9011a5b2021-02-12 18:59:44 +0000446 remote, remote_branch = changelist.GetRemoteBranch()
Edward Lemur45768512020-03-02 19:03:14 +0000447 requests[-1]['scheduleBuild']['gitilesCommit'] = {
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000448 'host': _canonical_git_googlesource_host(gerrit_changes[0]['host']),
Edward Lemur45768512020-03-02 19:03:14 +0000449 'project': gerrit_changes[0]['project'],
Josip Sokcevic9011a5b2021-02-12 18:59:44 +0000450 'id': options.revision,
451 'ref': GetTargetRef(remote, remote_branch, None)
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000452 }
Anthony Polito1a5fe232020-01-24 23:17:52 +0000453
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000454 return requests
kjellander@chromium.org44424542015-06-02 18:35:29 +0000455
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000456
Quinten Yearsley777660f2020-03-04 23:37:06 +0000457def _fetch_tryjobs(changelist, buildbucket_host, patchset=None):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000458 """Fetches tryjobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000459
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000460 Returns list of buildbucket.v2.Build with the try jobs for the changelist.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000461 """
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000462 fields = ['id', 'builder', 'status', 'createTime', 'tags']
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000463 request = {
464 'predicate': {
465 'gerritChanges': [changelist.GetGerritChange(patchset)],
466 },
467 'fields': ','.join('builds.*.' + field for field in fields),
468 }
tandrii221ab252016-10-06 08:12:04 -0700469
Edward Lemur5b929a42019-10-21 17:57:39 +0000470 authenticator = auth.Authenticator()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000471 if authenticator.has_cached_credentials():
472 http = authenticator.authorize(httplib2.Http())
473 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700474 print('Warning: Some results might be missing because %s' %
475 # Get the message on how to login.
Andrii Shyshkalov2517afd2021-01-19 17:07:43 +0000476 (str(auth.LoginRequiredError()),))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000477 http = httplib2.Http()
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000478 http.force_exception_to_status_code = True
479
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000480 response = _call_buildbucket(http, buildbucket_host, 'SearchBuilds', request)
481 return response.get('builds', [])
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000482
Edward Lemur45768512020-03-02 19:03:14 +0000483
Edward Lemur5b929a42019-10-21 17:57:39 +0000484def _fetch_latest_builds(changelist, buildbucket_host, latest_patchset=None):
Quinten Yearsley983111f2019-09-26 17:18:48 +0000485 """Fetches builds from the latest patchset that has builds (within
486 the last few patchsets).
487
488 Args:
Quinten Yearsley983111f2019-09-26 17:18:48 +0000489 changelist (Changelist): The CL to fetch builds for
490 buildbucket_host (str): Buildbucket host, e.g. "cr-buildbucket.appspot.com"
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000491 lastest_patchset(int|NoneType): the patchset to start fetching builds from.
492 If None (default), starts with the latest available patchset.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000493 Returns:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000494 A tuple (builds, patchset) where builds is a list of buildbucket.v2.Build,
495 and patchset is the patchset number where those builds came from.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000496 """
497 assert buildbucket_host
498 assert changelist.GetIssue(), 'CL must be uploaded first'
499 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000500 if latest_patchset is None:
501 assert changelist.GetMostRecentPatchset()
502 ps = changelist.GetMostRecentPatchset()
503 else:
504 assert latest_patchset > 0, latest_patchset
505 ps = latest_patchset
506
Quinten Yearsley983111f2019-09-26 17:18:48 +0000507 min_ps = max(1, ps - 5)
508 while ps >= min_ps:
Quinten Yearsley777660f2020-03-04 23:37:06 +0000509 builds = _fetch_tryjobs(changelist, buildbucket_host, patchset=ps)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000510 if len(builds):
511 return builds, ps
512 ps -= 1
513 return [], 0
514
515
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000516def _filter_failed_for_retry(all_builds):
517 """Returns a list of buckets/builders that are worth retrying.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000518
519 Args:
Quinten Yearsley777660f2020-03-04 23:37:06 +0000520 all_builds (list): Builds, in the format returned by _fetch_tryjobs,
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000521 i.e. a list of buildbucket.v2.Builds which includes status and builder
522 info.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000523
524 Returns:
Edward Lemur45768512020-03-02 19:03:14 +0000525 A dict {(proj, bucket): [builders]}. This is the same format accepted by
Quinten Yearsley777660f2020-03-04 23:37:06 +0000526 _trigger_tryjobs.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000527 """
Edward Lemur45768512020-03-02 19:03:14 +0000528 grouped = {}
529 for build in all_builds:
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000530 builder = build['builder']
Edward Lemur45768512020-03-02 19:03:14 +0000531 key = (builder['project'], builder['bucket'], builder['builder'])
532 grouped.setdefault(key, []).append(build)
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000533
Edward Lemur45768512020-03-02 19:03:14 +0000534 jobs = []
535 for (project, bucket, builder), builds in grouped.items():
536 if 'triggered' in builder:
537 print('WARNING: Not scheduling %s. Triggered bots require an initial job '
538 'from a parent. Please schedule a manual job for the parent '
539 'instead.')
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000540 continue
541 if any(b['status'] in ('STARTED', 'SCHEDULED') for b in builds):
542 # Don't retry if any are running.
543 continue
Edward Lemur45768512020-03-02 19:03:14 +0000544 # If builder had several builds, retry only if the last one failed.
545 # This is a bit different from CQ, which would re-use *any* SUCCESS-full
546 # build, but in case of retrying failed jobs retrying a flaky one makes
547 # sense.
548 builds = sorted(builds, key=lambda b: b['createTime'])
549 if builds[-1]['status'] not in ('FAILURE', 'INFRA_FAILURE'):
550 continue
551 # Don't retry experimental build previously triggered by CQ.
552 if any(t['key'] == 'cq_experimental' and t['value'] == 'true'
553 for t in builds[-1]['tags']):
554 continue
555 jobs.append((project, bucket, builder))
556
557 # Sort the jobs to make testing easier.
558 return sorted(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000559
560
Quinten Yearsley777660f2020-03-04 23:37:06 +0000561def _print_tryjobs(options, builds):
562 """Prints nicely result of _fetch_tryjobs."""
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000563 if not builds:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000564 print('No tryjobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000565 return
566
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000567 longest_builder = max(len(b['builder']['builder']) for b in builds)
568 name_fmt = '{builder:<%d}' % longest_builder
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000569 if options.print_master:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000570 longest_bucket = max(len(b['builder']['bucket']) for b in builds)
571 name_fmt = ('{bucket:>%d} ' % longest_bucket) + name_fmt
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000572
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000573 builds_by_status = {}
574 for b in builds:
575 builds_by_status.setdefault(b['status'], []).append({
576 'id': b['id'],
577 'name': name_fmt.format(
578 builder=b['builder']['builder'], bucket=b['builder']['bucket']),
579 })
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000580
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000581 sort_key = lambda b: (b['name'], b['id'])
582
583 def print_builds(title, builds, fmt=None, color=None):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000584 """Pop matching builds from `builds` dict and print them."""
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000585 if not builds:
586 return
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000587
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000588 fmt = fmt or '{name} https://ci.chromium.org/b/{id}'
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000589 if not options.color or color is None:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000590 colorize = lambda x: x
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000591 else:
592 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
593
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000594 print(colorize(title))
595 for b in sorted(builds, key=sort_key):
596 print(' ', colorize(fmt.format(**b)))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000597
598 total = len(builds)
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000599 print_builds(
600 'Successes:', builds_by_status.pop('SUCCESS', []), color=Fore.GREEN)
601 print_builds(
602 'Infra Failures:', builds_by_status.pop('INFRA_FAILURE', []),
603 color=Fore.MAGENTA)
604 print_builds('Failures:', builds_by_status.pop('FAILURE', []), color=Fore.RED)
605 print_builds('Canceled:', builds_by_status.pop('CANCELED', []), fmt='{name}',
606 color=Fore.MAGENTA)
Andrii Shyshkalov792630c2020-10-19 16:47:44 +0000607 print_builds('Started:', builds_by_status.pop('STARTED', []),
608 color=Fore.YELLOW)
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000609 print_builds(
610 'Scheduled:', builds_by_status.pop('SCHEDULED', []), fmt='{name} id={id}')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000611 # The last section is just in case buildbucket API changes OR there is a bug.
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000612 print_builds(
613 'Other:', sum(builds_by_status.values(), []), fmt='{name} id={id}')
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000614 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000615
616
Aiden Bennerc08566e2018-10-03 17:52:42 +0000617def _ComputeDiffLineRanges(files, upstream_commit):
618 """Gets the changed line ranges for each file since upstream_commit.
619
620 Parses a git diff on provided files and returns a dict that maps a file name
621 to an ordered list of range tuples in the form (start_line, count).
622 Ranges are in the same format as a git diff.
623 """
624 # If files is empty then diff_output will be a full diff.
625 if len(files) == 0:
626 return {}
627
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000628 # Take the git diff and find the line ranges where there are changes.
Jamie Madill3671a6a2019-10-24 15:13:21 +0000629 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000630 diff_output = RunGit(diff_cmd)
631
632 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
633 # 2 capture groups
634 # 0 == fname of diff file
635 # 1 == 'diff_start,diff_count' or 'diff_start'
636 # will match each of
637 # diff --git a/foo.foo b/foo.py
638 # @@ -12,2 +14,3 @@
639 # @@ -12,2 +17 @@
640 # running re.findall on the above string with pattern will give
641 # [('foo.py', ''), ('', '14,3'), ('', '17')]
642
643 curr_file = None
644 line_diffs = {}
645 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
646 if match[0] != '':
647 # Will match the second filename in diff --git a/a.py b/b.py.
648 curr_file = match[0]
649 line_diffs[curr_file] = []
650 else:
651 # Matches +14,3
652 if ',' in match[1]:
653 diff_start, diff_count = match[1].split(',')
654 else:
655 # Single line changes are of the form +12 instead of +12,1.
656 diff_start = match[1]
657 diff_count = 1
658
659 diff_start = int(diff_start)
660 diff_count = int(diff_count)
661
662 # If diff_count == 0 this is a removal we can ignore.
663 line_diffs[curr_file].append((diff_start, diff_count))
664
665 return line_diffs
666
667
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000668def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000669 """Checks if a yapf file is in any parent directory of fpath until top_dir.
670
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000671 Recursively checks parent directories to find yapf file and if no yapf file
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000672 is found returns None. Uses yapf_config_cache as a cache for previously found
673 configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000674 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000675 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000676 # Return result if we've already computed it.
677 if fpath in yapf_config_cache:
678 return yapf_config_cache[fpath]
679
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000680 parent_dir = os.path.dirname(fpath)
681 if os.path.isfile(fpath):
682 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000683 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000684 # Otherwise fpath is a directory
685 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
686 if os.path.isfile(yapf_file):
687 ret = yapf_file
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +0000688 elif fpath in (top_dir, parent_dir):
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000689 # If we're at the top level directory, or if we're at root
690 # there is no provided style.
691 ret = None
692 else:
693 # Otherwise recurse on the current directory.
694 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000695 yapf_config_cache[fpath] = ret
696 return ret
697
698
Brian Sheedyb4307d52019-12-02 19:18:17 +0000699def _GetYapfIgnorePatterns(top_dir):
700 """Returns all patterns in the .yapfignore file.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000701
702 yapf is supposed to handle the ignoring of files listed in .yapfignore itself,
703 but this functionality appears to break when explicitly passing files to
704 yapf for formatting. According to
Josip Sokcevicc39ab992020-09-24 20:09:15 +0000705 https://github.com/google/yapf/blob/HEAD/README.rst#excluding-files-from-formatting-yapfignore,
Brian Sheedy59b06a82019-10-14 17:03:29 +0000706 the .yapfignore file should be in the directory that yapf is invoked from,
707 which we assume to be the top level directory in this case.
708
709 Args:
710 top_dir: The top level directory for the repository being formatted.
711
712 Returns:
Brian Sheedyb4307d52019-12-02 19:18:17 +0000713 A set of all fnmatch patterns to be ignored.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000714 """
715 yapfignore_file = os.path.join(top_dir, '.yapfignore')
Brian Sheedyb4307d52019-12-02 19:18:17 +0000716 ignore_patterns = set()
Brian Sheedy59b06a82019-10-14 17:03:29 +0000717 if not os.path.exists(yapfignore_file):
Brian Sheedyb4307d52019-12-02 19:18:17 +0000718 return ignore_patterns
Brian Sheedy59b06a82019-10-14 17:03:29 +0000719
Anthony Politoc64e3902021-04-30 21:55:25 +0000720 for line in gclient_utils.FileRead(yapfignore_file).split('\n'):
721 stripped_line = line.strip()
722 # Comments and blank lines should be ignored.
723 if stripped_line.startswith('#') or stripped_line == '':
724 continue
725 ignore_patterns.add(stripped_line)
Brian Sheedyb4307d52019-12-02 19:18:17 +0000726 return ignore_patterns
727
728
729def _FilterYapfIgnoredFiles(filepaths, patterns):
730 """Filters out any filepaths that match any of the given patterns.
731
732 Args:
733 filepaths: An iterable of strings containing filepaths to filter.
734 patterns: An iterable of strings containing fnmatch patterns to filter on.
735
736 Returns:
737 A list of strings containing all the elements of |filepaths| that did not
738 match any of the patterns in |patterns|.
739 """
740 # Not inlined so that tests can use the same implementation.
741 return [f for f in filepaths
742 if not any(fnmatch.fnmatch(f, p) for p in patterns)]
Brian Sheedy59b06a82019-10-14 17:03:29 +0000743
744
Aaron Gable13101a62018-02-09 13:20:41 -0800745def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000746 """Prints statistics about the change to the user."""
747 # --no-ext-diff is broken in some versions of Git, so try to work around
748 # this by overriding the environment (but there is still a problem if the
749 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000750 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000751 if 'GIT_EXTERNAL_DIFF' in env:
752 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000753
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000754 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800755 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
Edward Lemur0db01f02019-11-12 22:01:51 +0000756 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000757
758
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000759class BuildbucketResponseException(Exception):
760 pass
761
762
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000763class Settings(object):
764 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000765 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000766 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000767 self.tree_status_url = None
768 self.viewvc_url = None
769 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000770 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000771 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000772 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000773 self.git_editor = None
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000774 self.format_full_by_default = None
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +0000775 self.is_status_commit_order_by_date = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000776
Edward Lemur26964072020-02-19 19:18:51 +0000777 def _LazyUpdateIfNeeded(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000778 """Updates the settings from a codereview.settings file, if available."""
Edward Lemur26964072020-02-19 19:18:51 +0000779 if self.updated:
780 return
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000781
Edward Lemur26964072020-02-19 19:18:51 +0000782 # The only value that actually changes the behavior is
783 # autoupdate = "false". Everything else means "true".
784 autoupdate = (
785 scm.GIT.GetConfig(self.GetRoot(), 'rietveld.autoupdate', '').lower())
786
787 cr_settings_file = FindCodereviewSettingsFile()
788 if autoupdate != 'false' and cr_settings_file:
789 LoadCodereviewSettingsFromFile(cr_settings_file)
790 cr_settings_file.close()
791
792 self.updated = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000793
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000794 @staticmethod
795 def GetRelativeRoot():
Edward Lesmes50da7702020-03-30 19:23:43 +0000796 return scm.GIT.GetCheckoutRoot('.')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000797
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000798 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000799 if self.root is None:
800 self.root = os.path.abspath(self.GetRelativeRoot())
801 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000802
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000803 def GetTreeStatusUrl(self, error_ok=False):
804 if not self.tree_status_url:
Edward Lemur26964072020-02-19 19:18:51 +0000805 self.tree_status_url = self._GetConfig('rietveld.tree-status-url')
806 if self.tree_status_url is None and not error_ok:
807 DieWithError(
808 'You must configure your tree status URL by running '
809 '"git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000810 return self.tree_status_url
811
812 def GetViewVCUrl(self):
813 if not self.viewvc_url:
Edward Lemur26964072020-02-19 19:18:51 +0000814 self.viewvc_url = self._GetConfig('rietveld.viewvc-url')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000815 return self.viewvc_url
816
rmistry@google.com90752582014-01-14 21:04:50 +0000817 def GetBugPrefix(self):
Edward Lemur26964072020-02-19 19:18:51 +0000818 return self._GetConfig('rietveld.bug-prefix')
rmistry@google.com78948ed2015-07-08 23:09:57 +0000819
rmistry@google.com5626a922015-02-26 14:03:30 +0000820 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000821 run_post_upload_hook = self._GetConfig(
Edward Lemur26964072020-02-19 19:18:51 +0000822 'rietveld.run-post-upload-hook')
rmistry@google.com5626a922015-02-26 14:03:30 +0000823 return run_post_upload_hook == "True"
824
Joanna Wangc8f23e22023-01-19 21:18:10 +0000825 def GetDefaultCCList(self):
826 return self._GetConfig('rietveld.cc')
827
Dirk Pranke6f0df682021-06-25 00:42:33 +0000828 def GetUsePython3(self):
829 return self._GetConfig('rietveld.use-python3')
830
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000831 def GetSquashGerritUploads(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000832 """Returns True if uploads to Gerrit should be squashed by default."""
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000833 if self.squash_gerrit_uploads is None:
Edward Lesmes4de54132020-05-05 19:41:33 +0000834 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
835 if self.squash_gerrit_uploads is None:
Edward Lemur26964072020-02-19 19:18:51 +0000836 # Default is squash now (http://crbug.com/611892#c23).
837 self.squash_gerrit_uploads = self._GetConfig(
838 'gerrit.squash-uploads').lower() != 'false'
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000839 return self.squash_gerrit_uploads
840
Edward Lesmes4de54132020-05-05 19:41:33 +0000841 def GetSquashGerritUploadsOverride(self):
842 """Return True or False if codereview.settings should be overridden.
843
844 Returns None if no override has been defined.
845 """
846 # See also http://crbug.com/611892#c23
847 result = self._GetConfig('gerrit.override-squash-uploads').lower()
848 if result == 'true':
849 return True
850 if result == 'false':
851 return False
852 return None
853
Aleksey Khoroshilov35ef5ad2022-06-03 18:29:25 +0000854 def GetIsGerrit(self):
855 """Return True if gerrit.host is set."""
856 if self.is_gerrit is None:
857 self.is_gerrit = bool(self._GetConfig('gerrit.host', False))
858 return self.is_gerrit
859
tandrii@chromium.org28253532016-04-14 13:46:56 +0000860 def GetGerritSkipEnsureAuthenticated(self):
861 """Return True if EnsureAuthenticated should not be done for Gerrit
862 uploads."""
863 if self.gerrit_skip_ensure_authenticated is None:
Edward Lemur26964072020-02-19 19:18:51 +0000864 self.gerrit_skip_ensure_authenticated = self._GetConfig(
865 'gerrit.skip-ensure-authenticated').lower() == 'true'
tandrii@chromium.org28253532016-04-14 13:46:56 +0000866 return self.gerrit_skip_ensure_authenticated
867
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000868 def GetGitEditor(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000869 """Returns the editor specified in the git config, or None if none is."""
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000870 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000871 # Git requires single quotes for paths with spaces. We need to replace
872 # them with double quotes for Windows to treat such paths as a single
873 # path.
Edward Lemur26964072020-02-19 19:18:51 +0000874 self.git_editor = self._GetConfig('core.editor').replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000875 return self.git_editor or None
876
thestig@chromium.org44202a22014-03-11 19:22:18 +0000877 def GetLintRegex(self):
Edward Lemur26964072020-02-19 19:18:51 +0000878 return self._GetConfig('rietveld.cpplint-regex', DEFAULT_LINT_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000879
880 def GetLintIgnoreRegex(self):
Edward Lemur26964072020-02-19 19:18:51 +0000881 return self._GetConfig(
882 'rietveld.cpplint-ignore-regex', DEFAULT_LINT_IGNORE_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000883
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000884 def GetFormatFullByDefault(self):
885 if self.format_full_by_default is None:
Jamie Madillac6f6232021-07-07 20:54:08 +0000886 self._LazyUpdateIfNeeded()
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000887 result = (
888 RunGit(['config', '--bool', 'rietveld.format-full-by-default'],
889 error_ok=True).strip())
890 self.format_full_by_default = (result == 'true')
891 return self.format_full_by_default
892
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +0000893 def IsStatusCommitOrderByDate(self):
894 if self.is_status_commit_order_by_date is None:
895 result = (RunGit(['config', '--bool', 'cl.date-order'],
896 error_ok=True).strip())
897 self.is_status_commit_order_by_date = (result == 'true')
898 return self.is_status_commit_order_by_date
899
Edward Lemur26964072020-02-19 19:18:51 +0000900 def _GetConfig(self, key, default=''):
901 self._LazyUpdateIfNeeded()
902 return scm.GIT.GetConfig(self.GetRoot(), key, default)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000903
904
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000905class _CQState(object):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +0000906 """Enum for states of CL with respect to CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000907 NONE = 'none'
Greg Gutermanbe5fccd2021-06-14 17:58:20 +0000908 QUICK_RUN = 'quick_run'
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000909 DRY_RUN = 'dry_run'
910 COMMIT = 'commit'
911
Greg Gutermanbe5fccd2021-06-14 17:58:20 +0000912 ALL_STATES = [NONE, QUICK_RUN, DRY_RUN, COMMIT]
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000913
914
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000915class _ParsedIssueNumberArgument(object):
Edward Lemurf38bc172019-09-03 21:02:13 +0000916 def __init__(self, issue=None, patchset=None, hostname=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000917 self.issue = issue
918 self.patchset = patchset
919 self.hostname = hostname
920
921 @property
922 def valid(self):
923 return self.issue is not None
924
925
Edward Lemurf38bc172019-09-03 21:02:13 +0000926def ParseIssueNumberArgument(arg):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000927 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
928 fail_result = _ParsedIssueNumberArgument()
929
Edward Lemur678a6842019-10-03 22:25:05 +0000930 if isinstance(arg, int):
931 return _ParsedIssueNumberArgument(issue=arg)
932 if not isinstance(arg, basestring):
933 return fail_result
934
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000935 if arg.isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +0000936 return _ParsedIssueNumberArgument(issue=int(arg))
Aaron Gableaee6c852017-06-26 12:49:01 -0700937
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000938 url = gclient_utils.UpgradeToHttps(arg)
Alex Turner30ae6372022-01-04 02:32:52 +0000939 if not url.startswith('http'):
940 return fail_result
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000941 for gerrit_url, short_url in _KNOWN_GERRIT_TO_SHORT_URLS.items():
942 if url.startswith(short_url):
943 url = gerrit_url + url[len(short_url):]
944 break
945
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000946 try:
Edward Lemur79d4f992019-11-11 23:49:02 +0000947 parsed_url = urllib.parse.urlparse(url)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000948 except ValueError:
949 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +0200950
Alex Turner30ae6372022-01-04 02:32:52 +0000951 # If "https://" was automatically added, fail if `arg` looks unlikely to be a
952 # URL.
953 if not arg.startswith('http') and '.' not in parsed_url.netloc:
954 return fail_result
955
Edward Lemur678a6842019-10-03 22:25:05 +0000956 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
957 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
958 # Short urls like https://domain/<issue_number> can be used, but don't allow
959 # specifying the patchset (you'd 404), but we allow that here.
960 if parsed_url.path == '/':
961 part = parsed_url.fragment
962 else:
963 part = parsed_url.path
964
965 match = re.match(
966 r'(/c(/.*/\+)?)?/(?P<issue>\d+)(/(?P<patchset>\d+)?/?)?$', part)
967 if not match:
968 return fail_result
969
970 issue = int(match.group('issue'))
971 patchset = match.group('patchset')
972 return _ParsedIssueNumberArgument(
973 issue=issue,
974 patchset=int(patchset) if patchset else None,
975 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000976
977
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000978def _create_description_from_log(args):
979 """Pulls out the commit log to use as a base for the CL description."""
980 log_args = []
Bruce Dawson13acea32022-05-03 22:13:08 +0000981 if len(args) == 1 and args[0] == None:
982 # Handle the case where None is passed as the branch.
983 return ''
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000984 if len(args) == 1 and not args[0].endswith('.'):
985 log_args = [args[0] + '..']
986 elif len(args) == 1 and args[0].endswith('...'):
987 log_args = [args[0][:-1]]
988 elif len(args) == 2:
989 log_args = [args[0] + '..' + args[1]]
990 else:
991 log_args = args[:] # Hope for the best!
Manh Nguyene3644862020-08-05 18:25:46 +0000992 return RunGit(['log', '--pretty=format:%B%n'] + log_args)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000993
994
Aaron Gablea45ee112016-11-22 15:14:38 -0800995class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -0700996 def __init__(self, issue, url):
997 self.issue = issue
998 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -0800999 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001000
1001 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001002 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001003 self.issue, self.url)
1004
1005
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001006_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001007 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001008 # TODO(tandrii): these two aren't known in Gerrit.
1009 'approval', 'disapproval'])
1010
1011
Joanna Wang6215dd02023-02-07 15:58:03 +00001012# TODO(b/265929888): Change `parent` to `pushed_commit_base`.
Joanna Wange8523912023-01-21 02:05:40 +00001013_NewUpload = collections.namedtuple('NewUpload', [
Joanna Wang40497912023-01-24 21:18:16 +00001014 'reviewers', 'ccs', 'commit_to_push', 'new_last_uploaded_commit', 'parent',
Joanna Wang7603f042023-03-01 22:17:36 +00001015 'change_desc', 'prev_patchset'
Joanna Wange8523912023-01-21 02:05:40 +00001016])
1017
1018
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001019class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001020 """Changelist works with one changelist in local branch.
1021
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001022 Notes:
1023 * Not safe for concurrent multi-{thread,process} use.
1024 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001025 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001026 """
1027
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00001028 def __init__(self,
1029 branchref=None,
1030 issue=None,
1031 codereview_host=None,
1032 commit_date=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001033 """Create a new ChangeList instance.
1034
Edward Lemurf38bc172019-09-03 21:02:13 +00001035 **kwargs will be passed directly to Gerrit implementation.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001036 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001037 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001038 global settings
1039 if not settings:
1040 # Happens when git_cl.py is used as a utility library.
1041 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001042
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001043 self.branchref = branchref
1044 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001045 assert branchref.startswith('refs/heads/')
Edward Lemur85153282020-02-14 22:06:29 +00001046 self.branch = scm.GIT.ShortBranchName(self.branchref)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001047 else:
1048 self.branch = None
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00001049 self.commit_date = commit_date
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001050 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001051 self.lookedup_issue = False
1052 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001053 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001054 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001055 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001056 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001057 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001058 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001059 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001060
Edward Lemur125d60a2019-09-13 18:25:41 +00001061 # Lazily cached values.
1062 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1063 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Edward Lesmese1576912021-02-16 21:53:34 +00001064 self._owners_client = None
Edward Lemur125d60a2019-09-13 18:25:41 +00001065 # Map from change number (issue) to its detail cache.
1066 self._detail_cache = {}
1067
1068 if codereview_host is not None:
1069 assert not codereview_host.startswith('https://'), codereview_host
1070 self._gerrit_host = codereview_host
1071 self._gerrit_server = 'https://%s' % codereview_host
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001072
Edward Lesmese1576912021-02-16 21:53:34 +00001073 @property
1074 def owners_client(self):
1075 if self._owners_client is None:
1076 remote, remote_branch = self.GetRemoteBranch()
1077 branch = GetTargetRef(remote, remote_branch, None)
1078 self._owners_client = owners_client.GetCodeOwnersClient(
Edward Lesmese1576912021-02-16 21:53:34 +00001079 host=self.GetGerritHost(),
1080 project=self.GetGerritProject(),
1081 branch=branch)
1082 return self._owners_client
1083
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001084 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001085 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001086
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001087 The return value is a string suitable for passing to git cl with the --cc
1088 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001089 """
1090 if self.cc is None:
Joanna Wangc8f23e22023-01-19 21:18:10 +00001091 base_cc = settings.GetDefaultCCList()
1092 more_cc = ','.join(self.more_cc)
1093 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001094 return self.cc
1095
Daniel Cheng7227d212017-11-17 08:12:37 -08001096 def ExtendCC(self, more_cc):
1097 """Extends the list of users to cc on this CL based on the changed files."""
1098 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00001100 def GetCommitDate(self):
1101 """Returns the commit date as provided in the constructor"""
1102 return self.commit_date
1103
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001104 def GetBranch(self):
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001105 """Returns the short branch name, e.g. 'main'."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106 if not self.branch:
Edward Lemur85153282020-02-14 22:06:29 +00001107 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001108 if not branchref:
1109 return None
1110 self.branchref = branchref
Edward Lemur85153282020-02-14 22:06:29 +00001111 self.branch = scm.GIT.ShortBranchName(self.branchref)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001112 return self.branch
1113
1114 def GetBranchRef(self):
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001115 """Returns the full branch name, e.g. 'refs/heads/main'."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001116 self.GetBranch() # Poke the lazy loader.
1117 return self.branchref
1118
Edward Lemur85153282020-02-14 22:06:29 +00001119 def _GitGetBranchConfigValue(self, key, default=None):
1120 return scm.GIT.GetBranchConfig(
1121 settings.GetRoot(), self.GetBranch(), key, default)
tandrii5d48c322016-08-18 16:19:37 -07001122
Edward Lemur85153282020-02-14 22:06:29 +00001123 def _GitSetBranchConfigValue(self, key, value):
1124 action = 'set %s to %r' % (key, value)
1125 if not value:
1126 action = 'unset %s' % key
1127 assert self.GetBranch(), 'a branch is needed to ' + action
1128 return scm.GIT.SetBranchConfig(
1129 settings.GetRoot(), self.GetBranch(), key, value)
tandrii5d48c322016-08-18 16:19:37 -07001130
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001131 @staticmethod
1132 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001133 """Returns a tuple containing remote and remote ref,
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001134 e.g. 'origin', 'refs/heads/main'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001135 """
Edward Lemur15a9b8c2020-02-13 00:52:30 +00001136 remote, upstream_branch = scm.GIT.FetchUpstreamTuple(
1137 settings.GetRoot(), branch)
1138 if not remote or not upstream_branch:
1139 DieWithError(
1140 'Unable to determine default branch to diff against.\n'
Josip Sokcevicb038f722021-01-06 18:28:11 +00001141 'Verify this branch is set up to track another \n'
1142 '(via the --track argument to "git checkout -b ..."). \n'
1143 'or pass complete "git diff"-style arguments if supported, like\n'
1144 ' git cl upload origin/main\n')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145
1146 return remote, upstream_branch
1147
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001148 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001149 upstream_branch = self.GetUpstreamBranch()
Edward Lesmes50da7702020-03-30 19:23:43 +00001150 if not scm.GIT.IsValidRevision(settings.GetRoot(), upstream_branch):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001151 DieWithError('The upstream for the current branch (%s) does not exist '
1152 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001153 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001154 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001155
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156 def GetUpstreamBranch(self):
1157 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001158 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Raul Tambrefe1dbe12019-05-02 04:43:57 +00001159 if remote != '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001160 upstream_branch = upstream_branch.replace('refs/heads/',
1161 'refs/remotes/%s/' % remote)
1162 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1163 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001164 self.upstream_branch = upstream_branch
1165 return self.upstream_branch
1166
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001167 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001168 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001169 remote, branch = None, self.GetBranch()
1170 seen_branches = set()
1171 while branch not in seen_branches:
1172 seen_branches.add(branch)
1173 remote, branch = self.FetchUpstreamTuple(branch)
Edward Lemur85153282020-02-14 22:06:29 +00001174 branch = scm.GIT.ShortBranchName(branch)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001175 if remote != '.' or branch.startswith('refs/remotes'):
1176 break
1177 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001178 remotes = RunGit(['remote'], error_ok=True).split()
1179 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001180 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001181 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001182 remote = 'origin'
Gavin Make6a62332020-12-04 21:57:10 +00001183 logging.warning('Could not determine which remote this change is '
1184 'associated with, so defaulting to "%s".' %
1185 self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001186 else:
Gavin Make6a62332020-12-04 21:57:10 +00001187 logging.warning('Could not determine which remote this change is '
1188 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001189 branch = 'HEAD'
1190 if branch.startswith('refs/remotes'):
1191 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001192 elif branch.startswith('refs/branch-heads/'):
1193 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001194 else:
1195 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001196 return self._remote
1197
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001198 def GetRemoteUrl(self):
1199 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1200
1201 Returns None if there is no remote.
1202 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001203 is_cached, value = self._cached_remote_url
1204 if is_cached:
1205 return value
1206
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001207 remote, _ = self.GetRemoteBranch()
Edward Lemur26964072020-02-19 19:18:51 +00001208 url = scm.GIT.GetConfig(settings.GetRoot(), 'remote.%s.url' % remote, '')
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001209
Edward Lemur298f2cf2019-02-22 21:40:39 +00001210 # Check if the remote url can be parsed as an URL.
Edward Lemur79d4f992019-11-11 23:49:02 +00001211 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001212 if host:
1213 self._cached_remote_url = (True, url)
1214 return url
1215
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001216 # If it cannot be parsed as an url, assume it is a local directory,
1217 # probably a git cache.
Edward Lemur298f2cf2019-02-22 21:40:39 +00001218 logging.warning('"%s" doesn\'t appear to point to a git host. '
1219 'Interpreting it as a local directory.', url)
1220 if not os.path.isdir(url):
1221 logging.error(
Josip906bfde2020-01-31 22:38:49 +00001222 'Remote "%(remote)s" for branch "%(branch)s" points to "%(url)s", '
1223 'but it doesn\'t exist.',
1224 {'remote': remote, 'branch': self.GetBranch(), 'url': url})
Edward Lemur298f2cf2019-02-22 21:40:39 +00001225 return None
1226
1227 cache_path = url
Edward Lemur26964072020-02-19 19:18:51 +00001228 url = scm.GIT.GetConfig(url, 'remote.%s.url' % remote, '')
Edward Lemur298f2cf2019-02-22 21:40:39 +00001229
Edward Lemur79d4f992019-11-11 23:49:02 +00001230 host = urllib.parse.urlparse(url).netloc
Edward Lemur298f2cf2019-02-22 21:40:39 +00001231 if not host:
1232 logging.error(
1233 'Remote "%(remote)s" for branch "%(branch)s" points to '
1234 '"%(cache_path)s", but it is misconfigured.\n'
1235 '"%(cache_path)s" must be a git repo and must have a remote named '
1236 '"%(remote)s" pointing to the git host.', {
1237 'remote': remote,
1238 'cache_path': cache_path,
1239 'branch': self.GetBranch()})
1240 return None
1241
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001242 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001243 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001244
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001245 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001246 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001247 if self.issue is None and not self.lookedup_issue:
Bruce Dawson13acea32022-05-03 22:13:08 +00001248 if self.GetBranch():
1249 self.issue = self._GitGetBranchConfigValue(ISSUE_CONFIG_KEY)
Edward Lemur85153282020-02-14 22:06:29 +00001250 if self.issue is not None:
1251 self.issue = int(self.issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001252 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001253 return self.issue
1254
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00001255 def GetIssueURL(self, short=False):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001256 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001257 issue = self.GetIssue()
1258 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001259 return None
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00001260 server = self.GetCodereviewServer()
1261 if short:
1262 server = _KNOWN_GERRIT_TO_SHORT_URLS.get(server, server)
1263 return '%s/%s' % (server, issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001264
Dirk Pranke6f0df682021-06-25 00:42:33 +00001265 def GetUsePython3(self):
Josip Sokcevic340edc32021-07-08 17:01:46 +00001266 return settings.GetUsePython3()
Dirk Pranke6f0df682021-06-25 00:42:33 +00001267
Edward Lemur6c6827c2020-02-06 21:15:18 +00001268 def FetchDescription(self, pretty=False):
1269 assert self.GetIssue(), 'issue is required to query Gerrit'
1270
Edward Lemur9aa1a962020-02-25 00:58:38 +00001271 if self.description is None:
Edward Lemur6c6827c2020-02-06 21:15:18 +00001272 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
1273 current_rev = data['current_revision']
1274 self.description = data['revisions'][current_rev]['commit']['message']
Edward Lemur6c6827c2020-02-06 21:15:18 +00001275
1276 if not pretty:
1277 return self.description
1278
1279 # Set width to 72 columns + 2 space indent.
1280 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
1281 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1282 lines = self.description.splitlines()
1283 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001284
1285 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001286 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001287 if self.patchset is None and not self.lookedup_patchset:
Bruce Dawson13acea32022-05-03 22:13:08 +00001288 if self.GetBranch():
1289 self.patchset = self._GitGetBranchConfigValue(PATCHSET_CONFIG_KEY)
Edward Lemur85153282020-02-14 22:06:29 +00001290 if self.patchset is not None:
1291 self.patchset = int(self.patchset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001292 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293 return self.patchset
1294
Edward Lemur9aa1a962020-02-25 00:58:38 +00001295 def GetAuthor(self):
1296 return scm.GIT.GetConfig(settings.GetRoot(), 'user.email')
1297
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001299 """Set this branch's patchset. If patchset=0, clears the patchset."""
1300 assert self.GetBranch()
1301 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001302 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001303 else:
1304 self.patchset = int(patchset)
Edward Lesmes50da7702020-03-30 19:23:43 +00001305 self._GitSetBranchConfigValue(PATCHSET_CONFIG_KEY, str(self.patchset))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001306
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001307 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001308 """Set this branch's issue. If issue isn't given, clears the issue."""
1309 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001310 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001311 issue = int(issue)
Edward Lesmes50da7702020-03-30 19:23:43 +00001312 self._GitSetBranchConfigValue(ISSUE_CONFIG_KEY, str(issue))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001313 self.issue = issue
Edward Lemur125d60a2019-09-13 18:25:41 +00001314 codereview_server = self.GetCodereviewServer()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001315 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001316 self._GitSetBranchConfigValue(
Edward Lesmes50da7702020-03-30 19:23:43 +00001317 CODEREVIEW_SERVER_CONFIG_KEY, codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001318 else:
tandrii5d48c322016-08-18 16:19:37 -07001319 # Reset all of these just to be clean.
1320 reset_suffixes = [
Gavin Makbe2e9262022-11-08 23:41:55 +00001321 LAST_UPLOAD_HASH_CONFIG_KEY,
Edward Lesmes50da7702020-03-30 19:23:43 +00001322 ISSUE_CONFIG_KEY,
1323 PATCHSET_CONFIG_KEY,
1324 CODEREVIEW_SERVER_CONFIG_KEY,
Gavin Makbe2e9262022-11-08 23:41:55 +00001325 GERRIT_SQUASH_HASH_CONFIG_KEY,
Edward Lesmes50da7702020-03-30 19:23:43 +00001326 ]
tandrii5d48c322016-08-18 16:19:37 -07001327 for prop in reset_suffixes:
Edward Lemur85153282020-02-14 22:06:29 +00001328 try:
1329 self._GitSetBranchConfigValue(prop, None)
1330 except subprocess2.CalledProcessError:
1331 pass
Aaron Gableca01e2c2017-07-19 11:16:02 -07001332 msg = RunGit(['log', '-1', '--format=%B']).strip()
1333 if msg and git_footers.get_footer_change_id(msg):
1334 print('WARNING: The change patched into this branch has a Change-Id. '
1335 'Removing it.')
1336 RunGit(['commit', '--amend', '-m',
1337 git_footers.remove_footer(msg, 'Change-Id')])
Edward Lemurf38bc172019-09-03 21:02:13 +00001338 self.lookedup_issue = True
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001339 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001340 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001341
Joanna Wangb46232e2023-01-21 01:58:46 +00001342 def GetAffectedFiles(self, upstream, end_commit=None):
1343 # type: (str, Optional[str]) -> Sequence[str]
1344 """Returns the list of affected files for the given commit range."""
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001345 try:
Joanna Wangb46232e2023-01-21 01:58:46 +00001346 return [
1347 f for _, f in scm.GIT.CaptureStatus(
1348 settings.GetRoot(), upstream, end_commit=end_commit)
1349 ]
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001350 except subprocess2.CalledProcessError:
1351 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001352 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001353 'This branch probably doesn\'t exist anymore. To reset the\n'
1354 'tracking branch, please run\n'
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001355 ' git branch --set-upstream-to origin/main %s\n'
1356 'or replace origin/main with the relevant branch') %
Edward Lemur2c62b332020-03-12 22:12:33 +00001357 (upstream, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001358
dsansomee2d6fd92016-09-08 00:10:47 -07001359 def UpdateDescription(self, description, force=False):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001360 assert self.GetIssue(), 'issue is required to update description'
1361
1362 if gerrit_util.HasPendingChangeEdit(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001363 self.GetGerritHost(), self._GerritChangeIdentifier()):
Edward Lemur6c6827c2020-02-06 21:15:18 +00001364 if not force:
1365 confirm_or_exit(
1366 'The description cannot be modified while the issue has a pending '
1367 'unpublished edit. Either publish the edit in the Gerrit web UI '
1368 'or delete it.\n\n', action='delete the unpublished edit')
1369
1370 gerrit_util.DeletePendingChangeEdit(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001371 self.GetGerritHost(), self._GerritChangeIdentifier())
Edward Lemur6c6827c2020-02-06 21:15:18 +00001372 gerrit_util.SetCommitMessage(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001373 self.GetGerritHost(), self._GerritChangeIdentifier(),
Edward Lemur6c6827c2020-02-06 21:15:18 +00001374 description, notify='NONE')
1375
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001376 self.description = description
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001377
Edward Lemur75526302020-02-27 22:31:05 +00001378 def _GetCommonPresubmitArgs(self, verbose, upstream):
Edward Lemur227d5102020-02-25 23:45:35 +00001379 args = [
Edward Lemur227d5102020-02-25 23:45:35 +00001380 '--root', settings.GetRoot(),
1381 '--upstream', upstream,
1382 ]
1383
1384 args.extend(['--verbose'] * verbose)
1385
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001386 remote, remote_branch = self.GetRemoteBranch()
1387 target_ref = GetTargetRef(remote, remote_branch, None)
Aleksey Khoroshilov35ef5ad2022-06-03 18:29:25 +00001388 if settings.GetIsGerrit():
1389 args.extend(['--gerrit_url', self.GetCodereviewServer()])
1390 args.extend(['--gerrit_project', self.GetGerritProject()])
1391 args.extend(['--gerrit_branch', target_ref])
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001392
Edward Lemur99df04e2020-03-05 19:39:43 +00001393 author = self.GetAuthor()
Edward Lemur227d5102020-02-25 23:45:35 +00001394 issue = self.GetIssue()
1395 patchset = self.GetPatchset()
Edward Lemur99df04e2020-03-05 19:39:43 +00001396 if author:
1397 args.extend(['--author', author])
Edward Lemur227d5102020-02-25 23:45:35 +00001398 if issue:
1399 args.extend(['--issue', str(issue)])
1400 if patchset:
1401 args.extend(['--patchset', str(patchset)])
Edward Lemur227d5102020-02-25 23:45:35 +00001402
Edward Lemur75526302020-02-27 22:31:05 +00001403 return args
1404
Josip Sokcevic017544d2022-03-31 23:47:53 +00001405 def RunHook(self,
1406 committing,
1407 may_prompt,
1408 verbose,
1409 parallel,
1410 upstream,
1411 description,
1412 all_files,
1413 files=None,
1414 resultdb=False,
1415 realm=None):
Edward Lemur75526302020-02-27 22:31:05 +00001416 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1417 args = self._GetCommonPresubmitArgs(verbose, upstream)
1418 args.append('--commit' if committing else '--upload')
Edward Lemur227d5102020-02-25 23:45:35 +00001419 if may_prompt:
1420 args.append('--may_prompt')
1421 if parallel:
1422 args.append('--parallel')
1423 if all_files:
1424 args.append('--all_files')
Josip Sokcevic017544d2022-03-31 23:47:53 +00001425 if files:
1426 args.extend(files.split(';'))
1427 args.append('--source_controlled_only')
Bruce Dawson09c0c072022-05-26 20:28:58 +00001428 if files or all_files:
1429 args.append('--no_diffs')
Edward Lemur227d5102020-02-25 23:45:35 +00001430
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001431 if resultdb and not realm:
1432 # TODO (crbug.com/1113463): store realm somewhere and look it up so
1433 # it is not required to pass the realm flag
1434 print('Note: ResultDB reporting will NOT be performed because --realm'
1435 ' was not specified. To enable ResultDB, please run the command'
1436 ' again with the --realm argument to specify the LUCI realm.')
1437
Josip Sokcevic48d8e902023-03-09 02:38:26 +00001438 py3_results = self._RunPresubmit(args,
1439 description,
1440 use_python3=True,
1441 resultdb=resultdb,
1442 realm=realm)
1443 if py3_results.get('skipped_presubmits', 1) == 0:
1444 print('No more presubmits to run - skipping Python 2 presubmits.')
1445 return py3_results
1446
1447 py2_results = self._RunPresubmit(args,
1448 description,
1449 use_python3=False,
1450 resultdb=resultdb,
1451 realm=realm)
1452 return self._MergePresubmitResults(py2_results, py3_results)
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001453
Joanna Wanga8db0cb2023-01-24 15:43:17 +00001454 def _RunPresubmit(self,
1455 args,
1456 description,
1457 use_python3,
1458 resultdb=None,
1459 realm=None):
1460 # type: (Sequence[str], str, bool, Optional[bool], Optional[str]
1461 # ) -> Mapping[str, Any]
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001462 args = args[:]
1463 vpython = 'vpython3' if use_python3 else 'vpython'
1464
Edward Lemur227d5102020-02-25 23:45:35 +00001465 with gclient_utils.temporary_file() as description_file:
1466 with gclient_utils.temporary_file() as json_output:
Edward Lemur1a83da12020-03-04 21:18:36 +00001467 gclient_utils.FileWrite(description_file, description)
Edward Lemur227d5102020-02-25 23:45:35 +00001468 args.extend(['--json_output', json_output])
1469 args.extend(['--description_file', description_file])
Dirk Pranke6f0df682021-06-25 00:42:33 +00001470 if self.GetUsePython3():
1471 args.append('--use-python3')
Edward Lemur227d5102020-02-25 23:45:35 +00001472 start = time_time()
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001473 cmd = [vpython, PRESUBMIT_SUPPORT] + args
Saagar Sanghavi03b15132020-08-10 16:43:41 +00001474 if resultdb and realm:
1475 cmd = ['rdb', 'stream', '-new', '-realm', realm, '--'] + cmd
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001476
1477 p = subprocess2.Popen(cmd)
Edward Lemur227d5102020-02-25 23:45:35 +00001478 exit_code = p.wait()
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001479
Edward Lemur227d5102020-02-25 23:45:35 +00001480 metrics.collector.add_repeated('sub_commands', {
1481 'command': 'presubmit',
1482 'execution_time': time_time() - start,
1483 'exit_code': exit_code,
1484 })
1485
1486 if exit_code:
1487 sys.exit(exit_code)
1488
1489 json_results = gclient_utils.FileRead(json_output)
1490 return json.loads(json_results)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001491
Josip Sokcevic48d8e902023-03-09 02:38:26 +00001492 def _MergePresubmitResults(self, py2_results, py3_results):
1493 return {
1494 'more_cc': sorted(set(py2_results.get('more_cc', []) +
1495 py3_results.get('more_cc', []))),
1496 'errors': (
1497 py2_results.get('errors', []) + py3_results.get('errors', [])),
1498 'notifications': (
1499 py2_results.get('notifications', []) +
1500 py3_results.get('notifications', [])),
1501 'warnings': (
1502 py2_results.get('warnings', []) + py3_results.get('warnings', []))
1503 }
1504
Brian Sheedy7326ca22022-11-02 18:36:17 +00001505 def RunPostUploadHook(self, verbose, upstream, description, py3_only):
Edward Lemur75526302020-02-27 22:31:05 +00001506 args = self._GetCommonPresubmitArgs(verbose, upstream)
1507 args.append('--post_upload')
1508
1509 with gclient_utils.temporary_file() as description_file:
Edward Lemur1a83da12020-03-04 21:18:36 +00001510 gclient_utils.FileWrite(description_file, description)
Edward Lemur75526302020-02-27 22:31:05 +00001511 args.extend(['--description_file', description_file])
Josip Sokcevic48d8e902023-03-09 02:38:26 +00001512 if not py3_only:
1513 p_py2 = subprocess2.Popen(['vpython', PRESUBMIT_SUPPORT] + args)
1514 p_py3 = subprocess2.Popen(['vpython3', PRESUBMIT_SUPPORT] + args +
1515 ['--use-python3'])
1516 if not py3_only:
1517 p_py2.wait()
1518 p_py3.wait()
Edward Lemur75526302020-02-27 22:31:05 +00001519
Edward Lemur5a644f82020-03-18 16:44:57 +00001520 def _GetDescriptionForUpload(self, options, git_diff_args, files):
Joanna Wangb46232e2023-01-21 01:58:46 +00001521 # type: (optparse.Values, Sequence[str], Sequence[str]
1522 # ) -> ChangeDescription
1523 """Get description message for upload."""
Edward Lemur5a644f82020-03-18 16:44:57 +00001524 if self.GetIssue():
1525 description = self.FetchDescription()
1526 elif options.message:
1527 description = options.message
1528 else:
1529 description = _create_description_from_log(git_diff_args)
1530 if options.title and options.squash:
Edward Lesmes0dd54822020-03-26 18:24:25 +00001531 description = options.title + '\n\n' + description
Edward Lemur5a644f82020-03-18 16:44:57 +00001532
Edward Lemur5a644f82020-03-18 16:44:57 +00001533 bug = options.bug
1534 fixed = options.fixed
Josip Sokcevic340edc32021-07-08 17:01:46 +00001535 if not self.GetIssue():
1536 # Extract bug number from branch name, but only if issue is being created.
1537 # It must start with bug or fix, followed by _ or - and number.
1538 # Optionally, it may contain _ or - after number with arbitrary text.
1539 # Examples:
1540 # bug-123
1541 # bug_123
1542 # fix-123
1543 # fix-123-some-description
mlcui7a0b4cb2023-01-23 23:14:55 +00001544 branch = self.GetBranch()
1545 if branch is not None:
1546 match = re.match(
1547 r'^(?P<type>bug|fix(?:e[sd])?)[_-]?(?P<bugnum>\d+)([-_]|$)', branch)
1548 if not bug and not fixed and match:
1549 if match.group('type') == 'bug':
1550 bug = match.group('bugnum')
1551 else:
1552 fixed = match.group('bugnum')
Edward Lemur5a644f82020-03-18 16:44:57 +00001553
1554 change_description = ChangeDescription(description, bug, fixed)
1555
Joanna Wang39811b12023-01-20 23:09:48 +00001556 # Fill gaps in OWNERS coverage to reviewers if requested.
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001557 if options.add_owners_to:
Joanna Wang39811b12023-01-20 23:09:48 +00001558 assert options.add_owners_to in ('R'), options.add_owners_to
Edward Lesmese1576912021-02-16 21:53:34 +00001559 status = self.owners_client.GetFilesApprovalStatus(
Joanna Wang39811b12023-01-20 23:09:48 +00001560 files, [], options.reviewers)
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001561 missing_files = [
1562 f for f in files
Edward Lesmese1576912021-02-16 21:53:34 +00001563 if status[f] == self._owners_client.INSUFFICIENT_REVIEWERS
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001564 ]
Edward Lesmese1576912021-02-16 21:53:34 +00001565 owners = self.owners_client.SuggestOwners(
1566 missing_files, exclude=[self.GetAuthor()])
Joanna Wang39811b12023-01-20 23:09:48 +00001567 assert isinstance(options.reviewers, list), options.reviewers
1568 options.reviewers.extend(owners)
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001569
Edward Lemur5a644f82020-03-18 16:44:57 +00001570 # Set the reviewer list now so that presubmit checks can access it.
Joanna Wang39811b12023-01-20 23:09:48 +00001571 if options.reviewers:
1572 change_description.update_reviewers(options.reviewers)
Edward Lemur5a644f82020-03-18 16:44:57 +00001573
1574 return change_description
1575
Joanna Wanga1abbed2023-01-24 01:41:05 +00001576 def _GetTitleForUpload(self, options, multi_change_upload=False):
1577 # type: (optparse.Values, Optional[bool]) -> str
1578
1579 # Getting titles for multipl commits is not supported so we return the
1580 # default.
1581 if not options.squash or multi_change_upload or options.title:
Edward Lemur5a644f82020-03-18 16:44:57 +00001582 return options.title
1583
1584 # On first upload, patchset title is always this string, while options.title
1585 # gets converted to first line of message.
1586 if not self.GetIssue():
1587 return 'Initial upload'
1588
1589 # When uploading subsequent patchsets, options.message is taken as the title
1590 # if options.title is not provided.
Edward Lemur5a644f82020-03-18 16:44:57 +00001591 if options.message:
1592 return options.message.strip()
1593
1594 # Use the subject of the last commit as title by default.
Edward Lesmes50da7702020-03-30 19:23:43 +00001595 title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
Mario Bianuccicebfb4e2020-07-22 23:08:16 +00001596 if options.force or options.skip_title:
Edward Lemur5a644f82020-03-18 16:44:57 +00001597 return title
Edward Lesmesae3586b2020-03-23 21:21:14 +00001598 user_title = gclient_utils.AskForData('Title for patchset [%s]: ' % title)
mlcui3da91712021-05-05 10:00:30 +00001599
1600 # Use the default title if the user confirms the default with a 'y'.
1601 if user_title.lower() == 'y':
1602 return title
Edward Lesmesae3586b2020-03-23 21:21:14 +00001603 return user_title or title
Edward Lemur5a644f82020-03-18 16:44:57 +00001604
Joanna Wang562481d2023-01-26 21:57:14 +00001605 def _GetRefSpecOptions(self,
1606 options,
1607 change_desc,
1608 multi_change_upload=False,
1609 dogfood_path=False):
1610 # type: (optparse.Values, Sequence[Changelist], Optional[bool],
1611 # Optional[bool]) -> Sequence[str]
Joanna Wanga1abbed2023-01-24 01:41:05 +00001612
1613 # Extra options that can be specified at push time. Doc:
1614 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
1615 refspec_opts = []
1616
1617 # By default, new changes are started in WIP mode, and subsequent patchsets
1618 # don't send email. At any time, passing --send-mail or --send-email will
1619 # mark the change ready and send email for that particular patch.
1620 if options.send_mail:
1621 refspec_opts.append('ready')
1622 refspec_opts.append('notify=ALL')
Joanna Wang562481d2023-01-26 21:57:14 +00001623 elif (not self.GetIssue() and options.squash and not dogfood_path):
Joanna Wanga1abbed2023-01-24 01:41:05 +00001624 refspec_opts.append('wip')
1625 else:
1626 refspec_opts.append('notify=NONE')
1627
1628 # TODO(tandrii): options.message should be posted as a comment if
1629 # --send-mail or --send-email is set on non-initial upload as Rietveld used
1630 # to do it.
1631
1632 # Set options.title in case user was prompted in _GetTitleForUpload and
1633 # _CMDUploadChange needs to be called again.
1634 options.title = self._GetTitleForUpload(
1635 options, multi_change_upload=multi_change_upload)
1636
1637 if options.title:
1638 # Punctuation and whitespace in |title| must be percent-encoded.
1639 refspec_opts.append('m=' +
1640 gerrit_util.PercentEncodeForGitRef(options.title))
1641
1642 if options.private:
1643 refspec_opts.append('private')
1644
1645 if options.topic:
1646 # Documentation on Gerrit topics is here:
1647 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
1648 refspec_opts.append('topic=%s' % options.topic)
1649
1650 if options.enable_auto_submit:
1651 refspec_opts.append('l=Auto-Submit+1')
1652 if options.set_bot_commit:
1653 refspec_opts.append('l=Bot-Commit+1')
1654 if options.use_commit_queue:
1655 refspec_opts.append('l=Commit-Queue+2')
1656 elif options.cq_dry_run:
1657 refspec_opts.append('l=Commit-Queue+1')
1658 elif options.cq_quick_run:
1659 refspec_opts.append('l=Commit-Queue+1')
1660 refspec_opts.append('l=Quick-Run+1')
1661
1662 if change_desc.get_reviewers(tbr_only=True):
1663 score = gerrit_util.GetCodeReviewTbrScore(self.GetGerritHost(),
1664 self.GetGerritProject())
1665 refspec_opts.append('l=Code-Review+%s' % score)
1666
Joanna Wang40497912023-01-24 21:18:16 +00001667 # Gerrit sorts hashtags, so order is not important.
1668 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
1669 # We check GetIssue because we only add hashtags from the
1670 # description on the first upload.
Joanna Wang562481d2023-01-26 21:57:14 +00001671 # TODO(b/265929888): When we fully launch the new path:
1672 # 1) remove fetching hashtags from description alltogether
1673 # 2) Or use descrtiption hashtags for:
1674 # `not (self.GetIssue() and multi_change_upload)`
1675 # 3) Or enabled change description tags for multi and single changes
1676 # by adding them post `git push`.
1677 if not (self.GetIssue() and dogfood_path):
Joanna Wang40497912023-01-24 21:18:16 +00001678 hashtags.update(change_desc.get_hash_tags())
1679 refspec_opts.extend(['hashtag=%s' % t for t in hashtags])
Joanna Wang40497912023-01-24 21:18:16 +00001680
1681 # Note: Reviewers, and ccs are handled individually for each
Joanna Wanga1abbed2023-01-24 01:41:05 +00001682 # branch/change.
1683 return refspec_opts
1684
Joanna Wang6215dd02023-02-07 15:58:03 +00001685 def PrepareSquashedCommit(self, options, parent, end_commit=None):
1686 # type: (optparse.Values, str, Optional[str]) -> _NewUpload()
Joanna Wangb88a4342023-01-24 01:28:22 +00001687 """Create a squashed commit to upload."""
Joanna Wangb88a4342023-01-24 01:28:22 +00001688
1689 if end_commit is None:
1690 end_commit = RunGit(['rev-parse', self.branchref]).strip()
1691
1692 reviewers, ccs, change_desc = self._PrepareChange(options, parent,
1693 end_commit)
1694 latest_tree = RunGit(['rev-parse', end_commit + ':']).strip()
1695 with gclient_utils.temporary_file() as desc_tempfile:
1696 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
1697 commit_to_push = RunGit(
1698 ['commit-tree', latest_tree, '-p', parent, '-F',
1699 desc_tempfile]).strip()
1700
Joanna Wang7603f042023-03-01 22:17:36 +00001701 # Gerrit may or may not update fast enough to return the correct patchset
1702 # number after we push. Get the pre-upload patchset and increment later.
1703 prev_patchset = self.GetMostRecentPatchset(update=False) or 0
Joanna Wang40497912023-01-24 21:18:16 +00001704 return _NewUpload(reviewers, ccs, commit_to_push, end_commit, parent,
Joanna Wang7603f042023-03-01 22:17:36 +00001705 change_desc, prev_patchset)
Joanna Wangb88a4342023-01-24 01:28:22 +00001706
Joanna Wang6215dd02023-02-07 15:58:03 +00001707 def PrepareCherryPickSquashedCommit(self, options, parent):
1708 # type: (optparse.Values, str) -> _NewUpload()
Joanna Wange8523912023-01-21 02:05:40 +00001709 """Create a commit cherry-picked on parent to push."""
1710
Joanna Wang6215dd02023-02-07 15:58:03 +00001711 # The `parent` is what we will cherry-pick on top of.
1712 # The `cherry_pick_base` is the beginning range of what
1713 # we are cherry-picking.
1714 cherry_pick_base = self.GetCommonAncestorWithUpstream()
1715 reviewers, ccs, change_desc = self._PrepareChange(options, cherry_pick_base,
Joanna Wange8523912023-01-21 02:05:40 +00001716 self.branchref)
1717
1718 new_upload_hash = RunGit(['rev-parse', self.branchref]).strip()
1719 latest_tree = RunGit(['rev-parse', self.branchref + ':']).strip()
1720 with gclient_utils.temporary_file() as desc_tempfile:
1721 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
Joanna Wang6215dd02023-02-07 15:58:03 +00001722 commit_to_cp = RunGit([
1723 'commit-tree', latest_tree, '-p', cherry_pick_base, '-F',
1724 desc_tempfile
1725 ]).strip()
Joanna Wange8523912023-01-21 02:05:40 +00001726
Joanna Wang6215dd02023-02-07 15:58:03 +00001727 RunGit(['checkout', '-q', parent])
Joanna Wange8523912023-01-21 02:05:40 +00001728 ret, _out = RunGitWithCode(['cherry-pick', commit_to_cp])
1729 if ret:
1730 RunGit(['cherry-pick', '--abort'])
1731 RunGit(['checkout', '-q', self.branch])
1732 DieWithError('Could not cleanly cherry-pick')
1733
Joanna Wang6215dd02023-02-07 15:58:03 +00001734 commit_to_push = RunGit(['rev-parse', 'HEAD']).strip()
Joanna Wange8523912023-01-21 02:05:40 +00001735 RunGit(['checkout', '-q', self.branch])
1736
Joanna Wang7603f042023-03-01 22:17:36 +00001737 # Gerrit may or may not update fast enough to return the correct patchset
1738 # number after we push. Get the pre-upload patchset and increment later.
1739 prev_patchset = self.GetMostRecentPatchset(update=False) or 0
Joanna Wang6215dd02023-02-07 15:58:03 +00001740 return _NewUpload(reviewers, ccs, commit_to_push, new_upload_hash,
Joanna Wang7603f042023-03-01 22:17:36 +00001741 cherry_pick_base, change_desc, prev_patchset)
Joanna Wange8523912023-01-21 02:05:40 +00001742
Joanna Wangb46232e2023-01-21 01:58:46 +00001743 def _PrepareChange(self, options, parent, end_commit):
1744 # type: (optparse.Values, str, str) ->
1745 # Tuple[Sequence[str], Sequence[str], ChangeDescription]
1746 """Prepares the change to be uploaded."""
1747 self.EnsureCanUploadPatchset(options.force)
1748
1749 files = self.GetAffectedFiles(parent, end_commit=end_commit)
1750 change_desc = self._GetDescriptionForUpload(options, [parent, end_commit],
1751 files)
1752
1753 watchlist = watchlists.Watchlists(settings.GetRoot())
1754 self.ExtendCC(watchlist.GetWatchersForPaths(files))
1755 if not options.bypass_hooks:
1756 hook_results = self.RunHook(committing=False,
1757 may_prompt=not options.force,
1758 verbose=options.verbose,
1759 parallel=options.parallel,
1760 upstream=parent,
1761 description=change_desc.description,
1762 all_files=False)
1763 self.ExtendCC(hook_results['more_cc'])
1764
1765 # Update the change description and ensure we have a Change Id.
1766 if self.GetIssue():
1767 if options.edit_description:
1768 change_desc.prompt()
1769 change_detail = self._GetChangeDetail(['CURRENT_REVISION'])
1770 change_id = change_detail['change_id']
1771 change_desc.ensure_change_id(change_id)
1772
Joanna Wangb46232e2023-01-21 01:58:46 +00001773 else: # No change issue. First time uploading
1774 if not options.force and not options.message_file:
1775 change_desc.prompt()
1776
1777 # Check if user added a change_id in the descripiton.
1778 change_ids = git_footers.get_footer_change_id(change_desc.description)
1779 if len(change_ids) == 1:
1780 change_id = change_ids[0]
1781 else:
1782 change_id = GenerateGerritChangeId(change_desc.description)
1783 change_desc.ensure_change_id(change_id)
1784
1785 if options.preserve_tryjobs:
1786 change_desc.set_preserve_tryjobs()
1787
1788 SaveDescriptionBackup(change_desc)
1789
1790 # Add ccs
1791 ccs = []
Joanna Wangc4ac3022023-01-31 21:19:57 +00001792 # Add default, watchlist, presubmit ccs if this is the initial upload
Joanna Wangb46232e2023-01-21 01:58:46 +00001793 # and CL is not private and auto-ccing has not been disabled.
Joanna Wangc4ac3022023-01-31 21:19:57 +00001794 if not options.private and not options.no_autocc and not self.GetIssue():
Joanna Wangb46232e2023-01-21 01:58:46 +00001795 ccs = self.GetCCList().split(',')
1796 if len(ccs) > 100:
1797 lsc = ('https://chromium.googlesource.com/chromium/src/+/HEAD/docs/'
1798 'process/lsc/lsc_workflow.md')
1799 print('WARNING: This will auto-CC %s users.' % len(ccs))
1800 print('LSC may be more appropriate: %s' % lsc)
1801 print('You can also use the --no-autocc flag to disable auto-CC.')
1802 confirm_or_exit(action='continue')
1803
1804 # Add ccs from the --cc flag.
1805 if options.cc:
1806 ccs.extend(options.cc)
1807
1808 ccs = [email.strip() for email in ccs if email.strip()]
1809 if change_desc.get_cced():
1810 ccs.extend(change_desc.get_cced())
1811
1812 return change_desc.get_reviewers(), ccs, change_desc
1813
Joanna Wang40497912023-01-24 21:18:16 +00001814 def PostUploadUpdates(self, options, new_upload, change_number):
1815 # type: (optparse.Values, _NewUpload, change_number) -> None
1816 """Makes necessary post upload changes to the local and remote cl."""
1817 if not self.GetIssue():
1818 self.SetIssue(change_number)
1819
Joanna Wang7603f042023-03-01 22:17:36 +00001820 self.SetPatchset(new_upload.prev_patchset + 1)
1821
Joanna Wang40497912023-01-24 21:18:16 +00001822 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY,
1823 new_upload.commit_to_push)
1824 self._GitSetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY,
1825 new_upload.new_last_uploaded_commit)
1826
1827 if settings.GetRunPostUploadHook():
1828 self.RunPostUploadHook(options.verbose, new_upload.parent,
1829 new_upload.change_desc.description,
1830 options.no_python2_post_upload_hooks)
1831
1832 if new_upload.reviewers or new_upload.ccs:
1833 gerrit_util.AddReviewers(self.GetGerritHost(),
1834 self._GerritChangeIdentifier(),
1835 reviewers=new_upload.reviewers,
1836 ccs=new_upload.ccs,
1837 notify=bool(options.send_mail))
1838
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001839 def CMDUpload(self, options, git_diff_args, orig_args):
1840 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001841 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001842 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001843 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001844 else:
1845 if self.GetBranch() is None:
1846 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1847
1848 # Default to diffing against common ancestor of upstream branch
1849 base_branch = self.GetCommonAncestorWithUpstream()
1850 git_diff_args = [base_branch, 'HEAD']
1851
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001852 # Fast best-effort checks to abort before running potentially expensive
1853 # hooks if uploading is likely to fail anyway. Passing these checks does
1854 # not guarantee that uploading will not fail.
Edward Lemur125d60a2019-09-13 18:25:41 +00001855 self.EnsureAuthenticated(force=options.force)
1856 self.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001857
1858 # Apply watchlists on upload.
Edward Lemur2c62b332020-03-12 22:12:33 +00001859 watchlist = watchlists.Watchlists(settings.GetRoot())
1860 files = self.GetAffectedFiles(base_branch)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001861 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001862 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001863
Edward Lemur5a644f82020-03-18 16:44:57 +00001864 change_desc = self._GetDescriptionForUpload(options, git_diff_args, files)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001865 if not options.bypass_hooks:
Joanna Wanga8db0cb2023-01-24 15:43:17 +00001866 hook_results = self.RunHook(committing=False,
1867 may_prompt=not options.force,
1868 verbose=options.verbose,
1869 parallel=options.parallel,
1870 upstream=base_branch,
1871 description=change_desc.description,
1872 all_files=False)
Edward Lemur227d5102020-02-25 23:45:35 +00001873 self.ExtendCC(hook_results['more_cc'])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001874
Aaron Gable13101a62018-02-09 13:20:41 -08001875 print_stats(git_diff_args)
Edward Lemura12175c2020-03-09 16:58:26 +00001876 ret = self.CMDUploadChange(
Edward Lemur5a644f82020-03-18 16:44:57 +00001877 options, git_diff_args, custom_cl_base, change_desc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001878 if not ret:
mlcui7a0b4cb2023-01-23 23:14:55 +00001879 if self.GetBranch() is not None:
1880 self._GitSetBranchConfigValue(
1881 LAST_UPLOAD_HASH_CONFIG_KEY,
1882 scm.GIT.ResolveCommit(settings.GetRoot(), 'HEAD'))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001883 # Run post upload hooks, if specified.
1884 if settings.GetRunPostUploadHook():
Brian Sheedy7326ca22022-11-02 18:36:17 +00001885 self.RunPostUploadHook(options.verbose, base_branch,
1886 change_desc.description,
1887 options.no_python2_post_upload_hooks)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001888
1889 # Upload all dependencies if specified.
1890 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001891 print()
1892 print('--dependencies has been specified.')
1893 print('All dependent local branches will be re-uploaded.')
1894 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001895 # Remove the dependencies flag from args so that we do not end up in a
1896 # loop.
1897 orig_args.remove('--dependencies')
Jose Lopes3863fc52020-04-07 17:00:25 +00001898 ret = upload_branch_deps(self, orig_args, options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001899 return ret
1900
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001901 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001902 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001903
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00001904 Issue must have been already uploaded and known. Optionally allows for
1905 updating Quick-Run (QR) state.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001906 """
1907 assert new_state in _CQState.ALL_STATES
1908 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001909 try:
Edward Lemur125d60a2019-09-13 18:25:41 +00001910 vote_map = {
1911 _CQState.NONE: 0,
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00001912 _CQState.QUICK_RUN: 1,
Edward Lemur125d60a2019-09-13 18:25:41 +00001913 _CQState.DRY_RUN: 1,
1914 _CQState.COMMIT: 2,
1915 }
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00001916 if new_state == _CQState.QUICK_RUN:
1917 labels = {
1918 'Commit-Queue': vote_map[_CQState.DRY_RUN],
1919 'Quick-Run': vote_map[_CQState.QUICK_RUN],
1920 }
1921 else:
1922 labels = {'Commit-Queue': vote_map[new_state]}
Edward Lemur125d60a2019-09-13 18:25:41 +00001923 notify = False if new_state == _CQState.DRY_RUN else None
1924 gerrit_util.SetReview(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001925 self.GetGerritHost(), self._GerritChangeIdentifier(),
Edward Lemur125d60a2019-09-13 18:25:41 +00001926 labels=labels, notify=notify)
qyearsley1fdfcb62016-10-24 13:22:03 -07001927 return 0
1928 except KeyboardInterrupt:
1929 raise
1930 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001931 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001932 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001933 ' * Your project has no CQ,\n'
1934 ' * You don\'t have permission to change the CQ state,\n'
1935 ' * There\'s a bug in this code (see stack trace below).\n'
1936 'Consider specifying which bots to trigger manually or asking your '
1937 'project owners for permissions or contacting Chrome Infra at:\n'
1938 'https://www.chromium.org/infra\n\n' %
1939 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001940 # Still raise exception so that stack trace is printed.
1941 raise
1942
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001943 def GetGerritHost(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001944 # Lazy load of configs.
1945 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001946 if self._gerrit_host and '.' not in self._gerrit_host:
1947 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1948 # This happens for internal stuff http://crbug.com/614312.
Edward Lemur79d4f992019-11-11 23:49:02 +00001949 parsed = urllib.parse.urlparse(self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001950 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001951 print('WARNING: using non-https URLs for remote is likely broken\n'
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001952 ' Your current remote is: %s' % self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001953 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1954 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001955 return self._gerrit_host
1956
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001957 def _GetGitHost(self):
1958 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001959 remote_url = self.GetRemoteUrl()
1960 if not remote_url:
1961 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001962 return urllib.parse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001963
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001964 def GetCodereviewServer(self):
1965 if not self._gerrit_server:
1966 # If we're on a branch then get the server potentially associated
1967 # with that branch.
Edward Lemur85153282020-02-14 22:06:29 +00001968 if self.GetIssue() and self.GetBranch():
tandrii5d48c322016-08-18 16:19:37 -07001969 self._gerrit_server = self._GitGetBranchConfigValue(
Edward Lesmes50da7702020-03-30 19:23:43 +00001970 CODEREVIEW_SERVER_CONFIG_KEY)
tandrii5d48c322016-08-18 16:19:37 -07001971 if self._gerrit_server:
Edward Lemur79d4f992019-11-11 23:49:02 +00001972 self._gerrit_host = urllib.parse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001973 if not self._gerrit_server:
1974 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1975 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001976 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001977 parts[0] = parts[0] + '-review'
1978 self._gerrit_host = '.'.join(parts)
1979 self._gerrit_server = 'https://%s' % self._gerrit_host
1980 return self._gerrit_server
1981
Edward Lesmeseeca9c62020-11-20 00:00:17 +00001982 def GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001983 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001984 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001985 if remote_url is None:
Josip906bfde2020-01-31 22:38:49 +00001986 logging.warning('can\'t detect Gerrit project.')
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001987 return None
Edward Lemur79d4f992019-11-11 23:49:02 +00001988 project = urllib.parse.urlparse(remote_url).path.strip('/')
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001989 if project.endswith('.git'):
1990 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001991 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1992 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1993 # gitiles/git-over-https protocol. E.g.,
1994 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1995 # as
1996 # https://chromium.googlesource.com/v8/v8
1997 if project.startswith('a/'):
1998 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001999 return project
2000
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002001 def _GerritChangeIdentifier(self):
2002 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
2003
2004 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002005 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002006 """
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002007 project = self.GetGerritProject()
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002008 if project:
2009 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
2010 # Fall back on still unique, but less efficient change number.
2011 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002012
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002013 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002014 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002015 if settings.GetGerritSkipEnsureAuthenticated():
2016 # For projects with unusual authentication schemes.
2017 # See http://crbug.com/603378.
2018 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002019
2020 # Check presence of cookies only if using cookies-based auth method.
2021 cookie_auth = gerrit_util.Authenticator.get()
2022 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002023 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002024
Florian Mayerae510e82020-01-30 21:04:48 +00002025 remote_url = self.GetRemoteUrl()
2026 if remote_url is None:
Josip906bfde2020-01-31 22:38:49 +00002027 logging.warning('invalid remote')
Florian Mayerae510e82020-01-30 21:04:48 +00002028 return
Joanna Wang46ffd1b2022-09-16 20:44:44 +00002029 if urllib.parse.urlparse(remote_url).scheme not in ['https', 'sso']:
2030 logging.warning(
2031 'Ignoring branch %(branch)s with non-https/sso remote '
2032 '%(remote)s', {
2033 'branch': self.branch,
2034 'remote': self.GetRemoteUrl()
2035 })
Daniel Chengcf6269b2019-05-18 01:02:12 +00002036 return
2037
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002038 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002039 self.GetCodereviewServer()
2040 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00002041 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002042
2043 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2044 git_auth = cookie_auth.get_auth_header(git_host)
2045 if gerrit_auth and git_auth:
2046 if gerrit_auth == git_auth:
2047 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002048 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
Raul Tambre80ee78e2019-05-06 22:41:05 +00002049 print(
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002050 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002051 ' %s\n'
2052 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002053 ' Consider running the following command:\n'
2054 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002055 ' %s\n'
Raul Tambre80ee78e2019-05-06 22:41:05 +00002056 ' %s' %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002057 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002058 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002059 cookie_auth.get_new_password_message(git_host)))
2060 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002061 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002062 return
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002063
2064 missing = (
2065 ([] if gerrit_auth else [self._gerrit_host]) +
2066 ([] if git_auth else [git_host]))
2067 DieWithError('Credentials for the following hosts are required:\n'
2068 ' %s\n'
2069 'These are read from %s (or legacy %s)\n'
2070 '%s' % (
2071 '\n '.join(missing),
2072 cookie_auth.get_gitcookies_path(),
2073 cookie_auth.get_netrc_path(),
2074 cookie_auth.get_new_password_message(git_host)))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002075
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002076 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002077 if not self.GetIssue():
2078 return
2079
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002080 status = self._GetChangeDetail()['status']
Joanna Wang583ca662022-04-27 21:17:17 +00002081 if status == 'ABANDONED':
Bruce Dawsoneb8426e2022-08-05 23:58:15 +00002082 DieWithError(
2083 'Change %s has been abandoned, new uploads are not allowed' %
2084 (self.GetIssueURL()))
Joanna Wang583ca662022-04-27 21:17:17 +00002085 if status == 'MERGED':
2086 answer = gclient_utils.AskForData(
2087 'Change %s has been submitted, new uploads are not allowed. '
2088 'Would you like to start a new change (Y/n)?' % self.GetIssueURL()
2089 ).lower()
2090 if answer not in ('y', ''):
2091 DieWithError('New uploads are not allowed.')
2092 self.SetIssue()
2093 return
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002094
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002095 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2096 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2097 # Apparently this check is not very important? Otherwise get_auth_email
2098 # could have been added to other implementations of Authenticator.
2099 cookies_auth = gerrit_util.Authenticator.get()
2100 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002101 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002102
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002103 cookies_user = cookies_auth.get_auth_email(self.GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002104 if self.GetIssueOwner() == cookies_user:
2105 return
2106 logging.debug('change %s owner is %s, cookies user is %s',
2107 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002108 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002109 # so ask what Gerrit thinks of this user.
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002110 details = gerrit_util.GetAccountDetails(self.GetGerritHost(), 'self')
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002111 if details['email'] == self.GetIssueOwner():
2112 return
2113 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002114 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002115 'as %s.\n'
2116 'Uploading may fail due to lack of permissions.' %
2117 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2118 confirm_or_exit(action='upload')
2119
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002120 def GetStatus(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002121 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002122 or CQ status, assuming adherence to a common workflow.
2123
2124 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002125 * 'error' - error from review tool (including deleted issues)
2126 * 'unsent' - no reviewers added
2127 * 'waiting' - waiting for review
2128 * 'reply' - waiting for uploader to reply to review
2129 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002130 * 'dry-run' - dry-running in the CQ
2131 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07002132 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002133 """
2134 if not self.GetIssue():
2135 return None
2136
2137 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002138 data = self._GetChangeDetail([
2139 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Edward Lemur79d4f992019-11-11 23:49:02 +00002140 except GerritChangeNotExists:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002141 return 'error'
2142
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002143 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002144 return 'closed'
2145
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002146 cq_label = data['labels'].get('Commit-Queue', {})
2147 max_cq_vote = 0
2148 for vote in cq_label.get('all', []):
2149 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
2150 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002151 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002152 if max_cq_vote == 1:
2153 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002154
Aaron Gable9ab38c62017-04-06 14:36:33 -07002155 if data['labels'].get('Code-Review', {}).get('approved'):
2156 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002157
2158 if not data.get('reviewers', {}).get('REVIEWER', []):
2159 return 'unsent'
2160
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002161 owner = data['owner'].get('_account_id')
Edward Lemur79d4f992019-11-11 23:49:02 +00002162 messages = sorted(data.get('messages', []), key=lambda m: m.get('date'))
Andrii Shyshkalov8aa9d622020-03-10 19:15:35 +00002163 while messages:
2164 m = messages.pop()
Andrii Shyshkalov899785a2021-07-09 12:45:37 +00002165 if (m.get('tag', '').startswith('autogenerated:cq') or
2166 m.get('tag', '').startswith('autogenerated:cv')):
2167 # Ignore replies from LUCI CV/CQ.
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002168 continue
Andrii Shyshkalov8aa9d622020-03-10 19:15:35 +00002169 if m.get('author', {}).get('_account_id') == owner:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002170 # Most recent message was by owner.
2171 return 'waiting'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002172
2173 # Some reply from non-owner.
2174 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002175
2176 # Somehow there are no messages even though there are reviewers.
2177 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002178
Gavin Mak4e5e3992022-11-14 22:40:12 +00002179 def GetMostRecentPatchset(self, update=True):
Edward Lemur6c6827c2020-02-06 21:15:18 +00002180 if not self.GetIssue():
2181 return None
2182
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002183 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002184 patchset = data['revisions'][data['current_revision']]['_number']
Gavin Mak4e5e3992022-11-14 22:40:12 +00002185 if update:
2186 self.SetPatchset(patchset)
Aaron Gablee8856ee2017-12-07 12:41:46 -08002187 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002188
Gavin Makf35a9eb2022-11-17 18:34:36 +00002189 def _IsPatchsetRangeSignificant(self, lower, upper):
2190 """Returns True if the inclusive range of patchsets contains any reworks or
2191 rebases."""
2192 if not self.GetIssue():
2193 return False
2194
2195 data = self._GetChangeDetail(['ALL_REVISIONS'])
2196 ps_kind = {}
2197 for rev_info in data.get('revisions', {}).values():
2198 ps_kind[rev_info['_number']] = rev_info.get('kind', '')
2199
2200 for ps in range(lower, upper + 1):
2201 assert ps in ps_kind, 'expected patchset %d in change detail' % ps
2202 if ps_kind[ps] not in ('NO_CHANGE', 'NO_CODE_CHANGE'):
2203 return True
2204 return False
2205
Gavin Make61ccc52020-11-13 00:12:57 +00002206 def GetMostRecentDryRunPatchset(self):
2207 """Get patchsets equivalent to the most recent patchset and return
2208 the patchset with the latest dry run. If none have been dry run, return
2209 the latest patchset."""
2210 if not self.GetIssue():
2211 return None
2212
2213 data = self._GetChangeDetail(['ALL_REVISIONS'])
2214 patchset = data['revisions'][data['current_revision']]['_number']
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002215 dry_run = {int(m['_revision_number'])
2216 for m in data.get('messages', [])
2217 if m.get('tag', '').endswith('dry-run')}
Gavin Make61ccc52020-11-13 00:12:57 +00002218
2219 for revision_info in sorted(data.get('revisions', {}).values(),
2220 key=lambda c: c['_number'], reverse=True):
2221 if revision_info['_number'] in dry_run:
2222 patchset = revision_info['_number']
2223 break
2224 if revision_info.get('kind', '') not in \
2225 ('NO_CHANGE', 'NO_CODE_CHANGE', 'TRIVIAL_REBASE'):
2226 break
2227 self.SetPatchset(patchset)
2228 return patchset
2229
Aaron Gable636b13f2017-07-14 10:42:48 -07002230 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002231 gerrit_util.SetReview(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002232 self.GetGerritHost(), self._GerritChangeIdentifier(),
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002233 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002234
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002235 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002236 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002237 # CURRENT_REVISION is included to get the latest patchset so that
2238 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002239 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002240 options=['MESSAGES', 'DETAILED_ACCOUNTS',
2241 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002242 file_comments = gerrit_util.GetChangeComments(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002243 self.GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002244 robot_file_comments = gerrit_util.GetChangeRobotComments(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002245 self.GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002246
2247 # Add the robot comments onto the list of comments, but only
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +00002248 # keep those that are from the latest patchset.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002249 latest_patch_set = self.GetMostRecentPatchset()
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002250 for path, robot_comments in robot_file_comments.items():
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002251 line_comments = file_comments.setdefault(path, [])
2252 line_comments.extend(
2253 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002254
2255 # Build dictionary of file comments for easy access and sorting later.
2256 # {author+date: {path: {patchset: {line: url+message}}}}
2257 comments = collections.defaultdict(
2258 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
Andrii Shyshkalova3762a92020-11-25 10:20:42 +00002259
2260 server = self.GetCodereviewServer()
2261 if server in _KNOWN_GERRIT_TO_SHORT_URLS:
2262 # /c/ is automatically added by short URL server.
2263 url_prefix = '%s/%s' % (_KNOWN_GERRIT_TO_SHORT_URLS[server],
2264 self.GetIssue())
2265 else:
2266 url_prefix = '%s/c/%s' % (server, self.GetIssue())
2267
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002268 for path, line_comments in file_comments.items():
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002269 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002270 tag = comment.get('tag', '')
2271 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002272 continue
2273 key = (comment['author']['email'], comment['updated'])
2274 if comment.get('side', 'REVISION') == 'PARENT':
2275 patchset = 'Base'
2276 else:
2277 patchset = 'PS%d' % comment['patch_set']
2278 line = comment.get('line', 0)
Andrii Shyshkalova3762a92020-11-25 10:20:42 +00002279 url = ('%s/%s/%s#%s%s' %
2280 (url_prefix, comment['patch_set'], path,
2281 'b' if comment.get('side') == 'PARENT' else '',
2282 str(line) if line else ''))
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002283 comments[key][path][patchset][line] = (url, comment['message'])
2284
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002285 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002286 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002287 summary = self._BuildCommentSummary(msg, comments, readable)
2288 if summary:
2289 summaries.append(summary)
2290 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002291
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002292 @staticmethod
2293 def _BuildCommentSummary(msg, comments, readable):
Josip Sokcevic266129c2021-11-09 00:22:00 +00002294 if 'email' not in msg['author']:
2295 # Some bot accounts may not have an email associated.
2296 return None
2297
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002298 key = (msg['author']['email'], msg['date'])
2299 # Don't bother showing autogenerated messages that don't have associated
2300 # file or line comments. this will filter out most autogenerated
2301 # messages, but will keep robot comments like those from Tricium.
2302 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
2303 if is_autogenerated and not comments.get(key):
2304 return None
2305 message = msg['message']
2306 # Gerrit spits out nanoseconds.
2307 assert len(msg['date'].split('.')[-1]) == 9
2308 date = datetime.datetime.strptime(msg['date'][:-3],
2309 '%Y-%m-%d %H:%M:%S.%f')
2310 if key in comments:
2311 message += '\n'
2312 for path, patchsets in sorted(comments.get(key, {}).items()):
2313 if readable:
2314 message += '\n%s' % path
2315 for patchset, lines in sorted(patchsets.items()):
2316 for line, (url, content) in sorted(lines.items()):
2317 if line:
2318 line_str = 'Line %d' % line
2319 path_str = '%s:%d:' % (path, line)
2320 else:
2321 line_str = 'File comment'
2322 path_str = '%s:0:' % path
2323 if readable:
2324 message += '\n %s, %s: %s' % (patchset, line_str, url)
2325 message += '\n %s\n' % content
2326 else:
2327 message += '\n%s ' % path_str
2328 message += '\n%s\n' % content
2329
2330 return _CommentSummary(
2331 date=date,
2332 message=message,
2333 sender=msg['author']['email'],
2334 autogenerated=is_autogenerated,
2335 # These could be inferred from the text messages and correlated with
2336 # Code-Review label maximum, however this is not reliable.
2337 # Leaving as is until the need arises.
2338 approval=False,
2339 disapproval=False,
2340 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002341
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002342 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002343 gerrit_util.AbandonChange(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002344 self.GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002345
Xinan Lin1bd4ffa2021-07-28 00:54:22 +00002346 def SubmitIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002347 gerrit_util.SubmitChange(
Xinan Lin1bd4ffa2021-07-28 00:54:22 +00002348 self.GetGerritHost(), self._GerritChangeIdentifier())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002349
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002350 def _GetChangeDetail(self, options=None):
2351 """Returns details of associated Gerrit change and caching results."""
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002352 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002353 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002354
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002355 # Optimization to avoid multiple RPCs:
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002356 if 'CURRENT_REVISION' in options or 'ALL_REVISIONS' in options:
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002357 options.append('CURRENT_COMMIT')
2358
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002359 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002360 cache_key = str(self.GetIssue())
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002361 options_set = frozenset(o.upper() for o in options)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002362
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002363 for cached_options_set, data in self._detail_cache.get(cache_key, []):
2364 # Assumption: data fetched before with extra options is suitable
2365 # for return for a smaller set of options.
2366 # For example, if we cached data for
2367 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2368 # and request is for options=[CURRENT_REVISION],
2369 # THEN we can return prior cached data.
2370 if options_set.issubset(cached_options_set):
2371 return data
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002372
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002373 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002374 data = gerrit_util.GetChangeDetail(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002375 self.GetGerritHost(), self._GerritChangeIdentifier(), options_set)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002376 except gerrit_util.GerritError as e:
2377 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002378 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002379 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002380
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002381 self._detail_cache.setdefault(cache_key, []).append((options_set, data))
tandriic2405f52016-10-10 08:13:15 -07002382 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002383
Gavin Mak4e5e3992022-11-14 22:40:12 +00002384 def _GetChangeCommit(self, revision='current'):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002385 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002386 try:
Gavin Mak4e5e3992022-11-14 22:40:12 +00002387 data = gerrit_util.GetChangeCommit(self.GetGerritHost(),
2388 self._GerritChangeIdentifier(),
2389 revision)
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002390 except gerrit_util.GerritError as e:
2391 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002392 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002393 raise
agable32978d92016-11-01 12:55:02 -07002394 return data
2395
Karen Qian40c19422019-03-13 21:28:29 +00002396 def _IsCqConfigured(self):
2397 detail = self._GetChangeDetail(['LABELS'])
Andrii Shyshkalov8effa4d2020-01-21 13:23:36 +00002398 return u'Commit-Queue' in detail.get('labels', {})
Karen Qian40c19422019-03-13 21:28:29 +00002399
Saagar Sanghavi03b15132020-08-10 16:43:41 +00002400 def CMDLand(self, force, bypass_hooks, verbose, parallel, resultdb, realm):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002401 if git_common.is_dirty_git_tree('land'):
2402 return 1
Karen Qian40c19422019-03-13 21:28:29 +00002403
tandriid60367b2016-06-22 05:25:12 -07002404 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00002405 if not force and self._IsCqConfigured():
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002406 confirm_or_exit('\nIt seems this repository has a CQ, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002407 'which can test and land changes for you. '
2408 'Are you sure you wish to bypass it?\n',
2409 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002410 differs = True
Gavin Makbe2e9262022-11-08 23:41:55 +00002411 last_upload = self._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002412 # Note: git diff outputs nothing if there is no diff.
2413 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002414 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002415 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002416 if detail['current_revision'] == last_upload:
2417 differs = False
2418 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002419 print('WARNING: Local branch contents differ from latest uploaded '
2420 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002421 if differs:
2422 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002423 confirm_or_exit(
2424 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2425 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002426 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002427 elif not bypass_hooks:
Edward Lemur227d5102020-02-25 23:45:35 +00002428 upstream = self.GetCommonAncestorWithUpstream()
2429 if self.GetIssue():
2430 description = self.FetchDescription()
2431 else:
Edward Lemura12175c2020-03-09 16:58:26 +00002432 description = _create_description_from_log([upstream])
Edward Lemur227d5102020-02-25 23:45:35 +00002433 self.RunHook(
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002434 committing=True,
2435 may_prompt=not force,
2436 verbose=verbose,
Edward Lemur227d5102020-02-25 23:45:35 +00002437 parallel=parallel,
2438 upstream=upstream,
2439 description=description,
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00002440 all_files=False,
Saagar Sanghavi03b15132020-08-10 16:43:41 +00002441 resultdb=resultdb,
2442 realm=realm)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002443
Xinan Lin1bd4ffa2021-07-28 00:54:22 +00002444 self.SubmitIssue()
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002445 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002446 links = self._GetChangeCommit().get('web_links', [])
2447 for link in links:
Michael Mosse371c642021-09-29 16:41:04 +00002448 if link.get('name') in ['gitiles', 'browse'] and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002449 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002450 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002451 return 0
2452
Bruce Dawsonf362f6f2021-02-18 23:15:17 +00002453 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force,
2454 newbranch):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002455 assert parsed_issue_arg.valid
2456
Edward Lemur125d60a2019-09-13 18:25:41 +00002457 self.issue = parsed_issue_arg.issue
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002458
2459 if parsed_issue_arg.hostname:
2460 self._gerrit_host = parsed_issue_arg.hostname
2461 self._gerrit_server = 'https://%s' % self._gerrit_host
2462
tandriic2405f52016-10-10 08:13:15 -07002463 try:
2464 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002465 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002466 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002467
2468 if not parsed_issue_arg.patchset:
2469 # Use current revision by default.
2470 revision_info = detail['revisions'][detail['current_revision']]
2471 patchset = int(revision_info['_number'])
2472 else:
2473 patchset = parsed_issue_arg.patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00002474 for revision_info in detail['revisions'].values():
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002475 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2476 break
2477 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002478 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002479 (parsed_issue_arg.patchset, self.GetIssue()))
2480
Edward Lemur125d60a2019-09-13 18:25:41 +00002481 remote_url = self.GetRemoteUrl()
Aaron Gable697a91b2018-01-19 15:20:15 -08002482 if remote_url.endswith('.git'):
2483 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002484 remote_url = remote_url.rstrip('/')
2485
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002486 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002487 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002488
2489 if remote_url != fetch_info['url']:
2490 DieWithError('Trying to patch a change from %s but this repo appears '
2491 'to be %s.' % (fetch_info['url'], remote_url))
2492
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002493 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002494
Joanna Wangc023a632023-01-26 17:59:25 +00002495 # Set issue immediately in case the cherry-pick fails, which happens
2496 # when resolving conflicts.
2497 if self.GetBranch():
Bruce Dawsonf362f6f2021-02-18 23:15:17 +00002498 self.SetIssue(parsed_issue_arg.issue)
2499
Aaron Gable62619a32017-06-16 08:22:09 -07002500 if force:
2501 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2502 print('Checked out commit for change %i patchset %i locally' %
2503 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002504 elif nocommit:
2505 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2506 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002507 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002508 RunGit(['cherry-pick', 'FETCH_HEAD'])
2509 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002510 (parsed_issue_arg.issue, patchset))
2511 print('Note: this created a local commit which does not have '
2512 'the same hash as the one uploaded for review. This will make '
2513 'uploading changes based on top of this branch difficult.\n'
2514 'If you want to do that, use "git cl patch --force" instead.')
2515
Stefan Zagerd08043c2017-10-12 12:07:02 -07002516 if self.GetBranch():
Stefan Zagerd08043c2017-10-12 12:07:02 -07002517 self.SetPatchset(patchset)
Edward Lesmes50da7702020-03-30 19:23:43 +00002518 fetched_hash = scm.GIT.ResolveCommit(settings.GetRoot(), 'FETCH_HEAD')
Gavin Makbe2e9262022-11-08 23:41:55 +00002519 self._GitSetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY, fetched_hash)
2520 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY, fetched_hash)
Stefan Zagerd08043c2017-10-12 12:07:02 -07002521 else:
2522 print('WARNING: You are in detached HEAD state.\n'
2523 'The patch has been applied to your checkout, but you will not be '
2524 'able to upload a new patch set to the gerrit issue.\n'
2525 'Try using the \'-b\' option if you would like to work on a '
2526 'branch and/or upload a new patch set.')
2527
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002528 return 0
2529
Joanna Wang18de1f62023-01-21 01:24:24 +00002530 @staticmethod
2531 def _GerritCommitMsgHookCheck(offer_removal):
2532 # type: (bool) -> None
2533 """Checks for the gerrit's commit-msg hook and removes it if necessary."""
tandrii16e0b4e2016-06-07 10:34:28 -07002534 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2535 if not os.path.exists(hook):
2536 return
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002537 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
2538 # custom developer-made one.
tandrii16e0b4e2016-06-07 10:34:28 -07002539 data = gclient_utils.FileRead(hook)
2540 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2541 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002542 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002543 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002544 'and may interfere with it in subtle ways.\n'
2545 'We recommend you remove the commit-msg hook.')
2546 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002547 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002548 gclient_utils.rm_file_or_tree(hook)
2549 print('Gerrit commit-msg hook removed.')
2550 else:
2551 print('OK, will keep Gerrit commit-msg hook in place.')
2552
Edward Lemur1b52d872019-05-09 21:12:12 +00002553 def _CleanUpOldTraces(self):
2554 """Keep only the last |MAX_TRACES| traces."""
2555 try:
2556 traces = sorted([
2557 os.path.join(TRACES_DIR, f)
2558 for f in os.listdir(TRACES_DIR)
2559 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2560 and not f.startswith('tmp'))
2561 ])
2562 traces_to_delete = traces[:-MAX_TRACES]
2563 for trace in traces_to_delete:
Daniel Chengcf6269b2019-05-18 01:02:12 +00002564 os.remove(trace)
Edward Lemur1b52d872019-05-09 21:12:12 +00002565 except OSError:
2566 print('WARNING: Failed to remove old git traces from\n'
2567 ' %s'
2568 'Consider removing them manually.' % TRACES_DIR)
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002569
Edward Lemur5737f022019-05-17 01:24:00 +00002570 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
Edward Lemur1b52d872019-05-09 21:12:12 +00002571 """Zip and write the git push traces stored in traces_dir."""
2572 gclient_utils.safe_makedirs(TRACES_DIR)
Edward Lemur1b52d872019-05-09 21:12:12 +00002573 traces_zip = trace_name + '-traces'
2574 traces_readme = trace_name + '-README'
Michael Mosse7f0b4c2019-05-08 04:36:24 +00002575 # Create a temporary dir to store git config and gitcookies in. It will be
2576 # compressed and stored next to the traces.
2577 git_info_dir = tempfile.mkdtemp()
Edward Lemur1b52d872019-05-09 21:12:12 +00002578 git_info_zip = trace_name + '-git-info'
2579
Josip Sokcevic5e18b602020-04-23 21:47:00 +00002580 git_push_metadata['now'] = datetime_now().strftime('%Y-%m-%dT%H:%M:%S.%f')
sangwoo.ko7a614332019-05-22 02:46:19 +00002581
Edward Lemur1b52d872019-05-09 21:12:12 +00002582 git_push_metadata['trace_name'] = trace_name
2583 gclient_utils.FileWrite(
2584 traces_readme, TRACES_README_FORMAT % git_push_metadata)
2585
2586 # Keep only the first 6 characters of the git hashes on the packet
2587 # trace. This greatly decreases size after compression.
2588 packet_traces = os.path.join(traces_dir, 'trace-packet')
2589 if os.path.isfile(packet_traces):
2590 contents = gclient_utils.FileRead(packet_traces)
2591 gclient_utils.FileWrite(
2592 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2593 shutil.make_archive(traces_zip, 'zip', traces_dir)
2594
2595 # Collect and compress the git config and gitcookies.
2596 git_config = RunGit(['config', '-l'])
2597 gclient_utils.FileWrite(
2598 os.path.join(git_info_dir, 'git-config'),
2599 git_config)
2600
2601 cookie_auth = gerrit_util.Authenticator.get()
2602 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2603 gitcookies_path = cookie_auth.get_gitcookies_path()
2604 if os.path.isfile(gitcookies_path):
2605 gitcookies = gclient_utils.FileRead(gitcookies_path)
2606 gclient_utils.FileWrite(
2607 os.path.join(git_info_dir, 'gitcookies'),
2608 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2609 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2610
Edward Lemur1b52d872019-05-09 21:12:12 +00002611 gclient_utils.rmtree(git_info_dir)
2612
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002613 def _RunGitPushWithTraces(self,
2614 refspec,
2615 refspec_opts,
2616 git_push_metadata,
2617 git_push_options=None):
Edward Lemur1b52d872019-05-09 21:12:12 +00002618 """Run git push and collect the traces resulting from the execution."""
2619 # Create a temporary directory to store traces in. Traces will be compressed
2620 # and stored in a 'traces' dir inside depot_tools.
2621 traces_dir = tempfile.mkdtemp()
Edward Lemur5737f022019-05-17 01:24:00 +00002622 trace_name = os.path.join(
2623 TRACES_DIR, datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
Edward Lemur0f58ae42019-04-30 17:24:12 +00002624
2625 env = os.environ.copy()
2626 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2627 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Jonathan Nieder9779b142019-05-29 23:19:29 +00002628 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002629 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2630 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2631 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2632
2633 try:
2634 push_returncode = 0
Edward Lemur1b52d872019-05-09 21:12:12 +00002635 remote_url = self.GetRemoteUrl()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002636 before_push = time_time()
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002637 push_cmd = ['git', 'push', remote_url, refspec]
2638 if git_push_options:
2639 for opt in git_push_options:
2640 push_cmd.extend(['-o', opt])
2641
Edward Lemur0f58ae42019-04-30 17:24:12 +00002642 push_stdout = gclient_utils.CheckCallAndFilter(
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002643 push_cmd,
Edward Lemur0f58ae42019-04-30 17:24:12 +00002644 env=env,
2645 print_stdout=True,
2646 # Flush after every line: useful for seeing progress when running as
2647 # recipe.
2648 filter_fn=lambda _: sys.stdout.flush())
Edward Lemur79d4f992019-11-11 23:49:02 +00002649 push_stdout = push_stdout.decode('utf-8', 'replace')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002650 except subprocess2.CalledProcessError as e:
2651 push_returncode = e.returncode
Aravind Vasudevanc9508582022-10-18 03:07:41 +00002652 if 'blocked keyword' in str(e.stdout) or 'banned word' in str(e.stdout):
Josip Sokcevic740825e2021-05-12 18:28:34 +00002653 raise GitPushError(
2654 'Failed to create a change, very likely due to blocked keyword. '
2655 'Please examine output above for the reason of the failure.\n'
2656 'If this is a false positive, you can try to bypass blocked '
2657 'keyword by using push option '
2658 '-o uploadvalidator~skip, e.g.:\n'
2659 'git cl upload -o uploadvalidator~skip\n\n'
2660 'If git-cl is not working correctly, file a bug under the '
2661 'Infra>SDK component.')
Josip Sokcevic54e30e72022-02-10 22:32:24 +00002662 if 'git push -o nokeycheck' in str(e.stdout):
2663 raise GitPushError(
2664 'Failed to create a change, very likely due to a private key being '
2665 'detected. Please examine output above for the reason of the '
2666 'failure.\n'
2667 'If this is a false positive, you can try to bypass private key '
2668 'detection by using push option '
2669 '-o nokeycheck, e.g.:\n'
2670 'git cl upload -o nokeycheck\n\n'
2671 'If git-cl is not working correctly, file a bug under the '
2672 'Infra>SDK component.')
Josip Sokcevic740825e2021-05-12 18:28:34 +00002673
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002674 raise GitPushError(
2675 'Failed to create a change. Please examine output above for the '
2676 'reason of the failure.\n'
Josip Sokcevic7386a1e2021-02-12 19:00:34 +00002677 'For emergencies, Googlers can escalate to '
2678 'go/gob-support or go/notify#gob\n'
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002679 'Hint: run command below to diagnose common Git/Gerrit '
2680 'credential problems:\n'
2681 ' git cl creds-check\n'
2682 '\n'
2683 'If git-cl is not working correctly, file a bug under the Infra>SDK '
2684 'component including the files below.\n'
2685 'Review the files before upload, since they might contain sensitive '
2686 'information.\n'
2687 'Set the Restrict-View-Google label so that they are not publicly '
2688 'accessible.\n' + TRACES_MESSAGE % {'trace_name': trace_name})
Edward Lemur0f58ae42019-04-30 17:24:12 +00002689 finally:
2690 execution_time = time_time() - before_push
2691 metrics.collector.add_repeated('sub_commands', {
2692 'command': 'git push',
2693 'execution_time': execution_time,
2694 'exit_code': push_returncode,
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002695 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
Edward Lemur0f58ae42019-04-30 17:24:12 +00002696 })
2697
Edward Lemur1b52d872019-05-09 21:12:12 +00002698 git_push_metadata['execution_time'] = execution_time
2699 git_push_metadata['exit_code'] = push_returncode
Edward Lemur5737f022019-05-17 01:24:00 +00002700 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002701
Edward Lemur1b52d872019-05-09 21:12:12 +00002702 self._CleanUpOldTraces()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002703 gclient_utils.rmtree(traces_dir)
2704
2705 return push_stdout
2706
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002707 def CMDUploadChange(self, options, git_diff_args, custom_cl_base,
2708 change_desc):
2709 """Upload the current branch to Gerrit, retry if new remote HEAD is
2710 found. options and change_desc may be mutated."""
Josip Sokcevicb631a882021-01-06 18:18:10 +00002711 remote, remote_branch = self.GetRemoteBranch()
2712 branch = GetTargetRef(remote, remote_branch, options.target_branch)
2713
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002714 try:
2715 return self._CMDUploadChange(options, git_diff_args, custom_cl_base,
Josip Sokcevicb631a882021-01-06 18:18:10 +00002716 change_desc, branch)
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002717 except GitPushError as e:
Josip Sokcevicb631a882021-01-06 18:18:10 +00002718 # Repository might be in the middle of transition to main branch as
2719 # default, and uploads to old default might be blocked.
2720 if remote_branch not in [DEFAULT_OLD_BRANCH, DEFAULT_NEW_BRANCH]:
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002721 DieWithError(str(e), change_desc)
2722
Josip Sokcevicb631a882021-01-06 18:18:10 +00002723 project_head = gerrit_util.GetProjectHead(self._gerrit_host,
2724 self.GetGerritProject())
2725 if project_head == branch:
2726 DieWithError(str(e), change_desc)
2727 branch = project_head
2728
2729 print("WARNING: Fetching remote state and retrying upload to default "
2730 "branch...")
2731 RunGit(['fetch', '--prune', remote])
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002732 options.edit_description = False
2733 options.force = True
2734 try:
Josip Sokcevicb631a882021-01-06 18:18:10 +00002735 self._CMDUploadChange(options, git_diff_args, custom_cl_base,
2736 change_desc, branch)
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002737 except GitPushError as e:
2738 DieWithError(str(e), change_desc)
2739
2740 def _CMDUploadChange(self, options, git_diff_args, custom_cl_base,
Josip Sokcevicb631a882021-01-06 18:18:10 +00002741 change_desc, branch):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002742 """Upload the current branch to Gerrit."""
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002743 if options.squash:
Joanna Wangc4ac3022023-01-31 21:19:57 +00002744 Changelist._GerritCommitMsgHookCheck(offer_removal=not options.force)
Gavin Mak4e5e3992022-11-14 22:40:12 +00002745 external_parent = None
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002746 if self.GetIssue():
Josipe827b0f2020-01-30 00:07:20 +00002747 # User requested to change description
2748 if options.edit_description:
Josipe827b0f2020-01-30 00:07:20 +00002749 change_desc.prompt()
Gavin Mak4e5e3992022-11-14 22:40:12 +00002750 change_detail = self._GetChangeDetail(['CURRENT_REVISION'])
2751 change_id = change_detail['change_id']
Edward Lemur5a644f82020-03-18 16:44:57 +00002752 change_desc.ensure_change_id(change_id)
Gavin Mak4e5e3992022-11-14 22:40:12 +00002753
2754 # Check if changes outside of this workspace have been uploaded.
2755 current_rev = change_detail['current_revision']
2756 last_uploaded_rev = self._GitGetBranchConfigValue(
2757 GERRIT_SQUASH_HASH_CONFIG_KEY)
2758 if last_uploaded_rev and current_rev != last_uploaded_rev:
2759 external_parent = self._UpdateWithExternalChanges()
Aaron Gableb56ad332017-01-06 15:24:31 -08002760 else: # if not self.GetIssue()
Gavin Mak68e6cf32021-01-25 18:24:08 +00002761 if not options.force and not options.message_file:
Anthony Polito8b955342019-09-24 19:01:36 +00002762 change_desc.prompt()
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002763 change_ids = git_footers.get_footer_change_id(change_desc.description)
Edward Lemur5a644f82020-03-18 16:44:57 +00002764 if len(change_ids) == 1:
2765 change_id = change_ids[0]
2766 else:
2767 change_id = GenerateGerritChangeId(change_desc.description)
2768 change_desc.ensure_change_id(change_id)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002769
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002770 if options.preserve_tryjobs:
2771 change_desc.set_preserve_tryjobs()
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002772
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002773 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Gavin Mak4e5e3992022-11-14 22:40:12 +00002774 parent = external_parent or self._ComputeParent(
Edward Lemur5a644f82020-03-18 16:44:57 +00002775 remote, upstream_branch, custom_cl_base, options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002776 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Edward Lemur1773f372020-02-22 00:27:14 +00002777 with gclient_utils.temporary_file() as desc_tempfile:
2778 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
2779 ref_to_push = RunGit(
2780 ['commit-tree', tree, '-p', parent, '-F', desc_tempfile]).strip()
Anthony Polito8b955342019-09-24 19:01:36 +00002781 else: # if not options.squash
Gregory Nisbet48d9e1e2021-04-15 23:35:54 +00002782 if options.no_add_changeid:
2783 pass
2784 else: # adding Change-Ids is okay.
2785 if not git_footers.get_footer_change_id(change_desc.description):
2786 DownloadGerritHook(False)
2787 change_desc.set_description(
2788 self._AddChangeIdToCommitMessage(change_desc.description,
2789 git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002790 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002791 # For no-squash mode, we assume the remote called "origin" is the one we
2792 # want. It is not worthwhile to support different workflows for
2793 # no-squash mode.
2794 parent = 'origin/%s' % branch
Gregory Nisbet48d9e1e2021-04-15 23:35:54 +00002795 # attempt to extract the changeid from the current description
2796 # fail informatively if not possible.
2797 change_id_candidates = git_footers.get_footer_change_id(
2798 change_desc.description)
2799 if not change_id_candidates:
2800 DieWithError("Unable to extract change-id from message.")
2801 change_id = change_id_candidates[0]
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002802
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002803 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002804 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2805 ref_to_push)]).splitlines()
2806 if len(commits) > 1:
2807 print('WARNING: This will upload %d commits. Run the following command '
2808 'to see which commits will be uploaded: ' % len(commits))
2809 print('git log %s..%s' % (parent, ref_to_push))
2810 print('You can also use `git squash-branch` to squash these into a '
2811 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002812 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002813
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002814 reviewers = sorted(change_desc.get_reviewers())
Edward Lemur4508b422019-10-03 21:56:35 +00002815 cc = []
Joanna Wangc4ac3022023-01-31 21:19:57 +00002816 # Add default, watchlist, presubmit ccs if this is the initial upload
2817 # and CL is not private and auto-ccing has not been disabled.
2818 if not options.private and not options.no_autocc and not self.GetIssue():
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002819 cc = self.GetCCList().split(',')
Gavin Makb1c08f62021-04-01 18:05:58 +00002820 if len(cc) > 100:
2821 lsc = ('https://chromium.googlesource.com/chromium/src/+/HEAD/docs/'
2822 'process/lsc/lsc_workflow.md')
2823 print('WARNING: This will auto-CC %s users.' % len(cc))
2824 print('LSC may be more appropriate: %s' % lsc)
2825 print('You can also use the --no-autocc flag to disable auto-CC.')
2826 confirm_or_exit(action='continue')
Edward Lemur4508b422019-10-03 21:56:35 +00002827 # Add cc's from the --cc flag.
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002828 if options.cc:
2829 cc.extend(options.cc)
Edward Lemur79d4f992019-11-11 23:49:02 +00002830 cc = [email.strip() for email in cc if email.strip()]
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002831 if change_desc.get_cced():
2832 cc.extend(change_desc.get_cced())
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002833 if self.GetGerritHost() == 'chromium-review.googlesource.com':
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002834 valid_accounts = set(reviewers + cc)
2835 # TODO(crbug/877717): relax this for all hosts.
2836 else:
2837 valid_accounts = gerrit_util.ValidAccounts(
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002838 self.GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002839 logging.info('accounts %s are recognized, %s invalid',
2840 sorted(valid_accounts),
2841 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002842
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002843 # Extra options that can be specified at push time. Doc:
2844 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Joanna Wanga1abbed2023-01-24 01:41:05 +00002845 refspec_opts = self._GetRefSpecOptions(options, change_desc)
agablec6787972016-09-09 16:13:34 -07002846
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002847 for r in sorted(reviewers):
2848 if r in valid_accounts:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002849 refspec_opts.append('r=%s' % r)
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002850 reviewers.remove(r)
2851 else:
2852 # TODO(tandrii): this should probably be a hard failure.
2853 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2854 % r)
2855 for c in sorted(cc):
2856 # refspec option will be rejected if cc doesn't correspond to an
2857 # account, even though REST call to add such arbitrary cc may succeed.
2858 if c in valid_accounts:
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002859 refspec_opts.append('cc=%s' % c)
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002860 cc.remove(c)
2861
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002862 refspec_suffix = ''
2863 if refspec_opts:
2864 refspec_suffix = '%' + ','.join(refspec_opts)
2865 assert ' ' not in refspec_suffix, (
2866 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2867 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002868
Edward Lemur1b52d872019-05-09 21:12:12 +00002869 git_push_metadata = {
Edward Lesmeseeca9c62020-11-20 00:00:17 +00002870 'gerrit_host': self.GetGerritHost(),
Josip Sokcevicf736cab2020-10-20 23:41:38 +00002871 'title': options.title or '<untitled>',
Edward Lemur1b52d872019-05-09 21:12:12 +00002872 'change_id': change_id,
2873 'description': change_desc.description,
2874 }
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002875
Gavin Mak4e5e3992022-11-14 22:40:12 +00002876 # Gerrit may or may not update fast enough to return the correct patchset
2877 # number after we push. Get the pre-upload patchset and increment later.
2878 latest_ps = self.GetMostRecentPatchset(update=False) or 0
2879
Josip Sokcevicd0ba91f2021-03-29 20:12:09 +00002880 push_stdout = self._RunGitPushWithTraces(refspec, refspec_opts,
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002881 git_push_metadata,
2882 options.push_options)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002883
2884 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002885 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002886 change_numbers = [m.group(1)
2887 for m in map(regex.match, push_stdout.splitlines())
2888 if m]
2889 if len(change_numbers) != 1:
2890 DieWithError(
2891 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002892 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002893 self.SetIssue(change_numbers[0])
Gavin Mak4e5e3992022-11-14 22:40:12 +00002894 self.SetPatchset(latest_ps + 1)
Gavin Makbe2e9262022-11-08 23:41:55 +00002895 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY, ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002896
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002897 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002898 # GetIssue() is not set in case of non-squash uploads according to tests.
Aaron Gable6e7ddb62020-05-27 22:23:29 +00002899 # TODO(crbug.com/751901): non-squash uploads in git cl should be removed.
Thiago Perrottab0fb8d52022-08-30 21:26:19 +00002900 gerrit_util.AddReviewers(self.GetGerritHost(),
2901 self._GerritChangeIdentifier(),
2902 reviewers,
2903 cc,
2904 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002905
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002906 return 0
2907
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002908 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2909 change_desc):
2910 """Computes parent of the generated commit to be uploaded to Gerrit.
2911
2912 Returns revision or a ref name.
2913 """
2914 if custom_cl_base:
2915 # Try to avoid creating additional unintended CLs when uploading, unless
2916 # user wants to take this risk.
2917 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2918 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2919 local_ref_of_target_remote])
2920 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002921 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002922 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2923 'If you proceed with upload, more than 1 CL may be created by '
2924 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2925 'If you are certain that specified base `%s` has already been '
2926 'uploaded to Gerrit as another CL, you may proceed.\n' %
2927 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2928 if not force:
2929 confirm_or_exit(
2930 'Do you take responsibility for cleaning up potential mess '
2931 'resulting from proceeding with upload?',
2932 action='upload')
2933 return custom_cl_base
2934
Aaron Gablef97e33d2017-03-30 15:44:27 -07002935 if remote != '.':
2936 return self.GetCommonAncestorWithUpstream()
2937
2938 # If our upstream branch is local, we base our squashed commit on its
2939 # squashed version.
2940 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2941
Aaron Gablef97e33d2017-03-30 15:44:27 -07002942 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002943 return self.GetCommonAncestorWithUpstream()
Glen Robertson7d98e222020-08-27 17:53:11 +00002944 if upstream_branch_name == 'main':
2945 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002946
2947 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002948 # TODO(tandrii): consider checking parent change in Gerrit and using its
2949 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2950 # the tree hash of the parent branch. The upside is less likely bogus
2951 # requests to reupload parent change just because it's uploadhash is
2952 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Gavin Makbe2e9262022-11-08 23:41:55 +00002953 parent = scm.GIT.GetBranchConfig(settings.GetRoot(), upstream_branch_name,
2954 GERRIT_SQUASH_HASH_CONFIG_KEY)
Aaron Gablef97e33d2017-03-30 15:44:27 -07002955 # Verify that the upstream branch has been uploaded too, otherwise
2956 # Gerrit will create additional CLs when uploading.
2957 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2958 RunGitSilent(['rev-parse', parent + ':'])):
2959 DieWithError(
2960 '\nUpload upstream branch %s first.\n'
2961 'It is likely that this branch has been rebased since its last '
2962 'upload, so you just need to upload it again.\n'
2963 '(If you uploaded it with --no-squash, then branch dependencies '
2964 'are not supported, and you should reupload with --squash.)'
2965 % upstream_branch_name,
2966 change_desc)
2967 return parent
2968
Gavin Mak4e5e3992022-11-14 22:40:12 +00002969 def _UpdateWithExternalChanges(self):
2970 """Updates workspace with external changes.
2971
2972 Returns the commit hash that should be used as the merge base on upload.
2973 """
2974 local_ps = self.GetPatchset()
2975 if local_ps is None:
2976 return
2977
2978 external_ps = self.GetMostRecentPatchset(update=False)
Gavin Makf35a9eb2022-11-17 18:34:36 +00002979 if external_ps is None or local_ps == external_ps or \
2980 not self._IsPatchsetRangeSignificant(local_ps + 1, external_ps):
Gavin Mak4e5e3992022-11-14 22:40:12 +00002981 return
2982
2983 num_changes = external_ps - local_ps
Gavin Mak6f905472023-01-06 21:01:36 +00002984 if num_changes > 1:
2985 change_words = 'changes were'
2986 else:
2987 change_words = 'change was'
2988 print('\n%d external %s published to %s:\n' %
2989 (num_changes, change_words, self.GetIssueURL(short=True)))
2990
2991 # Print an overview of external changes.
2992 ps_to_commit = {}
2993 ps_to_info = {}
2994 revisions = self._GetChangeDetail(['ALL_REVISIONS'])
2995 for commit_id, revision_info in revisions.get('revisions', {}).items():
2996 ps_num = revision_info['_number']
2997 ps_to_commit[ps_num] = commit_id
2998 ps_to_info[ps_num] = revision_info
2999
3000 for ps in range(external_ps, local_ps, -1):
3001 commit = ps_to_commit[ps][:8]
3002 desc = ps_to_info[ps].get('description', '')
3003 print('Patchset %d [%s] %s' % (ps, commit, desc))
3004
3005 if not ask_for_explicit_yes('\nUploading as-is will override them. '
3006 'Get the latest changes and apply?'):
Gavin Mak4e5e3992022-11-14 22:40:12 +00003007 return
3008
3009 # Get latest Gerrit merge base. Use the first parent even if multiple exist.
3010 external_parent = self._GetChangeCommit(revision=external_ps)['parents'][0]
3011 external_base = external_parent['commit']
3012
3013 branch = git_common.current_branch()
3014 local_base = self.GetCommonAncestorWithUpstream()
3015 if local_base != external_base:
3016 print('\nLocal merge base %s is different from Gerrit %s.\n' %
3017 (local_base, external_base))
3018 if git_common.upstream(branch):
3019 DieWithError('Upstream branch set. Consider using `git rebase-update` '
3020 'to make these the same.')
3021 print('No upstream branch set. Consider setting it and using '
3022 '`git rebase-update`.\nContinuing upload with Gerrit merge base.')
3023
3024 # Fetch Gerrit's CL base if it doesn't exist locally.
3025 remote, _ = self.GetRemoteBranch()
3026 if not scm.GIT.IsValidRevision(settings.GetRoot(), external_base):
3027 RunGitSilent(['fetch', remote, external_base])
3028
3029 # Get the diff between local_ps and external_ps.
3030 issue = self.GetIssue()
Gavin Mak591ebaf2022-12-06 18:05:07 +00003031 changes_ref = 'refs/changes/%02d/%d/' % (issue % 100, issue)
Gavin Mak4e5e3992022-11-14 22:40:12 +00003032 RunGitSilent(['fetch', remote, changes_ref + str(local_ps)])
3033 last_uploaded = RunGitSilent(['rev-parse', 'FETCH_HEAD']).strip()
3034 RunGitSilent(['fetch', remote, changes_ref + str(external_ps)])
3035 latest_external = RunGitSilent(['rev-parse', 'FETCH_HEAD']).strip()
3036 diff = RunGitSilent(['diff', '%s..%s' % (last_uploaded, latest_external)])
3037
3038 # Diff can be empty in the case of trivial rebases.
3039 if not diff:
3040 return external_base
3041
3042 # Apply the diff.
3043 with gclient_utils.temporary_file() as diff_tempfile:
3044 gclient_utils.FileWrite(diff_tempfile, diff)
3045 clean_patch = RunGitWithCode(['apply', '--check', diff_tempfile])[0] == 0
3046 RunGitSilent(['apply', '-3', '--intent-to-add', diff_tempfile])
3047 if not clean_patch:
3048 # Normally patchset is set after upload. But because we exit, that never
3049 # happens. Updating here makes sure that subsequent uploads don't need
3050 # to fetch/apply the same diff again.
3051 self.SetPatchset(external_ps)
3052 DieWithError('\nPatch did not apply cleanly. Please resolve any '
3053 'conflicts and reupload.')
3054
3055 message = 'Incorporate external changes from '
3056 if num_changes == 1:
3057 message += 'patchset %d' % external_ps
3058 else:
3059 message += 'patchsets %d to %d' % (local_ps + 1, external_ps)
3060 RunGitSilent(['commit', '-am', message])
3061 # TODO(crbug.com/1382528): Use the previous commit's message as a default
3062 # patchset title instead of this 'Incorporate' message.
3063 return external_base
3064
Edward Lemura12175c2020-03-09 16:58:26 +00003065 def _AddChangeIdToCommitMessage(self, log_desc, args):
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003066 """Re-commits using the current message, assumes the commit hook is in
3067 place.
3068 """
Edward Lemura12175c2020-03-09 16:58:26 +00003069 RunGit(['commit', '--amend', '-m', log_desc])
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00003070 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003071 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003072 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003073 return new_log_desc
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003074
3075 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003076
tandriie113dfd2016-10-11 10:20:12 -07003077 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003078 try:
3079 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003080 except GerritChangeNotExists:
3081 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003082
3083 if data['status'] in ('ABANDONED', 'MERGED'):
3084 return 'CL %s is closed' % self.GetIssue()
3085
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003086 def GetGerritChange(self, patchset=None):
3087 """Returns a buildbucket.v2.GerritChange message for the current issue."""
Edward Lemur79d4f992019-11-11 23:49:02 +00003088 host = urllib.parse.urlparse(self.GetCodereviewServer()).hostname
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003089 issue = self.GetIssue()
Edward Lemur2c210a42019-09-16 23:58:35 +00003090 patchset = int(patchset or self.GetPatchset())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003091 data = self._GetChangeDetail(['ALL_REVISIONS'])
3092
3093 assert host and issue and patchset, 'CL must be uploaded first'
3094
3095 has_patchset = any(
3096 int(revision_data['_number']) == patchset
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003097 for revision_data in data['revisions'].values())
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003098 if not has_patchset:
Aaron Gablea45ee112016-11-22 15:14:38 -08003099 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003100 (patchset, self.GetIssue()))
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003101
tandrii8c5a3532016-11-04 07:52:02 -07003102 return {
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003103 'host': host,
3104 'change': issue,
3105 'project': data['project'],
3106 'patchset': patchset,
tandrii8c5a3532016-11-04 07:52:02 -07003107 }
tandriie113dfd2016-10-11 10:20:12 -07003108
tandriide281ae2016-10-12 06:02:30 -07003109 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003110 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003111
Edward Lemur707d70b2018-02-07 00:50:14 +01003112 def GetReviewers(self):
3113 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00003114 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01003115
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003116
Lei Zhang8a0efc12020-08-05 19:58:45 +00003117def _get_bug_line_values(default_project_prefix, bugs):
3118 """Given default_project_prefix and comma separated list of bugs, yields bug
3119 line values.
tandriif9aefb72016-07-01 09:06:51 -07003120
3121 Each bug can be either:
Lei Zhang8a0efc12020-08-05 19:58:45 +00003122 * a number, which is combined with default_project_prefix
tandriif9aefb72016-07-01 09:06:51 -07003123 * string, which is left as is.
3124
3125 This function may produce more than one line, because bugdroid expects one
3126 project per line.
3127
Lei Zhang8a0efc12020-08-05 19:58:45 +00003128 >>> list(_get_bug_line_values('v8:', '123,chromium:789'))
tandriif9aefb72016-07-01 09:06:51 -07003129 ['v8:123', 'chromium:789']
3130 """
3131 default_bugs = []
3132 others = []
3133 for bug in bugs.split(','):
3134 bug = bug.strip()
3135 if bug:
3136 try:
3137 default_bugs.append(int(bug))
3138 except ValueError:
3139 others.append(bug)
3140
3141 if default_bugs:
3142 default_bugs = ','.join(map(str, default_bugs))
Lei Zhang8a0efc12020-08-05 19:58:45 +00003143 if default_project_prefix:
3144 if not default_project_prefix.endswith(':'):
3145 default_project_prefix += ':'
3146 yield '%s%s' % (default_project_prefix, default_bugs)
tandriif9aefb72016-07-01 09:06:51 -07003147 else:
3148 yield default_bugs
3149 for other in sorted(others):
3150 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3151 yield other
3152
3153
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003154class ChangeDescription(object):
3155 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003156 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003157 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003158 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Dan Beamd8b04ca2019-10-10 21:23:26 +00003159 FIXED_LINE = r'^[ \t]*Fixed[ \t]*:[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003160 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003161 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3162 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
Anthony Polito02b5af32019-12-02 19:49:47 +00003163 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):($|[^:])'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003164 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003165
Dan Beamd8b04ca2019-10-10 21:23:26 +00003166 def __init__(self, description, bug=None, fixed=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003167 self._description_lines = (description or '').strip().splitlines()
Anthony Polito8b955342019-09-24 19:01:36 +00003168 if bug:
3169 regexp = re.compile(self.BUG_LINE)
3170 prefix = settings.GetBugPrefix()
3171 if not any((regexp.match(line) for line in self._description_lines)):
3172 values = list(_get_bug_line_values(prefix, bug))
3173 self.append_footer('Bug: %s' % ', '.join(values))
Dan Beamd8b04ca2019-10-10 21:23:26 +00003174 if fixed:
3175 regexp = re.compile(self.FIXED_LINE)
3176 prefix = settings.GetBugPrefix()
3177 if not any((regexp.match(line) for line in self._description_lines)):
3178 values = list(_get_bug_line_values(prefix, fixed))
3179 self.append_footer('Fixed: %s' % ', '.join(values))
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003180
agable@chromium.org42c20792013-09-12 17:34:49 +00003181 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003182 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003183 return '\n'.join(self._description_lines)
3184
3185 def set_description(self, desc):
3186 if isinstance(desc, basestring):
3187 lines = desc.splitlines()
3188 else:
3189 lines = [line.rstrip() for line in desc]
3190 while lines and not lines[0]:
3191 lines.pop(0)
3192 while lines and not lines[-1]:
3193 lines.pop(-1)
3194 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003195
Edward Lemur5a644f82020-03-18 16:44:57 +00003196 def ensure_change_id(self, change_id):
3197 description = self.description
3198 footer_change_ids = git_footers.get_footer_change_id(description)
3199 # Make sure that the Change-Id in the description matches the given one.
3200 if footer_change_ids != [change_id]:
3201 if footer_change_ids:
3202 # Remove any existing Change-Id footers since they don't match the
3203 # expected change_id footer.
3204 description = git_footers.remove_footer(description, 'Change-Id')
3205 print('WARNING: Change-Id has been set to %s. Use `git cl issue 0` '
3206 'if you want to set a new one.')
3207 # Add the expected Change-Id footer.
3208 description = git_footers.add_footer_change_id(description, change_id)
3209 self.set_description(description)
3210
Joanna Wang39811b12023-01-20 23:09:48 +00003211 def update_reviewers(self, reviewers):
3212 """Rewrites the R= line(s) as a single line each.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003213
3214 Args:
3215 reviewers (list(str)) - list of additional emails to use for reviewers.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003216 """
Joanna Wang39811b12023-01-20 23:09:48 +00003217 if not reviewers:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003218 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003219
3220 reviewers = set(reviewers)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003221
Joanna Wang39811b12023-01-20 23:09:48 +00003222 # Get the set of R= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003223 regexp = re.compile(self.R_LINE)
3224 matches = [regexp.match(line) for line in self._description_lines]
3225 new_desc = [l for i, l in enumerate(self._description_lines)
3226 if not matches[i]]
3227 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003228
Joanna Wang39811b12023-01-20 23:09:48 +00003229 # Construct new unified R= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003230
Joanna Wang39811b12023-01-20 23:09:48 +00003231 # First, update reviewers with names from the R= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003232 for match in matches:
3233 if not match:
3234 continue
Joanna Wang39811b12023-01-20 23:09:48 +00003235 reviewers.update(cleanup_list([match.group(2).strip()]))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003236
Joanna Wang39811b12023-01-20 23:09:48 +00003237 new_r_line = 'R=' + ', '.join(sorted(reviewers))
agable@chromium.org42c20792013-09-12 17:34:49 +00003238
3239 # Put the new lines in the description where the old first R= line was.
3240 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3241 if 0 <= line_loc < len(self._description_lines):
Joanna Wang39811b12023-01-20 23:09:48 +00003242 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003243 else:
Joanna Wang39811b12023-01-20 23:09:48 +00003244 self.append_footer(new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003245
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00003246 def set_preserve_tryjobs(self):
3247 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
3248 footers = git_footers.parse_footers(self.description)
3249 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
3250 if v.lower() == 'true':
3251 return
3252 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
3253
Anthony Polito8b955342019-09-24 19:01:36 +00003254 def prompt(self):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003255 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003256 self.set_description([
3257 '# Enter a description of the change.',
3258 '# This will be displayed on the codereview site.',
3259 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003260 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003261 '--------------------',
3262 ] + self._description_lines)
Dan Beamd8b04ca2019-10-10 21:23:26 +00003263 bug_regexp = re.compile(self.BUG_LINE)
3264 fixed_regexp = re.compile(self.FIXED_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00003265 prefix = settings.GetBugPrefix()
Sigurd Schneider8630bb12020-11-11 14:02:49 +00003266 has_issue = lambda l: bug_regexp.match(l) or fixed_regexp.match(l)
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00003267
Dan Beamd8b04ca2019-10-10 21:23:26 +00003268 if not any((has_issue(line) for line in self._description_lines)):
Anthony Polito8b955342019-09-24 19:01:36 +00003269 self.append_footer('Bug: %s' % prefix)
tandriif9aefb72016-07-01 09:06:51 -07003270
Bruce Dawsonfc487042020-10-27 19:11:37 +00003271 print('Waiting for editor...')
agable@chromium.org42c20792013-09-12 17:34:49 +00003272 content = gclient_utils.RunEditor(self.description, True,
Edward Lemur79d4f992019-11-11 23:49:02 +00003273 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003274 if not content:
3275 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003276 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003277
Bruce Dawson2377b012018-01-11 16:46:49 -08003278 # Strip off comments and default inserted "Bug:" line.
3279 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00003280 (line.startswith('#') or
3281 line.rstrip() == "Bug:" or
3282 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00003283 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003284 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003285 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003286
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003287 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003288 """Adds a footer line to the description.
3289
3290 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3291 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3292 that Gerrit footers are always at the end.
3293 """
3294 parsed_footer_line = git_footers.parse_footer(line)
3295 if parsed_footer_line:
3296 # Line is a gerrit footer in the form: Footer-Key: any value.
3297 # Thus, must be appended observing Gerrit footer rules.
3298 self.set_description(
3299 git_footers.add_footer(self.description,
3300 key=parsed_footer_line[0],
3301 value=parsed_footer_line[1]))
3302 return
3303
3304 if not self._description_lines:
3305 self._description_lines.append(line)
3306 return
3307
3308 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3309 if gerrit_footers:
3310 # git_footers.split_footers ensures that there is an empty line before
3311 # actual (gerrit) footers, if any. We have to keep it that way.
3312 assert top_lines and top_lines[-1] == ''
3313 top_lines, separator = top_lines[:-1], top_lines[-1:]
3314 else:
3315 separator = [] # No need for separator if there are no gerrit_footers.
3316
3317 prev_line = top_lines[-1] if top_lines else ''
Josip Sokcevic7958e302023-03-01 23:02:21 +00003318 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3319 not presubmit_support.Change.TAG_LINE_RE.match(line)):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003320 top_lines.append('')
3321 top_lines.append(line)
3322 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003323
tandrii99a72f22016-08-17 14:33:24 -07003324 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003325 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003326 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003327 reviewers = [match.group(2).strip()
3328 for match in matches
3329 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003330 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003331
bradnelsond975b302016-10-23 12:20:23 -07003332 def get_cced(self):
3333 """Retrieves the list of reviewers."""
3334 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3335 cced = [match.group(2).strip() for match in matches if match]
3336 return cleanup_list(cced)
3337
Nodir Turakulov23b82142017-11-16 11:04:25 -08003338 def get_hash_tags(self):
3339 """Extracts and sanitizes a list of Gerrit hashtags."""
3340 subject = (self._description_lines or ('',))[0]
3341 subject = re.sub(
3342 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3343
3344 tags = []
3345 start = 0
3346 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3347 while True:
3348 m = bracket_exp.match(subject, start)
3349 if not m:
3350 break
3351 tags.append(self.sanitize_hash_tag(m.group(1)))
3352 start = m.end()
3353
3354 if not tags:
3355 # Try "Tag: " prefix.
3356 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3357 if m:
3358 tags.append(self.sanitize_hash_tag(m.group(1)))
3359 return tags
3360
3361 @classmethod
3362 def sanitize_hash_tag(cls, tag):
3363 """Returns a sanitized Gerrit hash tag.
3364
3365 A sanitized hashtag can be used as a git push refspec parameter value.
3366 """
3367 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3368
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003369
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003370def FindCodereviewSettingsFile(filename='codereview.settings'):
3371 """Finds the given file starting in the cwd and going up.
3372
3373 Only looks up to the top of the repository unless an
3374 'inherit-review-settings-ok' file exists in the root of the repository.
3375 """
3376 inherit_ok_file = 'inherit-review-settings-ok'
3377 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003378 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003379 if os.path.isfile(os.path.join(root, inherit_ok_file)):
Aleksey Khoroshilov2a229712022-06-02 16:24:11 +00003380 root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003381 while True:
Aleksey Khoroshilov2a229712022-06-02 16:24:11 +00003382 if os.path.isfile(os.path.join(cwd, filename)):
3383 return open(os.path.join(cwd, filename))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003384 if cwd == root:
3385 break
Aleksey Khoroshilov2a229712022-06-02 16:24:11 +00003386 parent_dir = os.path.dirname(cwd)
3387 if parent_dir == cwd:
3388 # We hit the system root directory.
3389 break
3390 cwd = parent_dir
3391 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003392
3393
3394def LoadCodereviewSettingsFromFile(fileobj):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003395 """Parses a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003396 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003397
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003398 def SetProperty(name, setting, unset_error_ok=False):
3399 fullname = 'rietveld.' + name
3400 if setting in keyvals:
3401 RunGit(['config', fullname, keyvals[setting]])
3402 else:
3403 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3404
tandrii48df5812016-10-17 03:55:37 -07003405 if not keyvals.get('GERRIT_HOST', False):
3406 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003407 # Only server setting is required. Other settings can be absent.
3408 # In that case, we ignore errors raised during option deletion attempt.
Joanna Wangc8f23e22023-01-19 21:18:10 +00003409 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003410 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3411 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003412 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003413 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3414 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003415 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3416 unset_error_ok=True)
Jamie Madilldc4d19e2019-10-24 21:50:02 +00003417 SetProperty(
3418 'format-full-by-default', 'FORMAT_FULL_BY_DEFAULT', unset_error_ok=True)
Dirk Pranke6f0df682021-06-25 00:42:33 +00003419 SetProperty('use-python3', 'USE_PYTHON3', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003420
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003421 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003422 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003423
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003424 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
Edward Lesmes4de54132020-05-05 19:41:33 +00003425 RunGit(['config', 'gerrit.squash-uploads',
3426 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003427
tandrii@chromium.org28253532016-04-14 13:46:56 +00003428 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003429 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003430 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3431
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003432 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003433 # should be of the form
3434 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3435 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003436 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3437 keyvals['ORIGIN_URL_CONFIG']])
3438
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003439
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003440def urlretrieve(source, destination):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003441 """Downloads a network object to a local file, like urllib.urlretrieve.
3442
3443 This is necessary because urllib is broken for SSL connections via a proxy.
3444 """
Vadim Shtayuraf7b8f8f2021-11-15 19:10:05 +00003445 with open(destination, 'wb') as f:
Edward Lemur79d4f992019-11-11 23:49:02 +00003446 f.write(urllib.request.urlopen(source).read())
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003447
3448
ukai@chromium.org712d6102013-11-27 00:52:58 +00003449def hasSheBang(fname):
3450 """Checks fname is a #! script."""
3451 with open(fname) as f:
3452 return f.read(2).startswith('#!')
3453
3454
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003455def DownloadGerritHook(force):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003456 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003457
3458 Args:
3459 force: True to update hooks. False to install hooks if not present.
3460 """
ukai@chromium.org712d6102013-11-27 00:52:58 +00003461 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003462 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3463 if not os.access(dst, os.X_OK):
3464 if os.path.exists(dst):
3465 if not force:
3466 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003467 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003468 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003469 if not hasSheBang(dst):
3470 DieWithError('Not a script: %s\n'
3471 'You need to download from\n%s\n'
3472 'into .git/hooks/commit-msg and '
3473 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003474 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3475 except Exception:
3476 if os.path.exists(dst):
3477 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003478 DieWithError('\nFailed to download hooks.\n'
3479 'You need to download from\n%s\n'
3480 'into .git/hooks/commit-msg and '
3481 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003482
3483
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003484class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003485 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003486
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003487 def __init__(self):
3488 # Cached list of [host, identity, source], where source is either
3489 # .gitcookies or .netrc.
3490 self._all_hosts = None
3491
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003492 def ensure_configured_gitcookies(self):
3493 """Runs checks and suggests fixes to make git use .gitcookies from default
3494 path."""
3495 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3496 configured_path = RunGitSilent(
3497 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003498 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003499 if configured_path:
3500 self._ensure_default_gitcookies_path(configured_path, default)
3501 else:
3502 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003503
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003504 @staticmethod
3505 def _ensure_default_gitcookies_path(configured_path, default_path):
3506 assert configured_path
3507 if configured_path == default_path:
3508 print('git is already configured to use your .gitcookies from %s' %
3509 configured_path)
3510 return
3511
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003512 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003513 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3514 (configured_path, default_path))
3515
3516 if not os.path.exists(configured_path):
3517 print('However, your configured .gitcookies file is missing.')
3518 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3519 action='reconfigure')
3520 RunGit(['config', '--global', 'http.cookiefile', default_path])
3521 return
3522
3523 if os.path.exists(default_path):
3524 print('WARNING: default .gitcookies file already exists %s' %
3525 default_path)
3526 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3527 default_path)
3528
3529 confirm_or_exit('Move existing .gitcookies to default location?',
3530 action='move')
3531 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003532 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003533 print('Moved and reconfigured git to use .gitcookies from %s' %
3534 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003535
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003536 @staticmethod
3537 def _configure_gitcookies_path(default_path):
3538 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3539 if os.path.exists(netrc_path):
3540 print('You seem to be using outdated .netrc for git credentials: %s' %
3541 netrc_path)
3542 print('This tool will guide you through setting up recommended '
3543 '.gitcookies store for git credentials.\n'
3544 '\n'
3545 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3546 ' git config --global --unset http.cookiefile\n'
3547 ' mv %s %s.backup\n\n' % (default_path, default_path))
3548 confirm_or_exit(action='setup .gitcookies')
3549 RunGit(['config', '--global', 'http.cookiefile', default_path])
3550 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003551
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003552 def get_hosts_with_creds(self, include_netrc=False):
3553 if self._all_hosts is None:
3554 a = gerrit_util.CookiesAuthenticator()
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003555 self._all_hosts = [(h, u, s) for h, u, s in itertools.chain((
3556 (h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.items()), (
3557 (h, u, '.gitcookies') for h, (u, _) in a.gitcookies.items()))
3558 if h.endswith(_GOOGLESOURCE)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003559
3560 if include_netrc:
3561 return self._all_hosts
3562 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3563
3564 def print_current_creds(self, include_netrc=False):
3565 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3566 if not hosts:
3567 print('No Git/Gerrit credentials found')
3568 return
Edward Lemur79d4f992019-11-11 23:49:02 +00003569 lengths = [max(map(len, (row[i] for row in hosts))) for i in range(3)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003570 header = [('Host', 'User', 'Which file'),
3571 ['=' * l for l in lengths]]
3572 for row in (header + hosts):
3573 print('\t'.join((('%%+%ds' % l) % s)
3574 for l, s in zip(lengths, row)))
3575
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003576 @staticmethod
3577 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003578 """Parses identity "git-<username>.domain" into <username> and domain."""
3579 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003580 # distinguishable from sub-domains. But we do know typical domains:
3581 if identity.endswith('.chromium.org'):
3582 domain = 'chromium.org'
3583 username = identity[:-len('.chromium.org')]
3584 else:
3585 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003586 if username.startswith('git-'):
3587 username = username[len('git-'):]
3588 return username, domain
3589
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003590 def has_generic_host(self):
3591 """Returns whether generic .googlesource.com has been configured.
3592
3593 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3594 """
3595 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003596 if host == '.' + _GOOGLESOURCE:
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003597 return True
3598 return False
3599
3600 def _get_git_gerrit_identity_pairs(self):
3601 """Returns map from canonic host to pair of identities (Git, Gerrit).
3602
3603 One of identities might be None, meaning not configured.
3604 """
3605 host_to_identity_pairs = {}
3606 for host, identity, _ in self.get_hosts_with_creds():
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003607 canonical = _canonical_git_googlesource_host(host)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003608 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3609 idx = 0 if canonical == host else 1
3610 pair[idx] = identity
3611 return host_to_identity_pairs
3612
3613 def get_partially_configured_hosts(self):
3614 return set(
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003615 (host if i1 else _canonical_gerrit_googlesource_host(host))
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003616 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003617 if None in (i1, i2) and host != '.' + _GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003618
3619 def get_conflicting_hosts(self):
3620 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003621 host
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003622 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003623 if None not in (i1, i2) and i1 != i2)
3624
3625 def get_duplicated_hosts(self):
3626 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
Marc-Antoine Ruel8e57b4b2019-10-11 01:01:36 +00003627 return set(host for host, count in counters.items() if count > 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003628
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003629
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003630 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003631 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003632 hosts = sorted(hosts)
3633 assert hosts
3634 if extra_column_func is None:
3635 extras = [''] * len(hosts)
3636 else:
3637 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003638 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3639 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003640 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003641 lines.append(tmpl % he)
3642 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003643
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003644 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003645 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003646 yield ('.googlesource.com wildcard record detected',
3647 ['Chrome Infrastructure team recommends to list full host names '
3648 'explicitly.'],
3649 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003650
3651 dups = self.get_duplicated_hosts()
3652 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003653 yield ('The following hosts were defined twice',
3654 self._format_hosts(dups),
3655 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003656
3657 partial = self.get_partially_configured_hosts()
3658 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003659 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3660 'These hosts are missing',
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003661 self._format_hosts(
3662 partial, lambda host: 'but %s defined' % _get_counterpart_host(
3663 host)), partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003664
3665 conflicting = self.get_conflicting_hosts()
3666 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003667 yield ('The following Git hosts have differing credentials from their '
3668 'Gerrit counterparts',
3669 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3670 tuple(self._get_git_gerrit_identity_pairs()[host])),
3671 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003672
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003673 def find_and_report_problems(self):
3674 """Returns True if there was at least one problem, else False."""
3675 found = False
3676 bad_hosts = set()
3677 for title, sublines, hosts in self._find_problems():
3678 if not found:
3679 found = True
3680 print('\n\n.gitcookies problem report:\n')
3681 bad_hosts.update(hosts or [])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003682 print(' %s%s' % (title, (':' if sublines else '')))
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003683 if sublines:
3684 print()
3685 print(' %s' % '\n '.join(sublines))
3686 print()
3687
3688 if bad_hosts:
3689 assert found
3690 print(' You can manually remove corresponding lines in your %s file and '
3691 'visit the following URLs with correct account to generate '
3692 'correct credential lines:\n' %
3693 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
Garrett Beaty08bb5c42022-09-21 17:34:20 +00003694 print(' %s' % '\n '.join(
3695 sorted(
3696 set(gerrit_util.CookiesAuthenticator().get_new_password_url(
3697 _canonical_git_googlesource_host(host))
3698 for host in bad_hosts))))
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003699 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003700
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003701
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003702@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003703def CMDcreds_check(parser, args):
3704 """Checks credentials and suggests changes."""
3705 _, _ = parser.parse_args(args)
3706
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003707 # Code below checks .gitcookies. Abort if using something else.
3708 authn = gerrit_util.Authenticator.get()
3709 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
Edward Lemur57d47422020-03-06 20:43:07 +00003710 message = (
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003711 'This command is not designed for bot environment. It checks '
3712 '~/.gitcookies file not generally used on bots.')
Edward Lemur57d47422020-03-06 20:43:07 +00003713 # TODO(crbug.com/1059384): Automatically detect when running on cloudtop.
3714 if isinstance(authn, gerrit_util.GceAuthenticator):
3715 message += (
3716 '\n'
3717 'If you need to run this on GCE or a cloudtop instance, '
3718 'export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
3719 DieWithError(message)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003720
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003721 checker = _GitCookiesChecker()
3722 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003723
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003724 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003725 checker.print_current_creds(include_netrc=True)
3726
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003727 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003728 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003729 return 0
3730 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003731
3732
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003733@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003734def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003735 """Gets or sets base-url for this branch."""
Thiago Perrotta16d08f02022-07-20 18:18:50 +00003736 _, args = parser.parse_args(args)
Edward Lesmes50da7702020-03-30 19:23:43 +00003737 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
Edward Lemur85153282020-02-14 22:06:29 +00003738 branch = scm.GIT.ShortBranchName(branchref)
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003739 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003740 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003741 return RunGit(['config', 'branch.%s.base-url' % branch],
3742 error_ok=False).strip()
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003743
3744 print('Setting base-url to %s' % args[0])
3745 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3746 error_ok=False).strip()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003747
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003748
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003749def color_for_status(status):
3750 """Maps a Changelist status to color, for CMDstatus and other tools."""
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00003751 BOLD = '\033[1m'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003752 return {
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00003753 'unsent': BOLD + Fore.YELLOW,
3754 'waiting': BOLD + Fore.RED,
3755 'reply': BOLD + Fore.YELLOW,
3756 'not lgtm': BOLD + Fore.RED,
3757 'lgtm': BOLD + Fore.GREEN,
3758 'commit': BOLD + Fore.MAGENTA,
3759 'closed': BOLD + Fore.CYAN,
3760 'error': BOLD + Fore.WHITE,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003761 }.get(status, Fore.WHITE)
3762
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003763
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003764def get_cl_statuses(changes, fine_grained, max_processes=None):
3765 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003766
3767 If fine_grained is true, this will fetch CL statuses from the server.
3768 Otherwise, simply indicate if there's a matching url for the given branches.
3769
3770 If max_processes is specified, it is used as the maximum number of processes
3771 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3772 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003773
3774 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003775 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003776 if not changes:
Edward Lemur61bf4172020-02-24 23:22:37 +00003777 return
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003778
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003779 if not fine_grained:
3780 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003781 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003782 for cl in changes:
3783 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003784 return
3785
3786 # First, sort out authentication issues.
3787 logging.debug('ensuring credentials exist')
3788 for cl in changes:
3789 cl.EnsureAuthenticated(force=False, refresh=True)
3790
3791 def fetch(cl):
3792 try:
3793 return (cl, cl.GetStatus())
3794 except:
3795 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003796 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003797 raise
3798
3799 threads_count = len(changes)
3800 if max_processes:
3801 threads_count = max(1, min(threads_count, max_processes))
3802 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3803
Edward Lemur61bf4172020-02-24 23:22:37 +00003804 pool = multiprocessing.pool.ThreadPool(threads_count)
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003805 fetched_cls = set()
3806 try:
3807 it = pool.imap_unordered(fetch, changes).__iter__()
3808 while True:
3809 try:
3810 cl, status = it.next(timeout=5)
Edward Lemur61bf4172020-02-24 23:22:37 +00003811 except (multiprocessing.TimeoutError, StopIteration):
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003812 break
3813 fetched_cls.add(cl)
3814 yield cl, status
3815 finally:
3816 pool.close()
3817
3818 # Add any branches that failed to fetch.
3819 for cl in set(changes) - fetched_cls:
3820 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003821
rmistry@google.com2dd99862015-06-22 12:22:18 +00003822
Jose Lopes3863fc52020-04-07 17:00:25 +00003823def upload_branch_deps(cl, args, force=False):
rmistry@google.com2dd99862015-06-22 12:22:18 +00003824 """Uploads CLs of local branches that are dependents of the current branch.
3825
3826 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003827
3828 test1 -> test2.1 -> test3.1
3829 -> test3.2
3830 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003831
3832 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3833 run on the dependent branches in this order:
3834 test2.1, test3.1, test3.2, test2.2, test3.3
3835
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003836 Note: This function does not rebase your local dependent branches. Use it
3837 when you make a change to the parent branch that will not conflict
3838 with its dependent branches, and you would like their dependencies
3839 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003840 """
3841 if git_common.is_dirty_git_tree('upload-branch-deps'):
3842 return 1
3843
3844 root_branch = cl.GetBranch()
3845 if root_branch is None:
3846 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3847 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003848 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003849 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3850 'patchset dependencies without an uploaded CL.')
3851
3852 branches = RunGit(['for-each-ref',
3853 '--format=%(refname:short) %(upstream:short)',
3854 'refs/heads'])
3855 if not branches:
3856 print('No local branches found.')
3857 return 0
3858
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003859 # Create a dictionary of all local branches to the branches that are
3860 # dependent on it.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003861 tracked_to_dependents = collections.defaultdict(list)
3862 for b in branches.splitlines():
3863 tokens = b.split()
3864 if len(tokens) == 2:
3865 branch_name, tracked = tokens
3866 tracked_to_dependents[tracked].append(branch_name)
3867
vapiera7fbd5a2016-06-16 09:17:49 -07003868 print()
3869 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003870 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003871
rmistry@google.com2dd99862015-06-22 12:22:18 +00003872 def traverse_dependents_preorder(branch, padding=''):
3873 dependents_to_process = tracked_to_dependents.get(branch, [])
3874 padding += ' '
3875 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003876 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003877 dependents.append(dependent)
3878 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003879
rmistry@google.com2dd99862015-06-22 12:22:18 +00003880 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003881 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003882
3883 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003884 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003885 return 0
3886
rmistry@google.com2dd99862015-06-22 12:22:18 +00003887 # Record all dependents that failed to upload.
3888 failures = {}
3889 # Go through all dependents, checkout the branch and upload.
3890 try:
3891 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003892 print()
3893 print('--------------------------------------')
3894 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003895 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003896 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003897 try:
3898 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003899 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003900 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003901 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003902 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003903 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003904 finally:
3905 # Swap back to the original root branch.
3906 RunGit(['checkout', '-q', root_branch])
3907
vapiera7fbd5a2016-06-16 09:17:49 -07003908 print()
3909 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003910 for dependent_branch in dependents:
3911 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003912 print(' %s : %s' % (dependent_branch, upload_status))
3913 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003914
3915 return 0
3916
3917
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003918def GetArchiveTagForBranch(issue_num, branch_name, existing_tags, pattern):
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003919 """Given a proposed tag name, returns a tag name that is guaranteed to be
3920 unique. If 'foo' is proposed but already exists, then 'foo-2' is used,
3921 or 'foo-3', and so on."""
3922
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003923 proposed_tag = pattern.format(**{'issue': issue_num, 'branch': branch_name})
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003924 for suffix_num in itertools.count(1):
3925 if suffix_num == 1:
3926 to_check = proposed_tag
3927 else:
3928 to_check = '%s-%d' % (proposed_tag, suffix_num)
3929
3930 if to_check not in existing_tags:
3931 return to_check
3932
3933
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003934@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003935def CMDarchive(parser, args):
3936 """Archives and deletes branches associated with closed changelists."""
3937 parser.add_option(
3938 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003939 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003940 parser.add_option(
3941 '-f', '--force', action='store_true',
3942 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003943 parser.add_option(
3944 '-d', '--dry-run', action='store_true',
3945 help='Skip the branch tagging and removal steps.')
3946 parser.add_option(
3947 '-t', '--notags', action='store_true',
3948 help='Do not tag archived branches. '
3949 'Note: local commit history may be lost.')
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003950 parser.add_option(
3951 '-p',
3952 '--pattern',
3953 default='git-cl-archived-{issue}-{branch}',
3954 help='Format string for archive tags. '
3955 'E.g. \'archived-{issue}-{branch}\'.')
kmarshall3bff56b2016-06-06 18:31:47 -07003956
kmarshall3bff56b2016-06-06 18:31:47 -07003957 options, args = parser.parse_args(args)
3958 if args:
3959 parser.error('Unsupported args: %s' % ' '.join(args))
kmarshall3bff56b2016-06-06 18:31:47 -07003960
3961 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3962 if not branches:
3963 return 0
3964
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00003965 tags = RunGit(['for-each-ref', '--format=%(refname)',
3966 'refs/tags']).splitlines() or []
3967 tags = [t.split('/')[-1] for t in tags]
3968
vapiera7fbd5a2016-06-16 09:17:49 -07003969 print('Finding all branches associated with closed issues...')
Edward Lemur934836a2019-09-09 20:16:54 +00003970 changes = [Changelist(branchref=b)
3971 for b in branches.splitlines()]
kmarshall3bff56b2016-06-06 18:31:47 -07003972 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3973 statuses = get_cl_statuses(changes,
3974 fine_grained=True,
3975 max_processes=options.maxjobs)
3976 proposal = [(cl.GetBranch(),
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00003977 GetArchiveTagForBranch(cl.GetIssue(), cl.GetBranch(), tags,
3978 options.pattern))
kmarshall3bff56b2016-06-06 18:31:47 -07003979 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00003980 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07003981 proposal.sort()
3982
3983 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003984 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003985 return 0
3986
Edward Lemur85153282020-02-14 22:06:29 +00003987 current_branch = scm.GIT.GetBranch(settings.GetRoot())
kmarshall3bff56b2016-06-06 18:31:47 -07003988
vapiera7fbd5a2016-06-16 09:17:49 -07003989 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003990 if options.notags:
3991 for next_item in proposal:
3992 print(' ' + next_item[0])
3993 else:
3994 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3995 for next_item in proposal:
3996 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003997
kmarshall9249e012016-08-23 12:02:16 -07003998 # Quit now on precondition failure or if instructed by the user, either
3999 # via an interactive prompt or by command line flags.
4000 if options.dry_run:
4001 print('\nNo changes were made (dry run).\n')
4002 return 0
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00004003
4004 if any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004005 print('You are currently on a branch \'%s\' which is associated with a '
4006 'closed codereview issue, so archive cannot proceed. Please '
4007 'checkout another branch and run this command again.' %
4008 current_branch)
4009 return 1
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00004010
4011 if not options.force:
Edward Lesmesae3586b2020-03-23 21:21:14 +00004012 answer = gclient_utils.AskForData('\nProceed with deletion (Y/n)? ').lower()
sergiyb4a5ecbe2016-06-20 09:46:00 -07004013 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004014 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004015 return 1
4016
4017 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004018 if not options.notags:
4019 RunGit(['tag', tagname, branch])
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00004020
4021 if RunGitWithCode(['branch', '-D', branch])[0] != 0:
4022 # Clean up the tag if we failed to delete the branch.
4023 RunGit(['tag', '-d', tagname])
kmarshall9249e012016-08-23 12:02:16 -07004024
vapiera7fbd5a2016-06-16 09:17:49 -07004025 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004026
4027 return 0
4028
4029
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004030@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004031def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004032 """Show status of changelists.
4033
4034 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004035 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004036 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004037 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004038 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004039 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004040 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004041 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004042
4043 Also see 'git cl comments'.
4044 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00004045 parser.add_option(
4046 '--no-branch-color',
4047 action='store_true',
4048 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004049 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004050 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004051 parser.add_option('-f', '--fast', action='store_true',
4052 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004053 parser.add_option(
4054 '-j', '--maxjobs', action='store', type=int,
4055 help='The maximum number of jobs to use when retrieving review status')
Edward Lemur52969c92020-02-06 18:15:28 +00004056 parser.add_option(
4057 '-i', '--issue', type=int,
4058 help='Operate on this issue instead of the current branch\'s implicit '
4059 'issue. Requires --field to be set.')
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00004060 parser.add_option('-d',
4061 '--date-order',
4062 action='store_true',
4063 help='Order branches by committer date.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004064 options, args = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004065 if args:
4066 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004067
iannuccie53c9352016-08-17 14:40:40 -07004068 if options.issue is not None and not options.field:
Edward Lemur6c6827c2020-02-06 21:15:18 +00004069 parser.error('--field must be given when --issue is set.')
iannucci3c972b92016-08-17 13:24:10 -07004070
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004071 if options.field:
Edward Lemur934836a2019-09-09 20:16:54 +00004072 cl = Changelist(issue=options.issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004073 if options.field.startswith('desc'):
Edward Lemur6c6827c2020-02-06 21:15:18 +00004074 if cl.GetIssue():
4075 print(cl.FetchDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004076 elif options.field == 'id':
4077 issueid = cl.GetIssue()
4078 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004079 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004080 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004081 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004082 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004083 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004084 elif options.field == 'status':
4085 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004086 elif options.field == 'url':
4087 url = cl.GetIssueURL()
4088 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004089 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004090 return 0
4091
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00004092 branches = RunGit([
4093 'for-each-ref', '--format=%(refname) %(committerdate:unix)', 'refs/heads'
4094 ])
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004095 if not branches:
4096 print('No local branch found.')
4097 return 0
4098
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004099 changes = [
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00004100 Changelist(branchref=b, commit_date=ct)
4101 for b, ct in map(lambda line: line.split(' '), branches.splitlines())
4102 ]
vapiera7fbd5a2016-06-16 09:17:49 -07004103 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004104 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004105 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004106 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004107
Edward Lemur85153282020-02-14 22:06:29 +00004108 current_branch = scm.GIT.GetBranch(settings.GetRoot())
Daniel McArdlea23bf592019-02-12 00:25:12 +00004109
4110 def FormatBranchName(branch, colorize=False):
4111 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
4112 an asterisk when it is the current branch."""
4113
4114 asterisk = ""
4115 color = Fore.RESET
4116 if branch == current_branch:
4117 asterisk = "* "
4118 color = Fore.GREEN
Edward Lemur85153282020-02-14 22:06:29 +00004119 branch_name = scm.GIT.ShortBranchName(branch)
Daniel McArdlea23bf592019-02-12 00:25:12 +00004120
4121 if colorize:
4122 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00004123 return asterisk + branch_name
4124
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004125 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00004126
4127 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +00004128
4129 if options.date_order or settings.IsStatusCommitOrderByDate():
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00004130 sorted_changes = sorted(changes,
4131 key=lambda c: c.GetCommitDate(),
4132 reverse=True)
4133 else:
4134 sorted_changes = sorted(changes, key=lambda c: c.GetBranch())
4135 for cl in sorted_changes:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004136 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004137 while branch not in branch_statuses:
Edward Lemur79d4f992019-11-11 23:49:02 +00004138 c, status = next(output)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004139 branch_statuses[c.GetBranch()] = status
4140 status = branch_statuses.pop(branch)
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +00004141 url = cl.GetIssueURL(short=True)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004142 if url and (not status or status == 'error'):
4143 # The issue probably doesn't exist anymore.
4144 url += ' (broken)'
4145
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004146 color = color_for_status(status)
Bruce Dawsonb73f8a92020-03-27 22:03:08 +00004147 # Turn off bold as well as colors.
4148 END = '\033[0m'
4149 reset = Fore.RESET + END
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004150 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004151 color = ''
4152 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004153 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00004154
Alan Cuttera3be9a52019-03-04 18:50:33 +00004155 branch_display = FormatBranchName(branch)
4156 padding = ' ' * (alignment - len(branch_display))
4157 if not options.no_branch_color:
4158 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00004159
Alan Cuttera3be9a52019-03-04 18:50:33 +00004160 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
4161 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004162
vapiera7fbd5a2016-06-16 09:17:49 -07004163 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00004164 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004165 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00004166 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004167 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004168 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004169 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004170 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004171 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004172 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004173 print('Issue description:')
Edward Lemur6c6827c2020-02-06 21:15:18 +00004174 print(cl.FetchDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004175 return 0
4176
4177
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004178def colorize_CMDstatus_doc():
4179 """To be called once in main() to add colors to git cl status help."""
4180 colors = [i for i in dir(Fore) if i[0].isupper()]
4181
4182 def colorize_line(line):
4183 for color in colors:
4184 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004185 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004186 indent = len(line) - len(line.lstrip(' ')) + 1
4187 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4188 return line
4189
4190 lines = CMDstatus.__doc__.splitlines()
4191 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4192
4193
phajdan.jre328cf92016-08-22 04:12:17 -07004194def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004195 if path == '-':
4196 json.dump(contents, sys.stdout)
4197 else:
4198 with open(path, 'w') as f:
4199 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004200
4201
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004202@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004203@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004204def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004205 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004206
4207 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004208 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004209 parser.add_option('-r', '--reverse', action='store_true',
4210 help='Lookup the branch(es) for the specified issues. If '
4211 'no issues are specified, all branches with mapped '
4212 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004213 parser.add_option('--json',
4214 help='Path to JSON output file, or "-" for stdout.')
dnj@chromium.org406c4402015-03-03 17:22:28 +00004215 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004216
dnj@chromium.org406c4402015-03-03 17:22:28 +00004217 if options.reverse:
4218 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004219 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004220 # Reverse issue lookup.
4221 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004222
4223 git_config = {}
4224 for config in RunGit(['config', '--get-regexp',
4225 r'branch\..*issue']).splitlines():
4226 name, _space, val = config.partition(' ')
4227 git_config[name] = val
4228
dnj@chromium.org406c4402015-03-03 17:22:28 +00004229 for branch in branches:
Edward Lesmes50da7702020-03-30 19:23:43 +00004230 issue = git_config.get(
4231 'branch.%s.%s' % (scm.GIT.ShortBranchName(branch), ISSUE_CONFIG_KEY))
Edward Lemur52969c92020-02-06 18:15:28 +00004232 if issue:
4233 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004234 if not args:
Carlos Caballero81923d62020-07-06 18:22:27 +00004235 args = sorted(issue_branch_map.keys())
phajdan.jre328cf92016-08-22 04:12:17 -07004236 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004237 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00004238 try:
4239 issue_num = int(issue)
4240 except ValueError:
4241 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004242 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00004243 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07004244 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00004245 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004246 if options.json:
4247 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004248 return 0
4249
4250 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00004251 issue = ParseIssueNumberArgument(args[0])
Aaron Gable78753da2017-06-15 10:35:49 -07004252 if not issue.valid:
4253 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4254 'or no argument to list it.\n'
4255 'Maybe you want to run git cl status?')
Edward Lemurf38bc172019-09-03 21:02:13 +00004256 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07004257 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004258 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00004259 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07004260 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4261 if options.json:
4262 write_json(options.json, {
Nodir Turakulov27379632021-03-17 18:53:29 +00004263 'gerrit_host': cl.GetGerritHost(),
4264 'gerrit_project': cl.GetGerritProject(),
Aaron Gable78753da2017-06-15 10:35:49 -07004265 'issue_url': cl.GetIssueURL(),
Nodir Turakulov27379632021-03-17 18:53:29 +00004266 'issue': cl.GetIssue(),
Aaron Gable78753da2017-06-15 10:35:49 -07004267 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004268 return 0
4269
4270
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004271@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004272def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004273 """Shows or posts review comments for any changelist."""
4274 parser.add_option('-a', '--add-comment', dest='comment',
4275 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004276 parser.add_option('-p', '--publish', action='store_true',
4277 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004278 parser.add_option('-i', '--issue', dest='issue',
Edward Lemurf38bc172019-09-03 21:02:13 +00004279 help='review issue id (defaults to current issue).')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004280 parser.add_option('-m', '--machine-readable', dest='readable',
4281 action='store_false', default=True,
4282 help='output comments in a format compatible with '
4283 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004284 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004285 help='File to write JSON summary to, or "-" for stdout')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004286 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004287
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004288 issue = None
4289 if options.issue:
4290 try:
4291 issue = int(options.issue)
4292 except ValueError:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004293 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004294
Edward Lemur934836a2019-09-09 20:16:54 +00004295 cl = Changelist(issue=issue)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004296
4297 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004298 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004299 return 0
4300
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004301 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4302 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004303 for comment in summary:
4304 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004305 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004306 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004307 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004308 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004309 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00004310 elif comment.autogenerated:
4311 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004312 else:
4313 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004314 print('\n%s%s %s%s\n%s' % (
4315 color,
4316 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4317 comment.sender,
4318 Fore.RESET,
4319 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4320
smut@google.comc85ac942015-09-15 16:34:43 +00004321 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004322 def pre_serialize(c):
Edward Lemur79d4f992019-11-11 23:49:02 +00004323 dct = c._asdict().copy()
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004324 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4325 return dct
Edward Lemur79d4f992019-11-11 23:49:02 +00004326 write_json(options.json_file, [pre_serialize(x) for x in summary])
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004327 return 0
4328
4329
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004330@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004331@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004332def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004333 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004334 parser.add_option('-d', '--display', action='store_true',
4335 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004336 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004337 help='New description to set for this issue (- for stdin, '
4338 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004339 parser.add_option('-f', '--force', action='store_true',
4340 help='Delete any unpublished Gerrit edits for this issue '
4341 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004342
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004343 options, args = parser.parse_args(args)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004344
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004345 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004346 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00004347 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004348 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004349 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004350
Edward Lemur934836a2019-09-09 20:16:54 +00004351 kwargs = {}
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004352 if target_issue_arg:
4353 kwargs['issue'] = target_issue_arg.issue
4354 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07004355
4356 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004357 if not cl.GetIssue():
4358 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004359
Edward Lemur678a6842019-10-03 22:25:05 +00004360 if args and not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00004361 logging.info('canonical issue/change URL: %s\n', cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004362
Edward Lemur6c6827c2020-02-06 21:15:18 +00004363 description = ChangeDescription(cl.FetchDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004364
smut@google.com34fb6b12015-07-13 20:03:26 +00004365 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004366 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004367 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004368
4369 if options.new_description:
4370 text = options.new_description
4371 if text == '-':
4372 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004373 elif text == '+':
4374 base_branch = cl.GetCommonAncestorWithUpstream()
Edward Lemura12175c2020-03-09 16:58:26 +00004375 text = _create_description_from_log([base_branch])
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004376
4377 description.set_description(text)
4378 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00004379 description.prompt()
Edward Lemur6c6827c2020-02-06 21:15:18 +00004380 if cl.FetchDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004381 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004382 return 0
4383
4384
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004385@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004386def CMDlint(parser, args):
4387 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004388 parser.add_option('--filter', action='append', metavar='-x,+y',
4389 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004390 options, args = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004391
4392 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004393 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004394 try:
4395 import cpplint
4396 import cpplint_chromium
4397 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004398 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004399 return 1
4400
4401 # Change the current working directory before calling lint so that it
4402 # shows the correct base.
4403 previous_cwd = os.getcwd()
4404 os.chdir(settings.GetRoot())
4405 try:
Edward Lemur934836a2019-09-09 20:16:54 +00004406 cl = Changelist()
Edward Lemur2c62b332020-03-12 22:12:33 +00004407 files = cl.GetAffectedFiles(cl.GetCommonAncestorWithUpstream())
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004408 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004409 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004410 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004411
Lei Zhangb8c62cf2020-07-15 20:09:37 +00004412 # Process cpplint arguments, if any.
4413 filters = presubmit_canned_checks.GetCppLintFilters(options.filter)
4414 command = ['--filter=' + ','.join(filters)] + args + files
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004415 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004416
Lei Zhang379d1ad2020-07-15 19:40:06 +00004417 include_regex = re.compile(settings.GetLintRegex())
4418 ignore_regex = re.compile(settings.GetLintIgnoreRegex())
thestig@chromium.org44202a22014-03-11 19:22:18 +00004419 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4420 for filename in filenames:
Lei Zhang379d1ad2020-07-15 19:40:06 +00004421 if not include_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004422 print('Skipping file %s' % filename)
Lei Zhang379d1ad2020-07-15 19:40:06 +00004423 continue
4424
4425 if ignore_regex.match(filename):
4426 print('Ignoring file %s' % filename)
4427 continue
4428
4429 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4430 extra_check_functions)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004431 finally:
4432 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004433 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004434 if cpplint._cpplint_state.error_count != 0:
4435 return 1
4436 return 0
4437
4438
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004439@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004440def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004441 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004442 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004443 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004444 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004445 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004446 parser.add_option('--all', action='store_true',
4447 help='Run checks against all files, not just modified ones')
Josip Sokcevic017544d2022-03-31 23:47:53 +00004448 parser.add_option('--files',
4449 nargs=1,
4450 help='Semicolon-separated list of files to be marked as '
4451 'modified when executing presubmit or post-upload hooks. '
4452 'fnmatch wildcards can also be used.')
Edward Lesmes8e282792018-04-03 18:50:29 -04004453 parser.add_option('--parallel', action='store_true',
4454 help='Run all tests specified by input_api.RunTests in all '
4455 'PRESUBMIT files in parallel.')
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00004456 parser.add_option('--resultdb', action='store_true',
4457 help='Run presubmit checks in the ResultSink environment '
4458 'and send results to the ResultDB database.')
Saagar Sanghavi03b15132020-08-10 16:43:41 +00004459 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004460 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004461
sbc@chromium.org71437c02015-04-09 19:29:40 +00004462 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004463 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004464 return 1
4465
Edward Lemur934836a2019-09-09 20:16:54 +00004466 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004467 if args:
4468 base_branch = args[0]
4469 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004470 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004471 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004472
Bruce Dawsoneb8426e2022-08-05 23:58:15 +00004473 start = time.time()
4474 try:
4475 if not 'PRESUBMIT_SKIP_NETWORK' in os.environ and cl.GetIssue():
4476 description = cl.FetchDescription()
4477 else:
4478 description = _create_description_from_log([base_branch])
4479 except Exception as e:
4480 print('Failed to fetch CL description - %s' % str(e))
Edward Lemura12175c2020-03-09 16:58:26 +00004481 description = _create_description_from_log([base_branch])
Bruce Dawsoneb8426e2022-08-05 23:58:15 +00004482 elapsed = time.time() - start
4483 if elapsed > 5:
4484 print('%.1f s to get CL description.' % elapsed)
Aaron Gable8076c282017-11-29 14:39:41 -08004485
Bruce Dawson13acea32022-05-03 22:13:08 +00004486 if not base_branch:
4487 if not options.force:
4488 print('use --force to check even when not on a branch.')
4489 return 1
4490 base_branch = 'HEAD'
4491
Josip Sokcevic017544d2022-03-31 23:47:53 +00004492 cl.RunHook(committing=not options.upload,
4493 may_prompt=False,
4494 verbose=options.verbose,
4495 parallel=options.parallel,
4496 upstream=base_branch,
4497 description=description,
4498 all_files=options.all,
4499 files=options.files,
4500 resultdb=options.resultdb,
4501 realm=options.realm)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004502 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004503
4504
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004505def GenerateGerritChangeId(message):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004506 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004507
4508 Works the same way as
4509 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4510 but can be called on demand on all platforms.
4511
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004512 The basic idea is to generate git hash of a state of the tree, original
4513 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004514 """
4515 lines = []
4516 tree_hash = RunGitSilent(['write-tree'])
4517 lines.append('tree %s' % tree_hash.strip())
4518 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4519 if code == 0:
4520 lines.append('parent %s' % parent.strip())
4521 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4522 lines.append('author %s' % author.strip())
4523 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4524 lines.append('committer %s' % committer.strip())
4525 lines.append('')
4526 # Note: Gerrit's commit-hook actually cleans message of some lines and
4527 # whitespace. This code is not doing this, but it clearly won't decrease
4528 # entropy.
4529 lines.append(message)
4530 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00004531 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004532 return 'I%s' % change_hash.strip()
4533
4534
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004535def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004536 """Computes the remote branch ref to use for the CL.
4537
4538 Args:
4539 remote (str): The git remote for the CL.
4540 remote_branch (str): The git remote branch for the CL.
4541 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004542 """
4543 if not (remote and remote_branch):
4544 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004545
wittman@chromium.org455dc922015-01-26 20:15:50 +00004546 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004547 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004548 # refs, which are then translated into the remote full symbolic refs
4549 # below.
4550 if '/' not in target_branch:
4551 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4552 else:
4553 prefix_replacements = (
4554 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4555 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4556 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4557 )
4558 match = None
4559 for regex, replacement in prefix_replacements:
4560 match = re.search(regex, target_branch)
4561 if match:
4562 remote_branch = target_branch.replace(match.group(0), replacement)
4563 break
4564 if not match:
4565 # This is a branch path but not one we recognize; use as-is.
4566 remote_branch = target_branch
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00004567 # pylint: disable=consider-using-get
rmistry@google.comc68112d2015-03-03 12:48:06 +00004568 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00004569 # pylint: enable=consider-using-get
rmistry@google.comc68112d2015-03-03 12:48:06 +00004570 # Handle the refs that need to land in different refs.
4571 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004572
wittman@chromium.org455dc922015-01-26 20:15:50 +00004573 # Create the true path to the remote branch.
4574 # Does the following translation:
4575 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
Josip Sokcevicc39ab992020-09-24 20:09:15 +00004576 # * refs/remotes/origin/main -> refs/heads/main
wittman@chromium.org455dc922015-01-26 20:15:50 +00004577 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4578 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4579 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4580 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4581 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4582 'refs/heads/')
4583 elif remote_branch.startswith('refs/remotes/branch-heads'):
4584 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004585
wittman@chromium.org455dc922015-01-26 20:15:50 +00004586 return remote_branch
4587
4588
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004589def cleanup_list(l):
4590 """Fixes a list so that comma separated items are put as individual items.
4591
4592 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4593 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4594 """
4595 items = sum((i.split(',') for i in l), [])
4596 stripped_items = (i.strip() for i in items)
4597 return sorted(filter(None, stripped_items))
4598
4599
Aaron Gable4db38df2017-11-03 14:59:07 -07004600@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004601@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004602def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004603 """Uploads the current changelist to codereview.
4604
4605 Can skip dependency patchset uploads for a branch by running:
4606 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004607 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00004608 git config --unset branch.branch_name.skip-deps-uploads
4609 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004610
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004611 If the name of the checked out branch starts with "bug-" or "fix-" followed
4612 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02004613 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004614
4615 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004616 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004617 [git-cl] add support for hashtags
4618 Foo bar: implement foo
4619 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004620 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004621 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4622 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004623 parser.add_option('--bypass-watchlists', action='store_true',
4624 dest='bypass_watchlists',
4625 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004626 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004627 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004628 parser.add_option('--message', '-m', dest='message',
4629 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004630 parser.add_option('-b', '--bug',
4631 help='pre-populate the bug number(s) for this issue. '
4632 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004633 parser.add_option('--message-file', dest='message_file',
4634 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004635 parser.add_option('--title', '-t', dest='title',
4636 help='title for patchset')
Mario Bianuccicebfb4e2020-07-22 23:08:16 +00004637 parser.add_option('-T', '--skip-title', action='store_true',
4638 dest='skip_title',
4639 help='Use the most recent commit message as the title of '
4640 'the patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004641 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004642 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004643 help='reviewer email addresses')
4644 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004645 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004646 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004647 parser.add_option('--hashtag', dest='hashtags',
4648 action='append', default=[],
4649 help=('Gerrit hashtag for new CL; '
4650 'can be applied multiple times'))
Thiago Perrottab0fb8d52022-08-30 21:26:19 +00004651 parser.add_option('-s',
4652 '--send-mail',
4653 '--send-email',
4654 dest='send_mail',
4655 action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004656 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004657 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004658 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004659 metavar='TARGET',
4660 help='Apply CL to remote ref TARGET. ' +
Josip Sokcevicc39ab992020-09-24 20:09:15 +00004661 'Default: remote branch head, or main')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004662 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004663 help='Squash multiple commits into one')
Mike Frysingera989d552019-08-14 20:51:23 +00004664 parser.add_option('--no-squash', action='store_false', dest='squash',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004665 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004666 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004667 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004668 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4669 const='R', help='add a set of OWNERS to R')
Thiago Perrottab0fb8d52022-08-30 21:26:19 +00004670 parser.add_option('-c',
4671 '--use-commit-queue',
4672 action='store_true',
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004673 default=False,
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004674 help='tell the CQ to commit this patchset; '
Thiago Perrottab0fb8d52022-08-30 21:26:19 +00004675 'implies --send-mail')
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004676 parser.add_option('-d', '--cq-dry-run',
4677 action='store_true', default=False,
rmistry@google.comef966222015-04-07 11:15:01 +00004678 help='Send the patchset to do a CQ dry run right after '
4679 'upload.')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004680 parser.add_option(
4681 '-q',
4682 '--cq-quick-run',
4683 action='store_true',
4684 default=False,
4685 help='Send the patchset to do a CQ quick run right after '
4686 'upload (https://source.chromium.org/chromium/chromium/src/+/main:do'
4687 'cs/cq_quick_run.md) (chromium only).')
Edward Lesmes10c3dd62021-02-08 21:13:57 +00004688 parser.add_option('--set-bot-commit', action='store_true',
4689 help=optparse.SUPPRESS_HELP)
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00004690 parser.add_option('--preserve-tryjobs', action='store_true',
4691 help='instruct the CQ to let tryjobs running even after '
4692 'new patchsets are uploaded instead of canceling '
4693 'prior patchset\' tryjobs')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004694 parser.add_option('--dependencies', action='store_true',
4695 help='Uploads CLs of all the local branches that depend on '
4696 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004697 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4698 help='Sends your change to the CQ after an approval. Only '
4699 'works on repos that have the Auto-Submit label '
4700 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004701 parser.add_option('--parallel', action='store_true',
4702 help='Run all tests specified by input_api.RunTests in all '
4703 'PRESUBMIT files in parallel.')
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004704 parser.add_option('--no-autocc', action='store_true',
4705 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004706 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004707 help='Set the review private. This implies --no-autocc.')
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004708 parser.add_option('-R', '--retry-failed', action='store_true',
4709 help='Retry failed tryjobs from old patchset immediately '
4710 'after uploading new patchset. Cannot be used with '
4711 '--use-commit-queue or --cq-dry-run.')
Dan Beamd8b04ca2019-10-10 21:23:26 +00004712 parser.add_option('--fixed', '-x',
4713 help='List of bugs that will be commented on and marked '
4714 'fixed (pre-populates "Fixed:" tag). Same format as '
4715 '-b option / "Bug:" tag. If fixing several issues, '
4716 'separate with commas.')
Josipe827b0f2020-01-30 00:07:20 +00004717 parser.add_option('--edit-description', action='store_true', default=False,
4718 help='Modify description before upload. Cannot be used '
4719 'with --force. It is a noop when --no-squash is set '
4720 'or a new commit is created.')
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00004721 parser.add_option('--git-completion-helper', action="store_true",
4722 help=optparse.SUPPRESS_HELP)
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00004723 parser.add_option('-o',
4724 '--push-options',
4725 action='append',
4726 default=[],
4727 help='Transmit the given string to the server when '
4728 'performing git push (pass-through). See git-push '
4729 'documentation for more details.')
Gregory Nisbet48d9e1e2021-04-15 23:35:54 +00004730 parser.add_option('--no-add-changeid',
4731 action='store_true',
4732 dest='no_add_changeid',
4733 help='Do not add change-ids to messages.')
Brian Sheedy7326ca22022-11-02 18:36:17 +00004734 parser.add_option('--no-python2-post-upload-hooks',
4735 action='store_true',
4736 help='Only run post-upload hooks in Python 3.')
Joanna Wangd75fc882023-03-01 21:53:34 +00004737 parser.add_option('--cherry-pick-stacked',
4738 '--cp',
4739 dest='cherry_pick_stacked',
4740 action='store_true',
4741 help='If parent branch has un-uploaded updates, '
4742 'automatically skip parent branches and just upload '
4743 'the current branch cherry-pick on its parent\'s last '
4744 'uploaded commit. Allows users to skip the potential '
4745 'interactive confirmation step.')
Joanna Wanga1abbed2023-01-24 01:41:05 +00004746 # TODO(b/265929888): Add --wip option of --cl-status option.
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004747
rmistry@google.com2dd99862015-06-22 12:22:18 +00004748 orig_args = args
ukai@chromium.orge8077812012-02-03 03:41:46 +00004749 (options, args) = parser.parse_args(args)
4750
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00004751 if options.git_completion_helper:
Edward Lesmesb7db1832020-06-22 20:22:27 +00004752 print(' '.join(opt.get_opt_string() for opt in parser.option_list
4753 if opt.help != optparse.SUPPRESS_HELP))
4754 return
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00004755
sbc@chromium.org71437c02015-04-09 19:29:40 +00004756 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004757 return 1
4758
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004759 options.reviewers = cleanup_list(options.reviewers)
4760 options.cc = cleanup_list(options.cc)
4761
Josipe827b0f2020-01-30 00:07:20 +00004762 if options.edit_description and options.force:
4763 parser.error('Only one of --force and --edit-description allowed')
4764
tandriib80458a2016-06-23 12:20:07 -07004765 if options.message_file:
4766 if options.message:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004767 parser.error('Only one of --message and --message-file allowed.')
tandriib80458a2016-06-23 12:20:07 -07004768 options.message = gclient_utils.FileRead(options.message_file)
tandriib80458a2016-06-23 12:20:07 -07004769
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004770 if ([options.cq_dry_run,
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004771 options.cq_quick_run,
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004772 options.use_commit_queue,
4773 options.retry_failed].count(True) > 1):
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00004774 parser.error('Only one of --use-commit-queue, --cq-dry-run, --cq-quick-run '
4775 'or --retry-failed is allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07004776
Mario Bianuccicebfb4e2020-07-22 23:08:16 +00004777 if options.skip_title and options.title:
4778 parser.error('Only one of --title and --skip-title allowed.')
4779
Aaron Gableedbc4132017-09-11 13:22:28 -07004780 if options.use_commit_queue:
4781 options.send_mail = True
4782
Edward Lesmes0dd54822020-03-26 18:24:25 +00004783 if options.squash is None:
4784 # Load default for user, repo, squash=true, in this order.
4785 options.squash = settings.GetSquashGerritUploads()
4786
Joanna Wang5051ffe2023-03-01 22:24:07 +00004787 cl = Changelist(branchref=options.target_branch)
4788
4789 # Warm change details cache now to avoid RPCs later, reducing latency for
4790 # developers.
4791 if cl.GetIssue():
4792 cl._GetChangeDetail(
4793 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
4794
4795 if options.retry_failed and not cl.GetIssue():
4796 print('No previous patchsets, so --retry-failed has no effect.')
4797 options.retry_failed = False
4798
4799 remote = cl.GetRemoteUrl()
4800 dogfood_stacked_changes = (os.environ.get('DOGFOOD_STACKED_CHANGES')
4801 not in ['1', '0']
4802 and any(repo in remote
4803 for repo in DOGFOOD_STACKED_CHANGES_REPOS))
4804
4805 if dogfood_stacked_changes:
4806 print('This repo has been enrolled in the stacked changes dogfood. '
4807 'To opt-out use `export DOGFOOD_STACKED_CHANGES=0`. '
4808 'File bugs at https://bit.ly/3Y6opoI')
4809
4810 if options.squash and (dogfood_stacked_changes
4811 or os.environ.get('DOGFOOD_STACKED_CHANGES') == '1'):
Joanna Wangdd12deb2023-01-26 20:43:28 +00004812 if options.dependencies:
4813 parser.error('--dependencies is not available for this workflow.')
Joanna Wang18de1f62023-01-21 01:24:24 +00004814
Joanna Wangd75fc882023-03-01 21:53:34 +00004815 if options.cherry_pick_stacked:
4816 try:
4817 orig_args.remove('--cherry-pick-stacked')
4818 except ValueError:
4819 orig_args.remove('--cp')
Joanna Wang18de1f62023-01-21 01:24:24 +00004820 UploadAllSquashed(options, orig_args)
4821 return 0
4822
Joanna Wangd75fc882023-03-01 21:53:34 +00004823 if options.cherry_pick_stacked:
4824 parser.error('--cherry-pick-stacked is not available for this workflow.')
4825
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004826 # cl.GetMostRecentPatchset uses cached information, and can return the last
4827 # patchset before upload. Calling it here makes it clear that it's the
4828 # last patchset before upload. Note that GetMostRecentPatchset will fail
4829 # if no CL has been uploaded yet.
4830 if options.retry_failed:
4831 patchset = cl.GetMostRecentPatchset()
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004832
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004833 ret = cl.CMDUpload(options, args, orig_args)
4834
4835 if options.retry_failed:
4836 if ret != 0:
4837 print('Upload failed, so --retry-failed has no effect.')
4838 return ret
Joanna Wanga8db0cb2023-01-24 15:43:17 +00004839 builds, _ = _fetch_latest_builds(cl,
4840 DEFAULT_BUILDBUCKET_HOST,
4841 latest_patchset=patchset)
Edward Lemur45768512020-03-02 19:03:14 +00004842 jobs = _filter_failed_for_retry(builds)
4843 if len(jobs) == 0:
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004844 print('No failed tryjobs, so --retry-failed has no effect.')
4845 return ret
Quinten Yearsley777660f2020-03-04 23:37:06 +00004846 _trigger_tryjobs(cl, jobs, options, patchset + 1)
Quinten Yearsleya19d3532019-09-30 21:54:39 +00004847
4848 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00004849
4850
Joanna Wang18de1f62023-01-21 01:24:24 +00004851def UploadAllSquashed(options, orig_args):
4852 # type: (optparse.Values, Sequence[str]) -> Tuple[Sequence[Changelist], bool]
4853 """Uploads the current and upstream branches (if necessary)."""
Joanna Wangc710e2d2023-01-25 14:53:22 +00004854 cls, cherry_pick_current = _UploadAllPrecheck(options, orig_args)
Joanna Wang18de1f62023-01-21 01:24:24 +00004855
Joanna Wangc710e2d2023-01-25 14:53:22 +00004856 # Create commits.
4857 uploads_by_cl = [] #type: Sequence[Tuple[Changelist, _NewUpload]]
4858 if cherry_pick_current:
4859 parent = cls[1]._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
4860 new_upload = cls[0].PrepareCherryPickSquashedCommit(options, parent)
4861 uploads_by_cl.append((cls[0], new_upload))
4862 else:
Joanna Wangc710e2d2023-01-25 14:53:22 +00004863 ordered_cls = list(reversed(cls))
4864
Joanna Wang6215dd02023-02-07 15:58:03 +00004865 cl = ordered_cls[0]
Joanna Wang7603f042023-03-01 22:17:36 +00004866 # We can only support external changes when we're only uploading one
4867 # branch.
4868 parent = cl._UpdateWithExternalChanges() if len(ordered_cls) == 1 else None
4869 if parent is None:
4870 origin = '.'
4871 branch = cl.GetBranch()
Joanna Wang74c53b62023-03-01 22:00:22 +00004872
Joanna Wang7603f042023-03-01 22:17:36 +00004873 while origin == '.':
4874 # Search for cl's closest ancestor with a gerrit hash.
4875 origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(branch)
4876 if origin == '.':
4877 upstream_branch = scm.GIT.ShortBranchName(upstream_branch_ref)
4878
4879 # Support the `git merge` and `git pull` workflow.
4880 if upstream_branch in ['master', 'main']:
4881 parent = cl.GetCommonAncestorWithUpstream()
4882 else:
4883 parent = scm.GIT.GetBranchConfig(settings.GetRoot(),
4884 upstream_branch,
4885 GERRIT_SQUASH_HASH_CONFIG_KEY)
4886 if parent:
4887 break
4888 branch = upstream_branch
4889 else:
4890 # Either the root of the tree is the cl's direct parent and the while
4891 # loop above only found empty branches between cl and the root of the
4892 # tree.
4893 parent = cl.GetCommonAncestorWithUpstream()
Joanna Wang6215dd02023-02-07 15:58:03 +00004894
Joanna Wangc710e2d2023-01-25 14:53:22 +00004895 for i, cl in enumerate(ordered_cls):
4896 # If we're in the middle of the stack, set end_commit to downstream's
4897 # direct ancestor.
4898 if i + 1 < len(ordered_cls):
4899 child_base_commit = ordered_cls[i + 1].GetCommonAncestorWithUpstream()
4900 else:
4901 child_base_commit = None
4902 new_upload = cl.PrepareSquashedCommit(options,
Joanna Wang6215dd02023-02-07 15:58:03 +00004903 parent,
Joanna Wangc710e2d2023-01-25 14:53:22 +00004904 end_commit=child_base_commit)
4905 uploads_by_cl.append((cl, new_upload))
Joanna Wangc710e2d2023-01-25 14:53:22 +00004906 parent = new_upload.commit_to_push
4907
4908 # Create refspec options
4909 cl, new_upload = uploads_by_cl[-1]
4910 refspec_opts = cl._GetRefSpecOptions(
4911 options,
4912 new_upload.change_desc,
Joanna Wang562481d2023-01-26 21:57:14 +00004913 multi_change_upload=len(uploads_by_cl) > 1,
4914 dogfood_path=True)
Joanna Wangc710e2d2023-01-25 14:53:22 +00004915 refspec_suffix = ''
4916 if refspec_opts:
4917 refspec_suffix = '%' + ','.join(refspec_opts)
4918 assert ' ' not in refspec_suffix, ('spaces not allowed in refspec: "%s"' %
4919 refspec_suffix)
4920
4921 remote, remote_branch = cl.GetRemoteBranch()
4922 branch = GetTargetRef(remote, remote_branch, options.target_branch)
4923 refspec = '%s:refs/for/%s%s' % (new_upload.commit_to_push, branch,
4924 refspec_suffix)
4925
4926 # Git push
4927 git_push_metadata = {
4928 'gerrit_host':
4929 cl.GetGerritHost(),
4930 'title':
4931 options.title or '<untitled>',
4932 'change_id':
4933 git_footers.get_footer_change_id(new_upload.change_desc.description),
4934 'description':
4935 new_upload.change_desc.description,
4936 }
4937 push_stdout = cl._RunGitPushWithTraces(refspec, refspec_opts,
4938 git_push_metadata)
4939
4940 # Post push updates
4941 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
4942 change_numbers = [
4943 m.group(1) for m in map(regex.match, push_stdout.splitlines()) if m
4944 ]
4945
4946 for i, (cl, new_upload) in enumerate(uploads_by_cl):
4947 cl.PostUploadUpdates(options, new_upload, change_numbers[i])
4948
4949 return 0
Joanna Wang18de1f62023-01-21 01:24:24 +00004950
4951
4952def _UploadAllPrecheck(options, orig_args):
4953 # type: (optparse.Values, Sequence[str]) -> Tuple[Sequence[Changelist], bool]
4954 """Checks the state of the tree and gives the user uploading options
4955
4956 Returns: A tuple of the ordered list of changes that have new commits
4957 since their last upload and a boolean of whether the user wants to
4958 cherry-pick and upload the current branch instead of uploading all cls.
4959 """
Joanna Wang6b98cdc2023-02-16 00:37:20 +00004960 cl = Changelist()
4961 if cl.GetBranch() is None:
4962 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
4963
Joanna Wang18de1f62023-01-21 01:24:24 +00004964 branch_ref = None
4965 cls = []
4966 must_upload_upstream = False
Joanna Wang6215dd02023-02-07 15:58:03 +00004967 first_pass = True
Joanna Wang18de1f62023-01-21 01:24:24 +00004968
4969 Changelist._GerritCommitMsgHookCheck(offer_removal=not options.force)
4970
4971 while True:
4972 if len(cls) > _MAX_STACKED_BRANCHES_UPLOAD:
4973 DieWithError(
4974 'More than %s branches in the stack have not been uploaded.\n'
4975 'Are your branches in a misconfigured state?\n'
4976 'If not, please upload some upstream changes first.' %
4977 (_MAX_STACKED_BRANCHES_UPLOAD))
4978
4979 cl = Changelist(branchref=branch_ref)
Joanna Wang18de1f62023-01-21 01:24:24 +00004980
Joanna Wang6215dd02023-02-07 15:58:03 +00004981 # Only add CL if it has anything to commit.
4982 base_commit = cl.GetCommonAncestorWithUpstream()
4983 end_commit = RunGit(['rev-parse', cl.GetBranchRef()]).strip()
4984
4985 diff = RunGitSilent(['diff', '%s..%s' % (base_commit, end_commit)])
4986 if diff:
4987 cls.append(cl)
4988 if (not first_pass and
4989 cl._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY) is None):
4990 # We are mid-stack and the user must upload their upstream branches.
4991 must_upload_upstream = True
4992 elif first_pass: # The current branch has nothing to commit. Exit.
4993 DieWithError('Branch %s has nothing to commit' % cl.GetBranch())
4994 # Else: A mid-stack branch has nothing to commit. We do not add it to cls.
4995 first_pass = False
4996
4997 # Cases below determine if we should continue to traverse up the tree.
Joanna Wang18de1f62023-01-21 01:24:24 +00004998 origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(cl.GetBranch())
Joanna Wang18de1f62023-01-21 01:24:24 +00004999 branch_ref = upstream_branch_ref # set branch for next run.
5000
Joanna Wang6215dd02023-02-07 15:58:03 +00005001 upstream_branch = scm.GIT.ShortBranchName(upstream_branch_ref)
5002 upstream_last_upload = scm.GIT.GetBranchConfig(settings.GetRoot(),
5003 upstream_branch,
5004 LAST_UPLOAD_HASH_CONFIG_KEY)
5005
Joanna Wang18de1f62023-01-21 01:24:24 +00005006 # Case 1: We've reached the beginning of the tree.
5007 if origin != '.':
5008 break
5009
Joanna Wang18de1f62023-01-21 01:24:24 +00005010 # Case 2: If any upstream branches have never been uploaded,
Joanna Wang6215dd02023-02-07 15:58:03 +00005011 # the user MUST upload them unless they are empty. Continue to
5012 # next loop to add upstream if it is not empty.
Joanna Wang18de1f62023-01-21 01:24:24 +00005013 if not upstream_last_upload:
Joanna Wang18de1f62023-01-21 01:24:24 +00005014 continue
5015
Joanna Wang18de1f62023-01-21 01:24:24 +00005016 # Case 3: If upstream's last_upload == cl.base_commit we do
5017 # not need to upload any more upstreams from this point on.
5018 # (Even if there may be diverged branches higher up the tree)
5019 if base_commit == upstream_last_upload:
5020 break
5021
5022 # Case 4: If upstream's last_upload < cl.base_commit we are
5023 # uploading cl and upstream_cl.
5024 # Continue up the tree to check other branch relations.
Joanna Wangab9c6ba2023-01-21 01:46:36 +00005025 if scm.GIT.IsAncestor(upstream_last_upload, base_commit):
Joanna Wang18de1f62023-01-21 01:24:24 +00005026 continue
5027
5028 # Case 5: If cl.base_commit < upstream's last_upload the user
5029 # must rebase before uploading.
Joanna Wangab9c6ba2023-01-21 01:46:36 +00005030 if scm.GIT.IsAncestor(base_commit, upstream_last_upload):
Joanna Wang18de1f62023-01-21 01:24:24 +00005031 DieWithError(
5032 'At least one branch in the stack has diverged from its upstream '
5033 'branch and does not contain its upstream\'s last upload.\n'
5034 'Please rebase the stack with `git rebase-update` before uploading.')
5035
5036 # The tree went through a rebase. LAST_UPLOAD_HASH_CONFIG_KEY no longer has
5037 # any relation to commits in the tree. Continue up the tree until we hit
5038 # the root.
5039
5040 # We assume all cls in the stack have the same auth requirements and only
5041 # check this once.
5042 cls[0].EnsureAuthenticated(force=options.force)
5043
5044 cherry_pick = False
5045 if len(cls) > 1:
Joanna Wangd75fc882023-03-01 21:53:34 +00005046 opt_message = ''
Joanna Wang6215dd02023-02-07 15:58:03 +00005047 branches = ', '.join([cl.branch for cl in cls])
Joanna Wang18de1f62023-01-21 01:24:24 +00005048 if len(orig_args):
Joanna Wangd75fc882023-03-01 21:53:34 +00005049 opt_message = ('options %s will be used for all uploads.\n' % orig_args)
Joanna Wang18de1f62023-01-21 01:24:24 +00005050 if must_upload_upstream:
Joanna Wangd75fc882023-03-01 21:53:34 +00005051 msg = ('At least one parent branch in `%s` has never been uploaded '
5052 'and must be uploaded before/with `%s`.\n' %
5053 (branches, cls[1].branch))
5054 if options.cherry_pick_stacked:
5055 DieWithError(msg)
5056 if not options.force:
5057 confirm_or_exit('\n' + opt_message + msg)
Joanna Wang18de1f62023-01-21 01:24:24 +00005058 else:
Joanna Wangd75fc882023-03-01 21:53:34 +00005059 if options.cherry_pick_stacked:
5060 print('cherry-picking `%s` on %s\'s last upload' %
5061 (cls[0].branch, cls[1].branch))
Joanna Wang18de1f62023-01-21 01:24:24 +00005062 cherry_pick = True
Joanna Wangd75fc882023-03-01 21:53:34 +00005063 elif not options.force:
5064 answer = gclient_utils.AskForData(
5065 '\n' + opt_message +
5066 'Press enter to update branches %s.\nOr type `n` to upload only '
5067 '`%s` cherry-picked on %s\'s last upload:' %
5068 (branches, cls[0].branch, cls[1].branch))
5069 if answer.lower() == 'n':
5070 cherry_pick = True
Joanna Wang18de1f62023-01-21 01:24:24 +00005071 return cls, cherry_pick
5072
5073
Francois Dorayd42c6812017-05-30 15:10:20 -04005074@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005075@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04005076def CMDsplit(parser, args):
5077 """Splits a branch into smaller branches and uploads CLs.
5078
5079 Creates a branch and uploads a CL for each group of files modified in the
5080 current branch that share a common OWNERS file. In the CL description and
Edward Lemurac5c55f2020-02-29 00:17:16 +00005081 comment, the string '$directory', is replaced with the directory containing
5082 the shared OWNERS file.
Francois Dorayd42c6812017-05-30 15:10:20 -04005083 """
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005084 parser.add_option('-d', '--description', dest='description_file',
5085 help='A text file containing a CL description in which '
5086 '$directory will be replaced by each CL\'s directory.')
5087 parser.add_option('-c', '--comment', dest='comment_file',
5088 help='A text file containing a CL comment.')
5089 parser.add_option('-n', '--dry-run', dest='dry_run', action='store_true',
Chris Watkinsba28e462017-12-13 11:22:17 +11005090 default=False,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005091 help='List the files and reviewers for each CL that would '
5092 'be created, but don\'t create branches or CLs.')
5093 parser.add_option('--cq-dry-run', action='store_true',
5094 help='If set, will do a cq dry run for each uploaded CL. '
5095 'Please be careful when doing this; more than ~10 CLs '
5096 'has the potential to overload our build '
5097 'infrastructure. Try to upload these not during high '
5098 'load times (usually 11-3 Mountain View time). Email '
5099 'infra-dev@chromium.org with any questions.')
Takuto Ikuta51eca592019-02-14 19:40:52 +00005100 parser.add_option('-a', '--enable-auto-submit', action='store_true',
5101 default=True,
5102 help='Sends your change to the CQ after an approval. Only '
5103 'works on repos that have the Auto-Submit label '
5104 'enabled')
Daniel Cheng403c44e2022-10-05 22:24:58 +00005105 parser.add_option('--max-depth',
5106 type='int',
5107 default=0,
5108 help='The max depth to look for OWNERS files. Useful for '
5109 'controlling the granularity of the split CLs, e.g. '
5110 '--max-depth=1 will only split by top-level '
5111 'directory. Specifying a value less than 1 means no '
5112 'limit on max depth.')
Francois Dorayd42c6812017-05-30 15:10:20 -04005113 options, _ = parser.parse_args(args)
5114
5115 if not options.description_file:
5116 parser.error('No --description flag specified.')
5117
5118 def WrappedCMDupload(args):
5119 return CMDupload(OptionParser(), args)
5120
Daniel Cheng403c44e2022-10-05 22:24:58 +00005121 return split_cl.SplitCl(options.description_file, options.comment_file,
5122 Changelist, WrappedCMDupload, options.dry_run,
5123 options.cq_dry_run, options.enable_auto_submit,
5124 options.max_depth, settings.GetRoot())
Francois Dorayd42c6812017-05-30 15:10:20 -04005125
5126
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005127@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005128@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005129def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005130 """DEPRECATED: Used to commit the current changelist via git-svn."""
5131 message = ('git-cl no longer supports committing to SVN repositories via '
5132 'git-svn. You probably want to use `git cl land` instead.')
5133 print(message)
5134 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005135
5136
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005137@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005138@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005139def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005140 """Commits the current changelist via git.
5141
5142 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5143 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005144 """
5145 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5146 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07005147 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005148 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04005149 parser.add_option('--parallel', action='store_true',
5150 help='Run all tests specified by input_api.RunTests in all '
5151 'PRESUBMIT files in parallel.')
Saagar Sanghavi03b15132020-08-10 16:43:41 +00005152 parser.add_option('--resultdb', action='store_true',
5153 help='Run presubmit checks in the ResultSink environment '
5154 'and send results to the ResultDB database.')
5155 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005156 (options, args) = parser.parse_args(args)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005157
Edward Lemur934836a2019-09-09 20:16:54 +00005158 cl = Changelist()
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005159
Robert Iannucci2e73d432018-03-14 01:10:47 -07005160 if not cl.GetIssue():
5161 DieWithError('You must upload the change first to Gerrit.\n'
5162 ' If you would rather have `git cl land` upload '
5163 'automatically for you, see http://crbug.com/642759')
Saagar Sanghavi03b15132020-08-10 16:43:41 +00005164 return cl.CMDLand(options.force, options.bypass_hooks, options.verbose,
5165 options.parallel, options.resultdb, options.realm)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005166
5167
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005168@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005169@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005170def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005171 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005172 parser.add_option('-b', dest='newbranch',
5173 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005174 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005175 help='overwrite state on the current or chosen branch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005176 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
Edward Lemurf38bc172019-09-03 21:02:13 +00005177 help='don\'t commit after patch applies.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005178
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005179 group = optparse.OptionGroup(
5180 parser,
5181 'Options for continuing work on the current issue uploaded from a '
5182 'different clone (e.g. different machine). Must be used independently '
5183 'from the other options. No issue number should be specified, and the '
5184 'branch must have an issue number associated with it')
5185 group.add_option('--reapply', action='store_true', dest='reapply',
5186 help='Reset the branch and reapply the issue.\n'
5187 'CAUTION: This will undo any local changes in this '
5188 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005189
5190 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005191 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005192 parser.add_option_group(group)
5193
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005194 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005195
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005196 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005197 if options.newbranch:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005198 parser.error('--reapply works on the current branch only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005199 if len(args) > 0:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005200 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005201
Edward Lemur934836a2019-09-09 20:16:54 +00005202 cl = Changelist()
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005203 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005204 parser.error('Current branch must have an associated issue.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005205
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005206 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005207 if upstream is None:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005208 parser.error('No upstream branch specified. Cannot reset branch.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005209
5210 RunGit(['reset', '--hard', upstream])
5211 if options.pull:
5212 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005213
Edward Lemur678a6842019-10-03 22:25:05 +00005214 target_issue_arg = ParseIssueNumberArgument(cl.GetIssue())
Joanna Wang44e9bee2023-01-25 21:51:42 +00005215 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit,
5216 options.force, False)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005217
5218 if len(args) != 1 or not args[0]:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005219 parser.error('Must specify issue number or URL.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005220
Edward Lemurf38bc172019-09-03 21:02:13 +00005221 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005222 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005223 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005224
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005225 # We don't want uncommitted changes mixed up with the patch.
5226 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005227 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005228
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005229 if options.newbranch:
5230 if options.force:
5231 RunGit(['branch', '-D', options.newbranch],
5232 stderr=subprocess2.PIPE, error_ok=True)
Edward Lemur84101642020-02-21 21:40:34 +00005233 git_new_branch.create_new_branch(options.newbranch)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005234
Edward Lemur678a6842019-10-03 22:25:05 +00005235 cl = Changelist(
5236 codereview_host=target_issue_arg.hostname, issue=target_issue_arg.issue)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005237
Edward Lemur678a6842019-10-03 22:25:05 +00005238 if not args[0].isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00005239 print('canonical issue/change URL: %s\n' % cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005240
Bruce Dawsonf362f6f2021-02-18 23:15:17 +00005241 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit,
5242 options.force, options.newbranch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005243
5244
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005245def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005246 """Fetches the tree status and returns either 'open', 'closed',
5247 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005248 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005249 if url:
Daniel McArdle8b4eeff2020-07-20 17:02:47 +00005250 status = str(urllib.request.urlopen(url).read().lower())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005251 if status.find('closed') != -1 or status == '0':
5252 return 'closed'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005253
5254 if status.find('open') != -1 or status == '1':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005255 return 'open'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005256
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005257 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005258 return 'unset'
5259
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005260
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005261def GetTreeStatusReason():
5262 """Fetches the tree status from a json url and returns the message
5263 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005264 url = settings.GetTreeStatusUrl()
Daniel McArdle8b4eeff2020-07-20 17:02:47 +00005265 json_url = urllib.parse.urljoin(url, '/current?format=json')
Edward Lemur79d4f992019-11-11 23:49:02 +00005266 connection = urllib.request.urlopen(json_url)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005267 status = json.loads(connection.read())
5268 connection.close()
5269 return status['message']
5270
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005271
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005272@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005273def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005274 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005275 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005276 status = GetTreeStatus()
5277 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005278 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005279 return 2
5280
vapiera7fbd5a2016-06-16 09:17:49 -07005281 print('The tree is %s' % status)
5282 print()
5283 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005284 if status != 'open':
5285 return 1
5286 return 0
5287
5288
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005289@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005290def CMDtry(parser, args):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005291 """Triggers tryjobs using either Buildbucket or CQ dry run."""
5292 group = optparse.OptionGroup(parser, 'Tryjob options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005293 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005294 '-b', '--bot', action='append',
5295 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5296 'times to specify multiple builders. ex: '
5297 '"-b win_rel -b win_layout". See '
5298 'the try server waterfall for the builders name and the tests '
5299 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005300 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005301 '-B', '--bucket', default='',
Ben Pastene08a30b22022-05-04 17:46:38 +00005302 help=('Buildbucket bucket to send the try requests. Format: '
5303 '"luci.$LUCI_PROJECT.$LUCI_BUCKET". eg: "luci.chromium.try"'))
borenet6c0efe62016-10-19 08:13:29 -07005304 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005305 '-r', '--revision',
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005306 help='Revision to use for the tryjob; default: the revision will '
tandriif7b29d42016-10-07 08:45:41 -07005307 'be determined by the try recipe that builder runs, which usually '
Josip Sokcevicc39ab992020-09-24 20:09:15 +00005308 'defaults to HEAD of origin/master or origin/main')
maruel@chromium.org15192402012-09-06 12:38:29 +00005309 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005310 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005311 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005312 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005313 group.add_option(
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005314 '-q',
5315 '--quick-run',
5316 action='store_true',
5317 default=False,
5318 help='trigger in quick run mode '
5319 '(https://source.chromium.org/chromium/chromium/src/+/main:docs/cq_q'
5320 'uick_run.md) (chromium only).')
5321 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005322 '--category', default='git_cl_try', help='Specify custom build category.')
5323 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005324 '--project',
5325 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005326 'in recipe to determine to which repository or directory to '
5327 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005328 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005329 '-p', '--property', dest='properties', action='append', default=[],
5330 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005331 'key2=value2 etc. The value will be treated as '
5332 'json if decodable, or as string otherwise. '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005333 'NOTE: using this may make your tryjob not usable for CQ, '
5334 'which will then schedule another tryjob with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005335 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005336 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5337 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005338 parser.add_option_group(group)
Quinten Yearsley983111f2019-09-26 17:18:48 +00005339 parser.add_option(
5340 '-R', '--retry-failed', action='store_true', default=False,
5341 help='Retry failed jobs from the latest set of tryjobs. '
5342 'Not allowed with --bucket and --bot options.')
Edward Lemur52969c92020-02-06 18:15:28 +00005343 parser.add_option(
5344 '-i', '--issue', type=int,
5345 help='Operate on this issue instead of the current branch\'s implicit '
5346 'issue.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005347 options, args = parser.parse_args(args)
5348
machenbach@chromium.org45453142015-09-15 08:45:22 +00005349 # Make sure that all properties are prop=value pairs.
5350 bad_params = [x for x in options.properties if '=' not in x]
5351 if bad_params:
5352 parser.error('Got properties with missing "=": %s' % bad_params)
5353
maruel@chromium.org15192402012-09-06 12:38:29 +00005354 if args:
5355 parser.error('Unknown arguments: %s' % args)
5356
Edward Lemur934836a2019-09-09 20:16:54 +00005357 cl = Changelist(issue=options.issue)
maruel@chromium.org15192402012-09-06 12:38:29 +00005358 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005359 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005360
Edward Lemurf38bc172019-09-03 21:02:13 +00005361 # HACK: warm up Gerrit change detail cache to save on RPCs.
Edward Lemur125d60a2019-09-13 18:25:41 +00005362 cl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005363
tandriie113dfd2016-10-11 10:20:12 -07005364 error_message = cl.CannotTriggerTryJobReason()
5365 if error_message:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005366 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005367
Edward Lemur45768512020-03-02 19:03:14 +00005368 if options.bot:
5369 if options.retry_failed:
5370 parser.error('--bot is not compatible with --retry-failed.')
5371 if not options.bucket:
5372 parser.error('A bucket (e.g. "chromium/try") is required.')
5373
5374 triggered = [b for b in options.bot if 'triggered' in b]
5375 if triggered:
5376 parser.error(
5377 'Cannot schedule builds on triggered bots: %s.\n'
5378 'This type of bot requires an initial job from a parent (usually a '
5379 'builder). Schedule a job on the parent instead.\n' % triggered)
5380
5381 if options.bucket.startswith('.master'):
5382 parser.error('Buildbot masters are not supported.')
5383
5384 project, bucket = _parse_bucket(options.bucket)
5385 if project is None or bucket is None:
5386 parser.error('Invalid bucket: %s.' % options.bucket)
5387 jobs = sorted((project, bucket, bot) for bot in options.bot)
5388 elif options.retry_failed:
Quinten Yearsley983111f2019-09-26 17:18:48 +00005389 print('Searching for failed tryjobs...')
Joanna Wanga8db0cb2023-01-24 15:43:17 +00005390 builds, patchset = _fetch_latest_builds(cl, DEFAULT_BUILDBUCKET_HOST)
Quinten Yearsley983111f2019-09-26 17:18:48 +00005391 if options.verbose:
5392 print('Got %d builds in patchset #%d' % (len(builds), patchset))
Edward Lemur45768512020-03-02 19:03:14 +00005393 jobs = _filter_failed_for_retry(builds)
5394 if not jobs:
Quinten Yearsley983111f2019-09-26 17:18:48 +00005395 print('There are no failed jobs in the latest set of jobs '
5396 '(patchset #%d), doing nothing.' % patchset)
5397 return 0
Edward Lemur45768512020-03-02 19:03:14 +00005398 num_builders = len(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +00005399 if num_builders > 10:
5400 confirm_or_exit('There are %d builders with failed builds.'
5401 % num_builders, action='continue')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005402 elif options.quick_run:
5403 print('Scheduling CQ quick run on: %s' % cl.GetIssueURL())
5404 return cl.SetCQState(_CQState.QUICK_RUN)
Quinten Yearsley983111f2019-09-26 17:18:48 +00005405 else:
qyearsley1fdfcb62016-10-24 13:22:03 -07005406 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005407 print('git cl try with no bots now defaults to CQ dry run.')
5408 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5409 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005410
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005411 patchset = cl.GetMostRecentPatchset()
Edward Lemur2c210a42019-09-16 23:58:35 +00005412 try:
Quinten Yearsley777660f2020-03-04 23:37:06 +00005413 _trigger_tryjobs(cl, jobs, options, patchset)
Edward Lemur2c210a42019-09-16 23:58:35 +00005414 except BuildbucketResponseException as ex:
5415 print('ERROR: %s' % ex)
5416 return 1
5417 return 0
maruel@chromium.org15192402012-09-06 12:38:29 +00005418
5419
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005420@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005421def CMDtry_results(parser, args):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005422 """Prints info about results for tryjobs associated with the current CL."""
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005423 group = optparse.OptionGroup(parser, 'Tryjob results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005424 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005425 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005426 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005427 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005428 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005429 '--color', action='store_true', default=setup_color.IS_TTY,
5430 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005431 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005432 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5433 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005434 group.add_option(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005435 '--json', help=('Path of JSON output file to write tryjob results to,'
Stefan Zager1306bd02017-06-22 19:26:46 -07005436 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005437 parser.add_option_group(group)
Edward Lemur52969c92020-02-06 18:15:28 +00005438 parser.add_option(
5439 '-i', '--issue', type=int,
5440 help='Operate on this issue instead of the current branch\'s implicit '
5441 'issue.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005442 options, args = parser.parse_args(args)
5443 if args:
5444 parser.error('Unrecognized args: %s' % ' '.join(args))
5445
Edward Lemur934836a2019-09-09 20:16:54 +00005446 cl = Changelist(issue=options.issue)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005447 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005448 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005449
tandrii221ab252016-10-06 08:12:04 -07005450 patchset = options.patchset
5451 if not patchset:
Gavin Make61ccc52020-11-13 00:12:57 +00005452 patchset = cl.GetMostRecentDryRunPatchset()
tandrii221ab252016-10-06 08:12:04 -07005453 if not patchset:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005454 parser.error('Code review host doesn\'t know about issue %s. '
tandrii221ab252016-10-06 08:12:04 -07005455 'No access to issue or wrong issue number?\n'
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005456 'Either upload first, or pass --patchset explicitly.' %
tandrii221ab252016-10-06 08:12:04 -07005457 cl.GetIssue())
5458
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005459 try:
Joanna Wanga8db0cb2023-01-24 15:43:17 +00005460 jobs = _fetch_tryjobs(cl, DEFAULT_BUILDBUCKET_HOST, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005461 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005462 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005463 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005464 if options.json:
Edward Lemurbaaf6be2019-10-09 18:00:44 +00005465 write_json(options.json, jobs)
qyearsley53f48a12016-09-01 10:45:13 -07005466 else:
Quinten Yearsley777660f2020-03-04 23:37:06 +00005467 _print_tryjobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005468 return 0
5469
5470
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005471@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005472@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005473def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005474 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005475 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005476 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005477 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005478
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005479 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005480 if args:
5481 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005482 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005483 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005484 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005485 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005486
5487 # Clear configured merge-base, if there is one.
5488 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005489 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005490 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005491 return 0
5492
5493
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005494@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005495def CMDweb(parser, args):
5496 """Opens the current CL in the web browser."""
Orr Bernstein0b960582022-12-22 20:16:18 +00005497 parser.add_option('-p',
5498 '--print-only',
5499 action='store_true',
5500 dest='print_only',
5501 help='Only print the Gerrit URL, don\'t open it in the '
5502 'browser.')
5503 (options, args) = parser.parse_args(args)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005504 if args:
5505 parser.error('Unrecognized args: %s' % ' '.join(args))
5506
5507 issue_url = Changelist().GetIssueURL()
5508 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005509 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005510 return 1
5511
Orr Bernstein0b960582022-12-22 20:16:18 +00005512 if options.print_only:
5513 print(issue_url)
5514 return 0
5515
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005516 # Redirect I/O before invoking browser to hide its output. For example, this
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005517 # allows us to hide the "Created new window in existing browser session."
5518 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005519 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005520 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005521 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005522 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005523 os.open(os.devnull, os.O_RDWR)
5524 try:
5525 webbrowser.open(issue_url)
5526 finally:
5527 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005528 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005529 return 0
5530
5531
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005532@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005533def CMDset_commit(parser, args):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00005534 """Sets the commit bit to trigger the CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005535 parser.add_option('-d', '--dry-run', action='store_true',
5536 help='trigger in dry run mode')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005537 parser.add_option(
5538 '-q',
5539 '--quick-run',
5540 action='store_true',
5541 help='trigger in quick run mode '
5542 '(https://source.chromium.org/chromium/chromium/src/+/main:docs/cq_qu'
5543 'ick_run.md) (chromium only).')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005544 parser.add_option('-c', '--clear', action='store_true',
5545 help='stop CQ run, if any')
Edward Lemur52969c92020-02-06 18:15:28 +00005546 parser.add_option(
5547 '-i', '--issue', type=int,
5548 help='Operate on this issue instead of the current branch\'s implicit '
5549 'issue.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005550 options, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005551 if args:
5552 parser.error('Unrecognized args: %s' % ' '.join(args))
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005553 if [options.dry_run, options.quick_run, options.clear].count(True) > 1:
5554 parser.error('Only one of --dry-run, --quick-run, and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005555
Edward Lemur934836a2019-09-09 20:16:54 +00005556 cl = Changelist(issue=options.issue)
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005557 if not cl.GetIssue():
5558 parser.error('Must upload the issue first.')
5559
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005560 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005561 state = _CQState.NONE
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005562 elif options.quick_run:
5563 state = _CQState.QUICK_RUN
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005564 elif options.dry_run:
5565 state = _CQState.DRY_RUN
5566 else:
5567 state = _CQState.COMMIT
tandrii9de9ec62016-07-13 03:01:59 -07005568 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005569 return 0
5570
5571
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005572@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005573def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005574 """Closes the issue."""
Edward Lemur52969c92020-02-06 18:15:28 +00005575 parser.add_option(
5576 '-i', '--issue', type=int,
5577 help='Operate on this issue instead of the current branch\'s implicit '
5578 'issue.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005579 options, args = parser.parse_args(args)
groby@chromium.org411034a2013-02-26 15:12:01 +00005580 if args:
5581 parser.error('Unrecognized args: %s' % ' '.join(args))
Edward Lemur934836a2019-09-09 20:16:54 +00005582 cl = Changelist(issue=options.issue)
groby@chromium.org411034a2013-02-26 15:12:01 +00005583 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005584 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005585 DieWithError('ERROR: No issue to close.')
groby@chromium.org411034a2013-02-26 15:12:01 +00005586 cl.CloseIssue()
5587 return 0
5588
5589
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005590@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005591def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005592 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005593 parser.add_option(
5594 '--stat',
5595 action='store_true',
5596 dest='stat',
5597 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005598 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005599 if args:
5600 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005601
Edward Lemur934836a2019-09-09 20:16:54 +00005602 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005603 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005604 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005605 if not issue:
5606 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005607
Gavin Makbe2e9262022-11-08 23:41:55 +00005608 base = cl._GitGetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY)
Aaron Gablea718c3e2017-08-28 17:47:28 -07005609 if not base:
Gavin Makbe2e9262022-11-08 23:41:55 +00005610 base = cl._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
Aaron Gablea718c3e2017-08-28 17:47:28 -07005611 if not base:
5612 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5613 revision_info = detail['revisions'][detail['current_revision']]
5614 fetch_info = revision_info['fetch']['http']
5615 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5616 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005617
Aaron Gablea718c3e2017-08-28 17:47:28 -07005618 cmd = ['git', 'diff']
5619 if options.stat:
5620 cmd.append('--stat')
5621 cmd.append(base)
5622 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005623
5624 return 0
5625
5626
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005627@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005628def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005629 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005630 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005631 '--ignore-current',
5632 action='store_true',
5633 help='Ignore the CL\'s current reviewers and start from scratch.')
5634 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005635 '--ignore-self',
5636 action='store_true',
5637 help='Do not consider CL\'s author as an owners.')
5638 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005639 '--no-color',
5640 action='store_true',
5641 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005642 parser.add_option(
5643 '--batch',
5644 action='store_true',
5645 help='Do not run interactively, just suggest some')
Yang Guo6e269a02019-06-26 11:17:02 +00005646 # TODO: Consider moving this to another command, since other
5647 # git-cl owners commands deal with owners for a given CL.
5648 parser.add_option(
5649 '--show-all',
5650 action='store_true',
5651 help='Show all owners for a particular file')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005652 options, args = parser.parse_args(args)
5653
Edward Lemur934836a2019-09-09 20:16:54 +00005654 cl = Changelist()
Edward Lesmes50da7702020-03-30 19:23:43 +00005655 author = cl.GetAuthor()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005656
Yang Guo6e269a02019-06-26 11:17:02 +00005657 if options.show_all:
Bruce Dawson97ed44a2020-05-06 17:04:03 +00005658 if len(args) == 0:
5659 print('No files specified for --show-all. Nothing to do.')
5660 return 0
Edward Lesmese1576912021-02-16 21:53:34 +00005661 owners_by_path = cl.owners_client.BatchListOwners(args)
Edward Lesmes0e4e5ae2021-01-08 18:28:46 +00005662 for path in args:
5663 print('Owners for %s:' % path)
5664 print('\n'.join(
5665 ' - %s' % owner
5666 for owner in owners_by_path.get(path, ['No owners found'])))
Yang Guo6e269a02019-06-26 11:17:02 +00005667 return 0
5668
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005669 if args:
5670 if len(args) > 1:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005671 parser.error('Unknown args.')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005672 base_branch = args[0]
5673 else:
5674 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005675 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005676
Edward Lemur2c62b332020-03-12 22:12:33 +00005677 affected_files = cl.GetAffectedFiles(base_branch)
Dirk Prankebf980882017-09-02 15:08:00 -07005678
5679 if options.batch:
Edward Lesmese1576912021-02-16 21:53:34 +00005680 owners = cl.owners_client.SuggestOwners(affected_files, exclude=[author])
5681 print('\n'.join(owners))
Dirk Prankebf980882017-09-02 15:08:00 -07005682 return 0
5683
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005684 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005685 affected_files,
Edward Lemur707d70b2018-02-07 00:50:14 +01005686 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005687 [] if options.ignore_current else cl.GetReviewers(),
Edward Lesmes5cd75472021-02-19 00:34:25 +00005688 cl.owners_client,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005689 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005690 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005691
5692
Aiden Bennerc08566e2018-10-03 17:52:42 +00005693def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005694 """Generates a diff command."""
5695 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005696 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5697
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005698 if allow_prefix:
5699 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5700 # case that diff.noprefix is set in the user's git config.
5701 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5702 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005703 diff_cmd += ['--no-prefix']
5704
5705 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005706
5707 if args:
5708 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005709 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005710 diff_cmd.append(arg)
5711 else:
5712 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005713
5714 return diff_cmd
5715
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005716
Jamie Madill5e96ad12020-01-13 16:08:35 +00005717def _RunClangFormatDiff(opts, clang_diff_files, top_dir, upstream_commit):
5718 """Runs clang-format-diff and sets a return value if necessary."""
5719
5720 if not clang_diff_files:
5721 return 0
5722
5723 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5724 # formatted. This is used to block during the presubmit.
5725 return_value = 0
5726
5727 # Locate the clang-format binary in the checkout
5728 try:
5729 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
5730 except clang_format.NotFoundError as e:
5731 DieWithError(e)
5732
5733 if opts.full or settings.GetFormatFullByDefault():
5734 cmd = [clang_format_tool]
5735 if not opts.dry_run and not opts.diff:
5736 cmd.append('-i')
5737 if opts.dry_run:
5738 for diff_file in clang_diff_files:
5739 with open(diff_file, 'r') as myfile:
5740 code = myfile.read().replace('\r\n', '\n')
5741 stdout = RunCommand(cmd + [diff_file], cwd=top_dir)
5742 stdout = stdout.replace('\r\n', '\n')
5743 if opts.diff:
5744 sys.stdout.write(stdout)
5745 if code != stdout:
5746 return_value = 2
5747 else:
5748 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
5749 if opts.diff:
5750 sys.stdout.write(stdout)
5751 else:
Jamie Madill5e96ad12020-01-13 16:08:35 +00005752 try:
5753 script = clang_format.FindClangFormatScriptInChromiumTree(
5754 'clang-format-diff.py')
5755 except clang_format.NotFoundError as e:
5756 DieWithError(e)
5757
Josip Sokcevic2a827fc2022-03-04 17:51:47 +00005758 cmd = ['vpython3', script, '-p0']
Jamie Madill5e96ad12020-01-13 16:08:35 +00005759 if not opts.dry_run and not opts.diff:
5760 cmd.append('-i')
5761
5762 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
Edward Lemur1a83da12020-03-04 21:18:36 +00005763 diff_output = RunGit(diff_cmd).encode('utf-8')
Jamie Madill5e96ad12020-01-13 16:08:35 +00005764
Edward Lesmes89624cd2020-04-06 17:51:56 +00005765 env = os.environ.copy()
5766 env['PATH'] = (
5767 str(os.path.dirname(clang_format_tool)) + os.pathsep + env['PATH'])
5768 stdout = RunCommand(
5769 cmd, stdin=diff_output, cwd=top_dir, env=env,
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00005770 shell=sys.platform.startswith('win32'))
Jamie Madill5e96ad12020-01-13 16:08:35 +00005771 if opts.diff:
5772 sys.stdout.write(stdout)
5773 if opts.dry_run and len(stdout) > 0:
5774 return_value = 2
5775
5776 return return_value
5777
5778
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005779def _RunRustFmt(opts, rust_diff_files, top_dir, upstream_commit):
5780 """Runs rustfmt. Just like _RunClangFormatDiff returns 2 to indicate that
5781 presubmit checks have failed (and returns 0 otherwise)."""
5782
5783 if not rust_diff_files:
5784 return 0
5785
5786 # Locate the rustfmt binary.
5787 try:
5788 rustfmt_tool = rustfmt.FindRustfmtToolInChromiumTree()
5789 except rustfmt.NotFoundError as e:
5790 DieWithError(e)
5791
5792 # TODO(crbug.com/1231317): Support formatting only the changed lines
5793 # if `opts.full or settings.GetFormatFullByDefault()` is False. See also:
5794 # https://github.com/emilio/rustfmt-format-diff
5795 cmd = [rustfmt_tool]
5796 if opts.dry_run:
5797 cmd.append('--check')
5798 cmd += rust_diff_files
5799 rustfmt_exitcode = subprocess2.call(cmd)
5800
5801 if opts.presubmit and rustfmt_exitcode != 0:
5802 return 2
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005803
5804 return 0
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005805
5806
Olivier Robin0a6b5442022-04-07 07:25:04 +00005807def _RunSwiftFormat(opts, swift_diff_files, top_dir, upstream_commit):
5808 """Runs swift-format. Just like _RunClangFormatDiff returns 2 to indicate
5809 that presubmit checks have failed (and returns 0 otherwise)."""
5810
5811 if not swift_diff_files:
5812 return 0
5813
5814 # Locate the swift-format binary.
5815 try:
5816 swift_format_tool = swift_format.FindSwiftFormatToolInChromiumTree()
5817 except swift_format.NotFoundError as e:
5818 DieWithError(e)
5819
5820 cmd = [swift_format_tool]
5821 if opts.dry_run:
Olivier Robin7f39e3d2022-04-28 08:20:49 +00005822 cmd += ['lint', '-s']
Olivier Robin0a6b5442022-04-07 07:25:04 +00005823 else:
5824 cmd += ['format', '-i']
5825 cmd += swift_diff_files
5826 swift_format_exitcode = subprocess2.call(cmd)
5827
5828 if opts.presubmit and swift_format_exitcode != 0:
5829 return 2
5830
5831 return 0
5832
5833
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005834def MatchingFileType(file_name, extensions):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005835 """Returns True if the file name ends with one of the given extensions."""
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005836 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005837
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005838
enne@chromium.org555cfe42014-01-29 18:21:39 +00005839@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005840@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005841def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005842 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005843 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005844 GN_EXTS = ['.gn', '.gni', '.typemap']
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005845 RUST_EXTS = ['.rs']
Olivier Robin0a6b5442022-04-07 07:25:04 +00005846 SWIFT_EXTS = ['.swift']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005847 parser.add_option('--full', action='store_true',
5848 help='Reformat the full content of all touched files')
Tomasz Åšniatowski58194462021-08-27 17:36:16 +00005849 parser.add_option('--upstream', help='Branch to check against')
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005850 parser.add_option('--dry-run', action='store_true',
5851 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005852 parser.add_option(
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00005853 '--no-clang-format',
5854 dest='clang_format',
5855 action='store_false',
5856 default=True,
5857 help='Disables formatting of various file types using clang-format.')
5858 parser.add_option(
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005859 '--python',
5860 action='store_true',
5861 default=None,
5862 help='Enables python formatting on all python files.')
5863 parser.add_option(
5864 '--no-python',
5865 action='store_true',
Garrett Beaty91a6f332020-01-06 16:57:24 +00005866 default=False,
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005867 help='Disables python formatting on all python files. '
Garrett Beaty91a6f332020-01-06 16:57:24 +00005868 'If neither --python or --no-python are set, python files that have a '
5869 '.style.yapf file in an ancestor directory will be formatted. '
5870 'It is an error to set both.')
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00005871 parser.add_option(
5872 '--js',
5873 action='store_true',
5874 help='Format javascript code with clang-format. '
5875 'Has no effect if --no-clang-format is set.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005876 parser.add_option('--diff', action='store_true',
5877 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005878 parser.add_option('--presubmit', action='store_true',
5879 help='Used when running the script from a presubmit.')
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005880
5881 parser.add_option('--rust-fmt',
5882 dest='use_rust_fmt',
5883 action='store_true',
5884 default=rustfmt.IsRustfmtSupported(),
5885 help='Enables formatting of Rust file types using rustfmt.')
5886 parser.add_option(
5887 '--no-rust-fmt',
5888 dest='use_rust_fmt',
5889 action='store_false',
5890 help='Disables formatting of Rust file types using rustfmt.')
5891
Olivier Robin0a6b5442022-04-07 07:25:04 +00005892 parser.add_option(
5893 '--swift-format',
5894 dest='use_swift_format',
5895 action='store_true',
Olivier Robin7f39e3d2022-04-28 08:20:49 +00005896 default=swift_format.IsSwiftFormatSupported(),
Olivier Robin0a6b5442022-04-07 07:25:04 +00005897 help='Enables formatting of Swift file types using swift-format '
5898 '(macOS host only).')
5899 parser.add_option(
5900 '--no-swift-format',
5901 dest='use_swift_format',
5902 action='store_false',
5903 help='Disables formatting of Swift file types using swift-format.')
5904
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005905 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005906
Garrett Beaty91a6f332020-01-06 16:57:24 +00005907 if opts.python is not None and opts.no_python:
5908 raise parser.error('Cannot set both --python and --no-python')
5909 if opts.no_python:
5910 opts.python = False
5911
Daniel Chengc55eecf2016-12-30 03:11:02 -08005912 # Normalize any remaining args against the current path, so paths relative to
5913 # the current directory are still resolved as expected.
5914 args = [os.path.join(os.getcwd(), arg) for arg in args]
5915
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005916 # git diff generates paths against the root of the repository. Change
5917 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005918 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005919 if rel_base_path:
5920 os.chdir(rel_base_path)
5921
digit@chromium.org29e47272013-05-17 17:01:46 +00005922 # Grab the merge-base commit, i.e. the upstream commit of the current
5923 # branch when it was created or the last time it was rebased. This is
5924 # to cover the case where the user may have called "git fetch origin",
5925 # moving the origin branch to a newer commit, but hasn't rebased yet.
5926 upstream_commit = None
Tomasz Åšniatowski58194462021-08-27 17:36:16 +00005927 upstream_branch = opts.upstream
5928 if not upstream_branch:
5929 cl = Changelist()
5930 upstream_branch = cl.GetUpstreamBranch()
digit@chromium.org29e47272013-05-17 17:01:46 +00005931 if upstream_branch:
5932 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5933 upstream_commit = upstream_commit.strip()
5934
5935 if not upstream_commit:
5936 DieWithError('Could not find base commit for this branch. '
5937 'Are you in detached state?')
5938
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005939 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5940 diff_output = RunGit(changed_files_cmd)
5941 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005942 # Filter out files deleted by this CL
5943 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005944
Andreas Haas417d89c2020-02-06 10:24:27 +00005945 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005946 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005947
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00005948 clang_diff_files = []
5949 if opts.clang_format:
5950 clang_diff_files = [
5951 x for x in diff_files if MatchingFileType(x, CLANG_EXTS)
5952 ]
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005953 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005954 rust_diff_files = [x for x in diff_files if MatchingFileType(x, RUST_EXTS)]
Olivier Robin0a6b5442022-04-07 07:25:04 +00005955 swift_diff_files = [x for x in diff_files if MatchingFileType(x, SWIFT_EXTS)]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005956 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005957
Edward Lesmes50da7702020-03-30 19:23:43 +00005958 top_dir = settings.GetRoot()
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005959
Jamie Madill5e96ad12020-01-13 16:08:35 +00005960 return_value = _RunClangFormatDiff(opts, clang_diff_files, top_dir,
5961 upstream_commit)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005962
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00005963 if opts.use_rust_fmt:
5964 rust_fmt_return_value = _RunRustFmt(opts, rust_diff_files, top_dir,
5965 upstream_commit)
5966 if rust_fmt_return_value == 2:
5967 return_value = 2
5968
Olivier Robin0a6b5442022-04-07 07:25:04 +00005969 if opts.use_swift_format:
5970 if sys.platform != 'darwin':
5971 DieWithError('swift-format is only supported on macOS.')
5972 swift_format_return_value = _RunSwiftFormat(opts, swift_diff_files, top_dir,
5973 upstream_commit)
5974 if swift_format_return_value == 2:
5975 return_value = 2
5976
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005977 # Similar code to above, but using yapf on .py files rather than clang-format
5978 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005979 py_explicitly_disabled = opts.python is not None and not opts.python
5980 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005981 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5982 yapf_tool = os.path.join(depot_tools_path, 'yapf')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005983
Aiden Bennerc08566e2018-10-03 17:52:42 +00005984 # Used for caching.
5985 yapf_configs = {}
5986 for f in python_diff_files:
5987 # Find the yapf style config for the current file, defaults to depot
5988 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005989 _FindYapfConfigFile(f, yapf_configs, top_dir)
5990
5991 # Turn on python formatting by default if a yapf config is specified.
5992 # This breaks in the case of this repo though since the specified
5993 # style file is also the global default.
5994 if opts.python is None:
5995 filtered_py_files = []
5996 for f in python_diff_files:
5997 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5998 filtered_py_files.append(f)
5999 else:
6000 filtered_py_files = python_diff_files
6001
6002 # Note: yapf still seems to fix indentation of the entire file
6003 # even if line ranges are specified.
6004 # See https://github.com/google/yapf/issues/499
6005 if not opts.full and filtered_py_files:
6006 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
6007
Brian Sheedyb4307d52019-12-02 19:18:17 +00006008 yapfignore_patterns = _GetYapfIgnorePatterns(top_dir)
6009 filtered_py_files = _FilterYapfIgnoredFiles(filtered_py_files,
6010 yapfignore_patterns)
Brian Sheedy59b06a82019-10-14 17:03:29 +00006011
Aiden Benner99b0ccb2018-11-20 19:53:31 +00006012 for f in filtered_py_files:
Andrew Grievefa40bfa2020-01-07 02:32:57 +00006013 yapf_style = _FindYapfConfigFile(f, yapf_configs, top_dir)
6014 # Default to pep8 if not .style.yapf is found.
6015 if not yapf_style:
6016 yapf_style = 'pep8'
Aiden Bennerc08566e2018-10-03 17:52:42 +00006017
Peter Wend9399922020-06-17 17:33:49 +00006018 with open(f, 'r') as py_f:
Andrew Grieveb9e694c2021-11-15 19:04:46 +00006019 if 'python2' in py_f.readline():
Peter Wend9399922020-06-17 17:33:49 +00006020 vpython_script = 'vpython'
Andrew Grieveb9e694c2021-11-15 19:04:46 +00006021 else:
6022 vpython_script = 'vpython3'
Peter Wend9399922020-06-17 17:33:49 +00006023
6024 cmd = [vpython_script, yapf_tool, '--style', yapf_style, f]
Aiden Bennerc08566e2018-10-03 17:52:42 +00006025
6026 has_formattable_lines = False
6027 if not opts.full:
6028 # Only run yapf over changed line ranges.
6029 for diff_start, diff_len in py_line_diffs[f]:
6030 diff_end = diff_start + diff_len - 1
6031 # Yapf errors out if diff_end < diff_start but this
6032 # is a valid line range diff for a removal.
6033 if diff_end >= diff_start:
6034 has_formattable_lines = True
6035 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
6036 # If all line diffs were removals we have nothing to format.
6037 if not has_formattable_lines:
6038 continue
6039
6040 if opts.diff or opts.dry_run:
6041 cmd += ['--diff']
6042 # Will return non-zero exit code if non-empty diff.
Edward Lesmesb7db1832020-06-22 20:22:27 +00006043 stdout = RunCommand(cmd,
6044 error_ok=True,
Josip Sokcevic673e8ed2021-10-27 23:46:18 +00006045 stderr=subprocess2.PIPE,
Edward Lesmesb7db1832020-06-22 20:22:27 +00006046 cwd=top_dir,
6047 shell=sys.platform.startswith('win32'))
Aiden Bennerc08566e2018-10-03 17:52:42 +00006048 if opts.diff:
6049 sys.stdout.write(stdout)
6050 elif len(stdout) > 0:
6051 return_value = 2
6052 else:
6053 cmd += ['-i']
Edward Lesmesb7db1832020-06-22 20:22:27 +00006054 RunCommand(cmd, cwd=top_dir, shell=sys.platform.startswith('win32'))
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006055
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006056 # Format GN build files. Always run on full build files for canonical form.
6057 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006058 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07006059 if opts.dry_run or opts.diff:
6060 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006061 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07006062 gn_ret = subprocess2.call(cmd + [gn_diff_file],
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00006063 shell=sys.platform.startswith('win'),
brettw4b8ed592016-08-05 16:19:12 -07006064 cwd=top_dir)
6065 if opts.dry_run and gn_ret == 2:
6066 return_value = 2 # Not formatted.
6067 elif opts.diff and gn_ret == 2:
6068 # TODO this should compute and print the actual diff.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006069 print('This change has GN build file diff for ' + gn_diff_file)
brettw4b8ed592016-08-05 16:19:12 -07006070 elif gn_ret != 0:
6071 # For non-dry run cases (and non-2 return values for dry-run), a
6072 # nonzero error code indicates a failure, probably because the file
6073 # doesn't parse.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006074 DieWithError('gn format failed on ' + gn_diff_file +
6075 '\nTry running `gn format` on this file manually.')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006076
Ilya Shermane081cbe2017-08-15 17:51:04 -07006077 # Skip the metrics formatting from the global presubmit hook. These files have
6078 # a separate presubmit hook that issues an error if the files need formatting,
6079 # whereas the top-level presubmit script merely issues a warning. Formatting
6080 # these files is somewhat slow, so it's important not to duplicate the work.
6081 if not opts.presubmit:
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006082 for diff_xml in GetDiffXMLs(diff_files):
6083 xml_dir = GetMetricsDir(diff_xml)
6084 if not xml_dir:
6085 continue
6086
Ilya Shermane081cbe2017-08-15 17:51:04 -07006087 tool_dir = os.path.join(top_dir, xml_dir)
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00006088 pretty_print_tool = os.path.join(tool_dir, 'pretty_print.py')
Fabrice de Gansecfab092022-09-15 20:59:01 +00006089 cmd = ['vpython3', pretty_print_tool, '--non-interactive']
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006090
6091 # If the XML file is histograms.xml or enums.xml, add the xml path to the
6092 # command as histograms/pretty_print.py now needs a relative path argument
6093 # after splitting the histograms into multiple directories.
6094 # For example, in tools/metrics/ukm, pretty-print could be run using:
6095 # $ python pretty_print.py
6096 # But in tools/metrics/histogrmas, pretty-print should be run with an
6097 # additional relative path argument, like:
Peter Kastingee088882021-08-03 17:57:00 +00006098 # $ python pretty_print.py metadata/UMA/histograms.xml
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006099 # $ python pretty_print.py enums.xml
6100
Weilun Shib92c4b72020-08-27 17:45:11 +00006101 if (diff_xml.endswith('histograms.xml') or diff_xml.endswith('enums.xml')
Weilun Shi4f50adb2023-01-17 20:43:17 +00006102 or diff_xml.endswith('histogram_suffixes_list.xml')):
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006103 cmd.append(diff_xml)
6104
Ilya Shermane081cbe2017-08-15 17:51:04 -07006105 if opts.dry_run or opts.diff:
6106 cmd.append('--diff')
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006107
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00006108 # TODO(isherman): Once this file runs only on Python 3.3+, drop the
6109 # `shell` param and instead replace `'vpython'` with
6110 # `shutil.which('frob')` above: https://stackoverflow.com/a/32799942
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006111 stdout = RunCommand(cmd,
6112 cwd=top_dir,
Ilya Sherman7aed4bb2020-05-20 22:34:14 +00006113 shell=sys.platform.startswith('win32'))
Ilya Shermane081cbe2017-08-15 17:51:04 -07006114 if opts.diff:
6115 sys.stdout.write(stdout)
6116 if opts.dry_run and stdout:
6117 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05006118
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006119 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006120
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006121
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006122def GetDiffXMLs(diff_files):
6123 return [
6124 os.path.normpath(x) for x in diff_files if MatchingFileType(x, ['.xml'])
6125 ]
6126
6127
6128def GetMetricsDir(diff_xml):
Steven Holte2e664bf2017-04-21 13:10:47 -07006129 metrics_xml_dirs = [
6130 os.path.join('tools', 'metrics', 'actions'),
6131 os.path.join('tools', 'metrics', 'histograms'),
6132 os.path.join('tools', 'metrics', 'rappor'),
Ilya Shermanb67e60c2020-05-20 22:27:03 +00006133 os.path.join('tools', 'metrics', 'structured'),
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006134 os.path.join('tools', 'metrics', 'ukm'),
6135 ]
Steven Holte2e664bf2017-04-21 13:10:47 -07006136 for xml_dir in metrics_xml_dirs:
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006137 if diff_xml.startswith(xml_dir):
6138 return xml_dir
6139 return None
Steven Holte2e664bf2017-04-21 13:10:47 -07006140
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006141
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006142@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006143@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006144def CMDcheckout(parser, args):
Edward Lemurf38bc172019-09-03 21:02:13 +00006145 """Checks out a branch associated with a given Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006146 _, args = parser.parse_args(args)
6147
6148 if len(args) != 1:
6149 parser.print_help()
6150 return 1
6151
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00006152 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00006153 if not issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00006154 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006155
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00006156 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006157
Edward Lemur52969c92020-02-06 18:15:28 +00006158 output = RunGit(['config', '--local', '--get-regexp',
Edward Lesmes50da7702020-03-30 19:23:43 +00006159 r'branch\..*\.' + ISSUE_CONFIG_KEY],
Edward Lemur52969c92020-02-06 18:15:28 +00006160 error_ok=True)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006161
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006162 branches = []
Edward Lemur52969c92020-02-06 18:15:28 +00006163 for key, issue in [x.split() for x in output.splitlines()]:
6164 if issue == target_issue:
Edward Lesmes50da7702020-03-30 19:23:43 +00006165 branches.append(re.sub(r'branch\.(.*)\.' + ISSUE_CONFIG_KEY, r'\1', key))
Edward Lemur52969c92020-02-06 18:15:28 +00006166
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006167 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07006168 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006169 return 1
6170 if len(branches) == 1:
6171 RunGit(['checkout', branches[0]])
6172 else:
vapiera7fbd5a2016-06-16 09:17:49 -07006173 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006174 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07006175 print('%d: %s' % (i, branches[i]))
Edward Lesmesae3586b2020-03-23 21:21:14 +00006176 which = gclient_utils.AskForData('Choose by index: ')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006177 try:
6178 RunGit(['checkout', branches[int(which)]])
6179 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07006180 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006181 return 1
6182
6183 return 0
6184
6185
maruel@chromium.org29404b52014-09-08 22:58:00 +00006186def CMDlol(parser, args):
6187 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07006188 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00006189 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6190 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6191 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
Gavin Mak18f45d22020-12-04 21:45:10 +00006192 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')).decode('utf-8'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00006193 return 0
6194
6195
Josip Sokcevic0399e172022-03-21 23:11:51 +00006196def CMDversion(parser, args):
Josip Sokcevic0399e172022-03-21 23:11:51 +00006197 print(utils.depot_tools_version())
6198
6199
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006200class OptionParser(optparse.OptionParser):
6201 """Creates the option parse and add --verbose support."""
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00006202
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006203 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006204 optparse.OptionParser.__init__(
6205 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006206 self.add_option(
6207 '-v', '--verbose', action='count', default=0,
6208 help='Use 2 times for more debugging info')
6209
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006210 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00006211 try:
6212 return self._parse_args(args)
6213 finally:
6214 # Regardless of success or failure of args parsing, we want to report
6215 # metrics, but only after logging has been initialized (if parsing
6216 # succeeded).
6217 global settings
6218 settings = Settings()
6219
Edward Lesmes9c349062021-05-06 20:02:39 +00006220 if metrics.collector.config.should_collect_metrics:
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00006221 # GetViewVCUrl ultimately calls logging method.
6222 project_url = settings.GetViewVCUrl().strip('/+')
6223 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
6224 metrics.collector.add('project_urls', [project_url])
6225
6226 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006227 # Create an optparse.Values object that will store only the actual passed
6228 # options, without the defaults.
6229 actual_options = optparse.Values()
6230 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
6231 # Create an optparse.Values object with the default options.
6232 options = optparse.Values(self.get_default_values().__dict__)
6233 # Update it with the options passed by the user.
6234 options._update_careful(actual_options.__dict__)
6235 # Store the options passed by the user in an _actual_options attribute.
6236 # We store only the keys, and not the values, since the values can contain
6237 # arbitrary information, which might be PII.
Edward Lemur79d4f992019-11-11 23:49:02 +00006238 metrics.collector.add('arguments', list(actual_options.__dict__.keys()))
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006239
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006240 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01006241 logging.basicConfig(
6242 level=levels[min(options.verbose, len(levels) - 1)],
6243 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6244 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00006245
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006246 return options, args
6247
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006248
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006249def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006250 if sys.hexversion < 0x02060000:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006251 print('\nYour Python version %s is unsupported, please upgrade.\n' %
vapiera7fbd5a2016-06-16 09:17:49 -07006252 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006253 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006254
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006255 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006256 dispatcher = subcommand.CommandDispatcher(__name__)
6257 try:
6258 return dispatcher.execute(OptionParser(), argv)
Edward Lemur5b929a42019-10-21 17:57:39 +00006259 except auth.LoginRequiredError as e:
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006260 DieWithError(str(e))
Edward Lemur79d4f992019-11-11 23:49:02 +00006261 except urllib.error.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006262 if e.code != 500:
6263 raise
6264 DieWithError(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006265 ('App Engine is misbehaving and returned HTTP %d, again. Keep faith '
Quinten Yearsleyc4202212019-07-22 23:34:40 +00006266 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006267 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006268
6269
6270if __name__ == '__main__':
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006271 # These affect sys.stdout, so do it outside of main() to simplify mocks in
6272 # the unit tests.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006273 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006274 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00006275 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00006276 sys.exit(main(sys.argv[1:]))