blob: 406db6464c4ae117dd58a1af2afd98e78c298273 [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>
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00007"""A git-command for integrating reviews on Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008
thakis@chromium.org3421c992014-11-02 02:20:32 +00009import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000010import collections
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010011import datetime
Brian Sheedyb4307d52019-12-02 19:18:17 +000012import fnmatch
Edward Lemur202c5592019-10-21 22:44:52 +000013import httplib2
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010014import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000015import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000017import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import optparse
19import os
20import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010021import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000022import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070024import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000026import time
Gavin Mak7f5b53f2023-09-07 18:13:01 +000027import urllib.parse
28import urllib.request
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000029import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000030import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000031import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000032
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000033from third_party import colorama
Daniel Chengabf48472023-08-30 15:45:13 +000034from typing import Any
35from typing import List
36from typing import Mapping
37from typing import NoReturn
Daniel Cheng66d0f152023-08-29 23:21:58 +000038from typing import Optional
39from typing import Sequence
40from typing import Tuple
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000041import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000042import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000043import fix_encoding
Andrew Grieved7ba85d2023-09-15 18:28:33 +000044import gclient_paths
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000045import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000046import gerrit_util
iannucci@chromium.org9e849272014-04-04 00:31:55 +000047import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000048import git_footers
Edward Lemur85153282020-02-14 22:06:29 +000049import git_new_branch
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000050import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000051import metrics_utils
Edward Lesmeseeca9c62020-11-20 00:00:17 +000052import owners_client
iannucci@chromium.org9e849272014-04-04 00:31:55 +000053import owners_finder
Lei Zhangb8c62cf2020-07-15 20:09:37 +000054import presubmit_canned_checks
Josip Sokcevic7958e302023-03-01 23:02:21 +000055import presubmit_support
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +000056import rustfmt
Josip Sokcevic7958e302023-03-01 23:02:21 +000057import scm
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000058import setup_color
Francois Dorayd42c6812017-05-30 15:10:20 -040059import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000060import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000061import subprocess2
Olivier Robin0a6b5442022-04-07 07:25:04 +000062import swift_format
maruel@chromium.org2a74d372011-03-29 19:05:50 +000063import watchlists
64
Edward Lemur79d4f992019-11-11 23:49:02 +000065
tandrii7400cf02016-06-21 08:48:07 -070066__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000067
Mike Frysinger124bb8e2023-09-06 05:48:55 +000068# TODO: Should fix these warnings.
69# pylint: disable=line-too-long
70
Edward Lemur0f58ae42019-04-30 17:24:12 +000071# Traces for git push will be stored in a traces directory inside the
72# depot_tools checkout.
73DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
74TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
Edward Lemur227d5102020-02-25 23:45:35 +000075PRESUBMIT_SUPPORT = os.path.join(DEPOT_TOOLS, 'presubmit_support.py')
Edward Lemur0f58ae42019-04-30 17:24:12 +000076
77# When collecting traces, Git hashes will be reduced to 6 characters to reduce
78# the size after compression.
79GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
80# Used to redact the cookies from the gitcookies file.
81GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
82
Edward Lemurd4d1ba42019-09-20 21:46:37 +000083MAX_ATTEMPTS = 3
84
Edward Lemur1b52d872019-05-09 21:12:12 +000085# The maximum number of traces we will keep. Multiplied by 3 since we store
86# 3 files per trace.
87MAX_TRACES = 3 * 10
Edward Lemur5737f022019-05-17 01:24:00 +000088# Message to be displayed to the user to inform where to find the traces for a
89# git-cl upload execution.
Edward Lemur0f58ae42019-04-30 17:24:12 +000090TRACES_MESSAGE = (
Mike Frysinger124bb8e2023-09-06 05:48:55 +000091 '\n'
92 'The traces of this git-cl execution have been recorded at:\n'
93 ' %(trace_name)s-traces.zip\n'
94 'Copies of your gitcookies file and git config have been recorded at:\n'
95 ' %(trace_name)s-git-info.zip\n')
Edward Lemur1b52d872019-05-09 21:12:12 +000096# Format of the message to be stored as part of the traces to give developers a
97# better context when they go through traces.
Mike Frysinger124bb8e2023-09-06 05:48:55 +000098TRACES_README_FORMAT = ('Date: %(now)s\n'
99 '\n'
100 'Change: https://%(gerrit_host)s/q/%(change_id)s\n'
101 'Title: %(title)s\n'
102 '\n'
103 '%(description)s\n'
104 '\n'
105 'Execution time: %(execution_time)s\n'
106 'Exit code: %(exit_code)s\n') + TRACES_MESSAGE
Edward Lemur0f58ae42019-04-30 17:24:12 +0000107
Aaron Gable1bc7bfe2016-12-19 10:08:14 -0800108POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
Henrique Ferreiroff249622019-11-28 23:19:29 +0000109DESCRIPTION_BACKUP_FILE = '.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +0000110REFS_THAT_ALIAS_TO_OTHER_REFS = {
Josip Sokcevic7e133ff2021-07-13 17:44:53 +0000111 'refs/remotes/origin/lkgr': 'refs/remotes/origin/main',
112 'refs/remotes/origin/lkcr': 'refs/remotes/origin/main',
rmistry@google.comc68112d2015-03-03 12:48:06 +0000113}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000114
Josip Sokcevicc39ab992020-09-24 20:09:15 +0000115DEFAULT_OLD_BRANCH = 'refs/remotes/origin/master'
116DEFAULT_NEW_BRANCH = 'refs/remotes/origin/main'
117
Joanna Wanga8db0cb2023-01-24 15:43:17 +0000118DEFAULT_BUILDBUCKET_HOST = 'cr-buildbucket.appspot.com'
119
thestig@chromium.org44202a22014-03-11 19:22:18 +0000120# Valid extensions for files we want to lint.
121DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
122DEFAULT_LINT_IGNORE_REGEX = r"$^"
123
Aiden Bennerc08566e2018-10-03 17:52:42 +0000124# File name for yapf style config files.
125YAPF_CONFIG_FILENAME = '.style.yapf'
126
Edward Lesmes50da7702020-03-30 19:23:43 +0000127# The issue, patchset and codereview server are stored on git config for each
128# branch under branch.<branch-name>.<config-key>.
129ISSUE_CONFIG_KEY = 'gerritissue'
130PATCHSET_CONFIG_KEY = 'gerritpatchset'
131CODEREVIEW_SERVER_CONFIG_KEY = 'gerritserver'
Gavin Makbe2e9262022-11-08 23:41:55 +0000132# When using squash workflow, _CMDUploadChange doesn't simply push the commit(s)
133# you make to Gerrit. Instead, it creates a new commit object that contains all
134# changes you've made, diffed against a parent/merge base.
135# This is the hash of the new squashed commit and you can find this on Gerrit.
136GERRIT_SQUASH_HASH_CONFIG_KEY = 'gerritsquashhash'
137# This is the latest uploaded local commit hash.
138LAST_UPLOAD_HASH_CONFIG_KEY = 'last-upload-hash'
Edward Lesmes50da7702020-03-30 19:23:43 +0000139
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000140# Shortcut since it quickly becomes repetitive.
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000141Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000142
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000143# Initialized in main()
144settings = None
145
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100146# Used by tests/git_cl_test.py to add extra logging.
147# Inside the weirdly failing test, add this:
148# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700149# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100150_IS_BEING_TESTED = False
151
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000152_GOOGLESOURCE = 'googlesource.com'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000153
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000154_KNOWN_GERRIT_TO_SHORT_URLS = {
155 'https://chrome-internal-review.googlesource.com': 'https://crrev.com/i',
156 'https://chromium-review.googlesource.com': 'https://crrev.com/c',
157}
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000158assert len(_KNOWN_GERRIT_TO_SHORT_URLS) == len(
159 set(_KNOWN_GERRIT_TO_SHORT_URLS.values())), 'must have unique values'
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000160
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
Joanna Wang892f2ce2023-03-14 21:39:47 +0000165# Environment variable to indicate if user is participating in the stcked
166# changes dogfood.
167DOGFOOD_STACKED_CHANGES_VAR = 'DOGFOOD_STACKED_CHANGES'
168
169
Josip Sokcevicf736cab2020-10-20 23:41:38 +0000170class GitPushError(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000171 pass
Josip Sokcevicf736cab2020-10-20 23:41:38 +0000172
173
Daniel Chengabf48472023-08-30 15:45:13 +0000174def DieWithError(message, change_desc=None) -> NoReturn:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000175 if change_desc:
176 SaveDescriptionBackup(change_desc)
177 print('\n ** Content of CL description **\n' + '=' * 72 + '\n' +
178 change_desc.description + '\n' + '=' * 72 + '\n')
Christopher Lamf732cd52017-01-24 12:40:11 +1100179
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000180 print(message, file=sys.stderr)
181 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000182
183
Christopher Lamf732cd52017-01-24 12:40:11 +1100184def SaveDescriptionBackup(change_desc):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000185 backup_path = os.path.join(DEPOT_TOOLS, DESCRIPTION_BACKUP_FILE)
186 print('\nsaving CL description to %s\n' % backup_path)
187 with open(backup_path, 'wb') as backup_file:
188 backup_file.write(change_desc.description.encode('utf-8'))
Christopher Lamf732cd52017-01-24 12:40:11 +1100189
190
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000191def GetNoGitPagerEnv():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000192 env = os.environ.copy()
193 # 'cat' is a magical git string that disables pagers on all platforms.
194 env['GIT_PAGER'] = 'cat'
195 return env
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000196
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000197
bsep@chromium.org627d9002016-04-29 00:00:52 +0000198def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000199 try:
200 stdout = subprocess2.check_output(args, shell=shell, **kwargs)
201 return stdout.decode('utf-8', 'replace')
202 except subprocess2.CalledProcessError as e:
203 logging.debug('Failed running %s', args)
204 if not error_ok:
205 message = error_message or e.stdout.decode('utf-8', 'replace') or ''
206 DieWithError('Command "%s" failed.\n%s' % (' '.join(args), message))
207 out = e.stdout.decode('utf-8', 'replace')
208 if e.stderr:
209 out += e.stderr.decode('utf-8', 'replace')
210 return out
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000211
212
213def RunGit(args, **kwargs):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000214 """Returns stdout."""
215 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000216
217
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000218def RunGitWithCode(args, suppress_stderr=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000219 """Returns return code and stdout."""
220 if suppress_stderr:
221 stderr = subprocess2.DEVNULL
222 else:
223 stderr = sys.stderr
224 try:
225 (out, _), code = subprocess2.communicate(['git'] + args,
226 env=GetNoGitPagerEnv(),
227 stdout=subprocess2.PIPE,
228 stderr=stderr)
229 return code, out.decode('utf-8', 'replace')
230 except subprocess2.CalledProcessError as e:
231 logging.debug('Failed running %s', ['git'] + args)
232 return e.returncode, e.stdout.decode('utf-8', 'replace')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000233
234
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000235def RunGitSilent(args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000236 """Returns stdout, suppresses stderr and ignores the return code."""
237 return RunGitWithCode(args, suppress_stderr=True)[1]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000238
239
tandrii2a16b952016-10-19 07:09:44 -0700240def time_sleep(seconds):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000241 # Use this so that it can be mocked in tests without interfering with python
242 # system machinery.
243 return time.sleep(seconds)
tandrii2a16b952016-10-19 07:09:44 -0700244
245
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000246def time_time():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000247 # Use this so that it can be mocked in tests without interfering with python
248 # system machinery.
249 return time.time()
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000250
251
Edward Lemur1b52d872019-05-09 21:12:12 +0000252def datetime_now():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000253 # Use this so that it can be mocked in tests without interfering with python
254 # system machinery.
255 return datetime.datetime.now()
Edward Lemur1b52d872019-05-09 21:12:12 +0000256
257
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100258def confirm_or_exit(prefix='', action='confirm'):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000259 """Asks user to press enter to continue or press Ctrl+C to abort."""
260 if not prefix or prefix.endswith('\n'):
261 mid = 'Press'
262 elif prefix.endswith('.') or prefix.endswith('?'):
263 mid = ' Press'
264 elif prefix.endswith(' '):
265 mid = 'press'
266 else:
267 mid = ' press'
268 gclient_utils.AskForData('%s%s Enter to %s, or Ctrl+C to abort' %
269 (prefix, mid, action))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100270
271
272def ask_for_explicit_yes(prompt):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000273 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
274 result = gclient_utils.AskForData(prompt + ' [Yes/No]: ').lower()
275 while True:
276 if 'yes'.startswith(result):
277 return True
278 if 'no'.startswith(result):
279 return False
280 result = gclient_utils.AskForData('Please, type yes or no: ').lower()
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100281
282
machenbach@chromium.org45453142015-09-15 08:45:22 +0000283def _get_properties_from_options(options):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000284 prop_list = getattr(options, 'properties', [])
285 properties = dict(x.split('=', 1) for x in prop_list)
286 for key, val in properties.items():
287 try:
288 properties[key] = json.loads(val)
289 except ValueError:
290 pass # If a value couldn't be evaluated, treat it as a string.
291 return properties
machenbach@chromium.org45453142015-09-15 08:45:22 +0000292
293
Edward Lemur4c707a22019-09-24 21:13:43 +0000294def _call_buildbucket(http, buildbucket_host, method, request):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000295 """Calls a buildbucket v2 method and returns the parsed json response."""
296 headers = {
297 'Accept': 'application/json',
298 'Content-Type': 'application/json',
299 }
300 request = json.dumps(request)
301 url = 'https://%s/prpc/buildbucket.v2.Builds/%s' % (buildbucket_host,
302 method)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000303
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000304 logging.info('POST %s with %s' % (url, request))
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000305
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000306 attempts = 1
307 time_to_sleep = 1
308 while True:
309 response, content = http.request(url,
310 'POST',
311 body=request,
312 headers=headers)
313 if response.status == 200:
314 return json.loads(content[4:])
315 if attempts >= MAX_ATTEMPTS or 400 <= response.status < 500:
316 msg = '%s error when calling POST %s with %s: %s' % (
317 response.status, url, request, content)
318 raise BuildbucketResponseException(msg)
319 logging.debug('%s error when calling POST %s with %s. '
320 'Sleeping for %d seconds and retrying...' %
321 (response.status, url, request, time_to_sleep))
322 time.sleep(time_to_sleep)
323 time_to_sleep *= 2
324 attempts += 1
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000325
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000326 assert False, 'unreachable'
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000327
328
Edward Lemur6215c792019-10-03 21:59:05 +0000329def _parse_bucket(raw_bucket):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000330 legacy = True
331 project = bucket = None
332 if '/' in raw_bucket:
333 legacy = False
334 project, bucket = raw_bucket.split('/', 1)
335 # Assume luci.<project>.<bucket>.
336 elif raw_bucket.startswith('luci.'):
337 project, bucket = raw_bucket[len('luci.'):].split('.', 1)
338 # Otherwise, assume prefix is also the project name.
339 elif '.' in raw_bucket:
340 project = raw_bucket.split('.')[0]
341 bucket = raw_bucket
342 # Legacy buckets.
343 if legacy and project and bucket:
344 print('WARNING Please use %s/%s to specify the bucket.' %
345 (project, bucket))
346 return project, bucket
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000347
348
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000349def _canonical_git_googlesource_host(host):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000350 """Normalizes Gerrit hosts (with '-review') to Git host."""
351 assert host.endswith(_GOOGLESOURCE)
352 # Prefix doesn't include '.' at the end.
353 prefix = host[:-(1 + len(_GOOGLESOURCE))]
354 if prefix.endswith('-review'):
355 prefix = prefix[:-len('-review')]
356 return prefix + '.' + _GOOGLESOURCE
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000357
358
359def _canonical_gerrit_googlesource_host(host):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000360 git_host = _canonical_git_googlesource_host(host)
361 prefix = git_host.split('.', 1)[0]
362 return prefix + '-review.' + _GOOGLESOURCE
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000363
364
365def _get_counterpart_host(host):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000366 assert host.endswith(_GOOGLESOURCE)
367 git = _canonical_git_googlesource_host(host)
368 gerrit = _canonical_gerrit_googlesource_host(git)
369 return git if gerrit == host else gerrit
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000370
371
Quinten Yearsley777660f2020-03-04 23:37:06 +0000372def _trigger_tryjobs(changelist, jobs, options, patchset):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000373 """Sends a request to Buildbucket to trigger tryjobs for a changelist.
qyearsley1fdfcb62016-10-24 13:22:03 -0700374
375 Args:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000376 changelist: Changelist that the tryjobs are associated with.
Edward Lemur45768512020-03-02 19:03:14 +0000377 jobs: A list of (project, bucket, builder).
qyearsley1fdfcb62016-10-24 13:22:03 -0700378 options: Command-line options.
379 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000380 print('Scheduling jobs on:')
381 for project, bucket, builder in jobs:
382 print(' %s/%s: %s' % (project, bucket, builder))
383 print('To see results here, run: git cl try-results')
384 print('To see results in browser, run: git cl web')
tandriide281ae2016-10-12 06:02:30 -0700385
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000386 requests = _make_tryjob_schedule_requests(changelist, jobs, options,
387 patchset)
388 if not requests:
389 return
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000390
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000391 http = auth.Authenticator().authorize(httplib2.Http())
392 http.force_exception_to_status_code = True
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000393
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000394 batch_request = {'requests': requests}
395 batch_response = _call_buildbucket(http, DEFAULT_BUILDBUCKET_HOST, 'Batch',
396 batch_request)
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000397
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000398 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))
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000406
407
Quinten Yearsley777660f2020-03-04 23:37:06 +0000408def _make_tryjob_schedule_requests(changelist, jobs, options, patchset):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000409 """Constructs requests for Buildbucket to trigger tryjobs."""
410 gerrit_changes = [changelist.GetGerritChange(patchset)]
411 shared_properties = {
412 'category': options.ensure_value('category', 'git_cl_try')
413 }
414 if options.ensure_value('clobber', False):
415 shared_properties['clobber'] = True
416 shared_properties.update(_get_properties_from_options(options) or {})
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000417
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000418 shared_tags = [{'key': 'user_agent', 'value': 'git_cl_try'}]
419 if options.ensure_value('retry_failed', False):
420 shared_tags.append({'key': 'retry_failed', 'value': '1'})
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000421
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000422 requests = []
423 for (project, bucket, builder) in jobs:
424 properties = shared_properties.copy()
425 if 'presubmit' in builder.lower():
426 properties['dry_run'] = 'true'
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000427
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000428 requests.append({
429 'scheduleBuild': {
430 'requestId': str(uuid.uuid4()),
431 'builder': {
432 'project': getattr(options, 'project', None) or project,
433 'bucket': bucket,
434 'builder': builder,
435 },
436 'gerritChanges': gerrit_changes,
437 'properties': properties,
438 'tags': [
439 {
440 'key': 'builder',
441 'value': builder
442 },
443 ] + shared_tags,
444 }
445 })
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000446
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000447 if options.ensure_value('revision', None):
448 remote, remote_branch = changelist.GetRemoteBranch()
449 requests[-1]['scheduleBuild']['gitilesCommit'] = {
450 'host':
451 _canonical_git_googlesource_host(gerrit_changes[0]['host']),
452 'project': gerrit_changes[0]['project'],
453 'id': options.revision,
454 'ref': GetTargetRef(remote, remote_branch, None)
455 }
Anthony Polito1a5fe232020-01-24 23:17:52 +0000456
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000457 return requests
kjellander@chromium.org44424542015-06-02 18:35:29 +0000458
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000459
Quinten Yearsley777660f2020-03-04 23:37:06 +0000460def _fetch_tryjobs(changelist, buildbucket_host, patchset=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000461 """Fetches tryjobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000462
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000463 Returns list of buildbucket.v2.Build with the try jobs for the changelist.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000464 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000465 fields = ['id', 'builder', 'status', 'createTime', 'tags']
466 request = {
467 'predicate': {
468 'gerritChanges': [changelist.GetGerritChange(patchset)],
469 },
470 'fields': ','.join('builds.*.' + field for field in fields),
471 }
tandrii221ab252016-10-06 08:12:04 -0700472
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000473 authenticator = auth.Authenticator()
474 if authenticator.has_cached_credentials():
475 http = authenticator.authorize(httplib2.Http())
476 else:
477 print('Warning: Some results might be missing because %s' %
478 # Get the message on how to login.
479 (
480 str(auth.LoginRequiredError()), ))
481 http = httplib2.Http()
482 http.force_exception_to_status_code = True
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000483
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000484 response = _call_buildbucket(http, buildbucket_host, 'SearchBuilds',
485 request)
486 return response.get('builds', [])
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000487
Edward Lemur45768512020-03-02 19:03:14 +0000488
Edward Lemur5b929a42019-10-21 17:57:39 +0000489def _fetch_latest_builds(changelist, buildbucket_host, latest_patchset=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000490 """Fetches builds from the latest patchset that has builds (within
Quinten Yearsley983111f2019-09-26 17:18:48 +0000491 the last few patchsets).
492
493 Args:
Quinten Yearsley983111f2019-09-26 17:18:48 +0000494 changelist (Changelist): The CL to fetch builds for
495 buildbucket_host (str): Buildbucket host, e.g. "cr-buildbucket.appspot.com"
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000496 lastest_patchset(int|NoneType): the patchset to start fetching builds from.
497 If None (default), starts with the latest available patchset.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000498 Returns:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000499 A tuple (builds, patchset) where builds is a list of buildbucket.v2.Build,
500 and patchset is the patchset number where those builds came from.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000501 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000502 assert buildbucket_host
503 assert changelist.GetIssue(), 'CL must be uploaded first'
504 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
505 if latest_patchset is None:
506 assert changelist.GetMostRecentPatchset()
507 ps = changelist.GetMostRecentPatchset()
508 else:
509 assert latest_patchset > 0, latest_patchset
510 ps = latest_patchset
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000511
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000512 min_ps = max(1, ps - 5)
513 while ps >= min_ps:
514 builds = _fetch_tryjobs(changelist, buildbucket_host, patchset=ps)
515 if len(builds):
516 return builds, ps
517 ps -= 1
518 return [], 0
Quinten Yearsley983111f2019-09-26 17:18:48 +0000519
520
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000521def _filter_failed_for_retry(all_builds):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000522 """Returns a list of buckets/builders that are worth retrying.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000523
524 Args:
Quinten Yearsley777660f2020-03-04 23:37:06 +0000525 all_builds (list): Builds, in the format returned by _fetch_tryjobs,
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000526 i.e. a list of buildbucket.v2.Builds which includes status and builder
527 info.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000528
529 Returns:
Edward Lemur45768512020-03-02 19:03:14 +0000530 A dict {(proj, bucket): [builders]}. This is the same format accepted by
Quinten Yearsley777660f2020-03-04 23:37:06 +0000531 _trigger_tryjobs.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000532 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000533 grouped = {}
534 for build in all_builds:
535 builder = build['builder']
536 key = (builder['project'], builder['bucket'], builder['builder'])
537 grouped.setdefault(key, []).append(build)
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000538
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000539 jobs = []
540 for (project, bucket, builder), builds in grouped.items():
541 if 'triggered' in builder:
542 print(
543 'WARNING: Not scheduling %s. Triggered bots require an initial job '
544 'from a parent. Please schedule a manual job for the parent '
545 'instead.')
546 continue
547 if any(b['status'] in ('STARTED', 'SCHEDULED') for b in builds):
548 # Don't retry if any are running.
549 continue
550 # If builder had several builds, retry only if the last one failed.
551 # This is a bit different from CQ, which would re-use *any* SUCCESS-full
552 # build, but in case of retrying failed jobs retrying a flaky one makes
553 # sense.
554 builds = sorted(builds, key=lambda b: b['createTime'])
555 if builds[-1]['status'] not in ('FAILURE', 'INFRA_FAILURE'):
556 continue
557 # Don't retry experimental build previously triggered by CQ.
558 if any(t['key'] == 'cq_experimental' and t['value'] == 'true'
559 for t in builds[-1]['tags']):
560 continue
561 jobs.append((project, bucket, builder))
Edward Lemur45768512020-03-02 19:03:14 +0000562
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000563 # Sort the jobs to make testing easier.
564 return sorted(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000565
566
Quinten Yearsley777660f2020-03-04 23:37:06 +0000567def _print_tryjobs(options, builds):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000568 """Prints nicely result of _fetch_tryjobs."""
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000569 if not builds:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000570 print('No tryjobs scheduled.')
571 return
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000572
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000573 longest_builder = max(len(b['builder']['builder']) for b in builds)
574 name_fmt = '{builder:<%d}' % longest_builder
575 if options.print_master:
576 longest_bucket = max(len(b['builder']['bucket']) for b in builds)
577 name_fmt = ('{bucket:>%d} ' % longest_bucket) + name_fmt
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000578
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000579 builds_by_status = {}
580 for b in builds:
581 builds_by_status.setdefault(b['status'], []).append({
582 'id':
583 b['id'],
584 'name':
585 name_fmt.format(builder=b['builder']['builder'],
586 bucket=b['builder']['bucket']),
587 })
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000588
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000589 sort_key = lambda b: (b['name'], b['id'])
590
591 def print_builds(title, builds, fmt=None, color=None):
592 """Pop matching builds from `builds` dict and print them."""
593 if not builds:
594 return
595
596 fmt = fmt or '{name} https://ci.chromium.org/b/{id}'
597 if not options.color or color is None:
598 colorize = lambda x: x
599 else:
600 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
601
602 print(colorize(title))
603 for b in sorted(builds, key=sort_key):
604 print(' ', colorize(fmt.format(**b)))
605
606 total = len(builds)
607 print_builds('Successes:',
608 builds_by_status.pop('SUCCESS', []),
609 color=Fore.GREEN)
610 print_builds('Infra Failures:',
611 builds_by_status.pop('INFRA_FAILURE', []),
612 color=Fore.MAGENTA)
613 print_builds('Failures:',
614 builds_by_status.pop('FAILURE', []),
615 color=Fore.RED)
616 print_builds('Canceled:',
617 builds_by_status.pop('CANCELED', []),
618 fmt='{name}',
619 color=Fore.MAGENTA)
620 print_builds('Started:',
621 builds_by_status.pop('STARTED', []),
622 color=Fore.YELLOW)
623 print_builds('Scheduled:',
624 builds_by_status.pop('SCHEDULED', []),
625 fmt='{name} id={id}')
626 # The last section is just in case buildbucket API changes OR there is a
627 # bug.
628 print_builds('Other:',
629 sum(builds_by_status.values(), []),
630 fmt='{name} id={id}')
631 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000632
633
Andrew Grieved7ba85d2023-09-15 18:28:33 +0000634def _ComputeFormatDiffLineRanges(files, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000635 """Gets the changed line ranges for each file since upstream_commit.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000636
637 Parses a git diff on provided files and returns a dict that maps a file name
638 to an ordered list of range tuples in the form (start_line, count).
639 Ranges are in the same format as a git diff.
640 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000641 # If files is empty then diff_output will be a full diff.
642 if len(files) == 0:
643 return {}
Aiden Bennerc08566e2018-10-03 17:52:42 +0000644
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000645 # Take the git diff and find the line ranges where there are changes.
Arthur Eubanks92d8c4e2023-10-09 19:57:24 +0000646 diff_cmd = BuildGitDiffCmd(['-U0'],
647 upstream_commit,
648 files,
649 allow_prefix=True)
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000650 diff_output = RunGit(diff_cmd)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000651
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000652 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
653 # 2 capture groups
654 # 0 == fname of diff file
655 # 1 == 'diff_start,diff_count' or 'diff_start'
656 # will match each of
657 # diff --git a/foo.foo b/foo.py
658 # @@ -12,2 +14,3 @@
659 # @@ -12,2 +17 @@
660 # running re.findall on the above string with pattern will give
661 # [('foo.py', ''), ('', '14,3'), ('', '17')]
Aiden Bennerc08566e2018-10-03 17:52:42 +0000662
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000663 curr_file = None
664 line_diffs = {}
665 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
666 if match[0] != '':
667 # Will match the second filename in diff --git a/a.py b/b.py.
668 curr_file = match[0]
669 line_diffs[curr_file] = []
670 else:
671 # Matches +14,3
672 if ',' in match[1]:
673 diff_start, diff_count = match[1].split(',')
674 else:
675 # Single line changes are of the form +12 instead of +12,1.
676 diff_start = match[1]
677 diff_count = 1
Aiden Bennerc08566e2018-10-03 17:52:42 +0000678
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000679 diff_start = int(diff_start)
680 diff_count = int(diff_count)
Andrew Grieved7ba85d2023-09-15 18:28:33 +0000681 diff_end = diff_start + diff_count - 1
Aiden Bennerc08566e2018-10-03 17:52:42 +0000682
Andrew Grieved7ba85d2023-09-15 18:28:33 +0000683 # Only format added ranges (not removed ones).
684 if diff_end >= diff_start:
685 line_diffs[curr_file].append((diff_start, diff_end))
Aiden Bennerc08566e2018-10-03 17:52:42 +0000686
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000687 return line_diffs
Aiden Bennerc08566e2018-10-03 17:52:42 +0000688
689
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000690def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000691 """Checks if a yapf file is in any parent directory of fpath until top_dir.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000692
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000693 Recursively checks parent directories to find yapf file and if no yapf file
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000694 is found returns None. Uses yapf_config_cache as a cache for previously found
695 configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000696 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000697 fpath = os.path.abspath(fpath)
698 # Return result if we've already computed it.
699 if fpath in yapf_config_cache:
700 return yapf_config_cache[fpath]
Aiden Bennerc08566e2018-10-03 17:52:42 +0000701
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000702 parent_dir = os.path.dirname(fpath)
703 if os.path.isfile(fpath):
704 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000705 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000706 # Otherwise fpath is a directory
707 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
708 if os.path.isfile(yapf_file):
709 ret = yapf_file
710 elif fpath in (top_dir, parent_dir):
711 # If we're at the top level directory, or if we're at root
712 # there is no provided style.
713 ret = None
714 else:
715 # Otherwise recurse on the current directory.
716 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
717 yapf_config_cache[fpath] = ret
718 return ret
Aiden Bennerc08566e2018-10-03 17:52:42 +0000719
720
Brian Sheedyb4307d52019-12-02 19:18:17 +0000721def _GetYapfIgnorePatterns(top_dir):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000722 """Returns all patterns in the .yapfignore file.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000723
724 yapf is supposed to handle the ignoring of files listed in .yapfignore itself,
725 but this functionality appears to break when explicitly passing files to
726 yapf for formatting. According to
Josip Sokcevicc39ab992020-09-24 20:09:15 +0000727 https://github.com/google/yapf/blob/HEAD/README.rst#excluding-files-from-formatting-yapfignore,
Brian Sheedy59b06a82019-10-14 17:03:29 +0000728 the .yapfignore file should be in the directory that yapf is invoked from,
729 which we assume to be the top level directory in this case.
730
731 Args:
732 top_dir: The top level directory for the repository being formatted.
733
734 Returns:
Brian Sheedyb4307d52019-12-02 19:18:17 +0000735 A set of all fnmatch patterns to be ignored.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000736 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000737 yapfignore_file = os.path.join(top_dir, '.yapfignore')
738 ignore_patterns = set()
739 if not os.path.exists(yapfignore_file):
740 return ignore_patterns
Brian Sheedy59b06a82019-10-14 17:03:29 +0000741
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000742 for line in gclient_utils.FileRead(yapfignore_file).split('\n'):
743 stripped_line = line.strip()
744 # Comments and blank lines should be ignored.
745 if stripped_line.startswith('#') or stripped_line == '':
746 continue
747 ignore_patterns.add(stripped_line)
748 return ignore_patterns
Brian Sheedyb4307d52019-12-02 19:18:17 +0000749
750
751def _FilterYapfIgnoredFiles(filepaths, patterns):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000752 """Filters out any filepaths that match any of the given patterns.
Brian Sheedyb4307d52019-12-02 19:18:17 +0000753
754 Args:
755 filepaths: An iterable of strings containing filepaths to filter.
756 patterns: An iterable of strings containing fnmatch patterns to filter on.
757
758 Returns:
759 A list of strings containing all the elements of |filepaths| that did not
760 match any of the patterns in |patterns|.
761 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000762 # Not inlined so that tests can use the same implementation.
763 return [
764 f for f in filepaths
765 if not any(fnmatch.fnmatch(f, p) for p in patterns)
766 ]
Brian Sheedy59b06a82019-10-14 17:03:29 +0000767
768
Daniel Cheng66d0f152023-08-29 23:21:58 +0000769def _GetCommitCountSummary(begin_commit: str,
770 end_commit: str = "HEAD") -> Optional[str]:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000771 """Generate a summary of the number of commits in (begin_commit, end_commit).
Daniel Cheng66d0f152023-08-29 23:21:58 +0000772
773 Returns a string containing the summary, or None if the range is empty.
774 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000775 count = int(
776 RunGitSilent(['rev-list', '--count', f'{begin_commit}..{end_commit}']))
Daniel Cheng66d0f152023-08-29 23:21:58 +0000777
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000778 if not count:
779 return None
Daniel Cheng66d0f152023-08-29 23:21:58 +0000780
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000781 return f'{count} commit{"s"[:count!=1]}'
Daniel Cheng66d0f152023-08-29 23:21:58 +0000782
783
Aaron Gable13101a62018-02-09 13:20:41 -0800784def print_stats(args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000785 """Prints statistics about the change to the user."""
786 # --no-ext-diff is broken in some versions of Git, so try to work around
787 # this by overriding the environment (but there is still a problem if the
788 # git config key "diff.external" is used).
789 env = GetNoGitPagerEnv()
790 if 'GIT_EXTERNAL_DIFF' in env:
791 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000792
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000793 return subprocess2.call(
794 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
795 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000796
797
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000798class BuildbucketResponseException(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000799 pass
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000800
801
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000802class Settings(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000803 def __init__(self):
804 self.cc = None
805 self.root = None
806 self.tree_status_url = None
807 self.viewvc_url = None
808 self.updated = False
809 self.is_gerrit = None
810 self.squash_gerrit_uploads = None
811 self.gerrit_skip_ensure_authenticated = None
812 self.git_editor = None
813 self.format_full_by_default = None
814 self.is_status_commit_order_by_date = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000815
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000816 def _LazyUpdateIfNeeded(self):
817 """Updates the settings from a codereview.settings file, if available."""
818 if self.updated:
819 return
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000820
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000821 # The only value that actually changes the behavior is
822 # autoupdate = "false". Everything else means "true".
823 autoupdate = (scm.GIT.GetConfig(self.GetRoot(), 'rietveld.autoupdate',
824 '').lower())
Edward Lemur26964072020-02-19 19:18:51 +0000825
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000826 cr_settings_file = FindCodereviewSettingsFile()
827 if autoupdate != 'false' and cr_settings_file:
828 LoadCodereviewSettingsFromFile(cr_settings_file)
829 cr_settings_file.close()
Edward Lemur26964072020-02-19 19:18:51 +0000830
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000831 self.updated = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000832
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000833 @staticmethod
834 def GetRelativeRoot():
835 return scm.GIT.GetCheckoutRoot('.')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000836
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000837 def GetRoot(self):
838 if self.root is None:
839 self.root = os.path.abspath(self.GetRelativeRoot())
840 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000842 def GetTreeStatusUrl(self, error_ok=False):
843 if not self.tree_status_url:
844 self.tree_status_url = self._GetConfig('rietveld.tree-status-url')
845 if self.tree_status_url is None and not error_ok:
846 DieWithError(
847 'You must configure your tree status URL by running '
848 '"git cl config".')
849 return self.tree_status_url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000850
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000851 def GetViewVCUrl(self):
852 if not self.viewvc_url:
853 self.viewvc_url = self._GetConfig('rietveld.viewvc-url')
854 return self.viewvc_url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000855
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000856 def GetBugPrefix(self):
857 return self._GetConfig('rietveld.bug-prefix')
rmistry@google.com78948ed2015-07-08 23:09:57 +0000858
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000859 def GetRunPostUploadHook(self):
860 run_post_upload_hook = self._GetConfig('rietveld.run-post-upload-hook')
861 return run_post_upload_hook == "True"
rmistry@google.com5626a922015-02-26 14:03:30 +0000862
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000863 def GetDefaultCCList(self):
864 return self._GetConfig('rietveld.cc')
Joanna Wangc8f23e22023-01-19 21:18:10 +0000865
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000866 def GetSquashGerritUploads(self):
867 """Returns True if uploads to Gerrit should be squashed by default."""
868 if self.squash_gerrit_uploads is None:
869 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
870 if self.squash_gerrit_uploads is None:
871 # Default is squash now (http://crbug.com/611892#c23).
872 self.squash_gerrit_uploads = self._GetConfig(
873 'gerrit.squash-uploads').lower() != 'false'
874 return self.squash_gerrit_uploads
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000875
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000876 def GetSquashGerritUploadsOverride(self):
877 """Return True or False if codereview.settings should be overridden.
Edward Lesmes4de54132020-05-05 19:41:33 +0000878
879 Returns None if no override has been defined.
880 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000881 # See also http://crbug.com/611892#c23
882 result = self._GetConfig('gerrit.override-squash-uploads').lower()
883 if result == 'true':
884 return True
885 if result == 'false':
886 return False
887 return None
Edward Lesmes4de54132020-05-05 19:41:33 +0000888
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000889 def GetIsGerrit(self):
890 """Return True if gerrit.host is set."""
891 if self.is_gerrit is None:
892 self.is_gerrit = bool(self._GetConfig('gerrit.host', False))
893 return self.is_gerrit
Aleksey Khoroshilov35ef5ad2022-06-03 18:29:25 +0000894
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000895 def GetGerritSkipEnsureAuthenticated(self):
896 """Return True if EnsureAuthenticated should not be done for Gerrit
tandrii@chromium.org28253532016-04-14 13:46:56 +0000897 uploads."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000898 if self.gerrit_skip_ensure_authenticated is None:
899 self.gerrit_skip_ensure_authenticated = self._GetConfig(
900 'gerrit.skip-ensure-authenticated').lower() == 'true'
901 return self.gerrit_skip_ensure_authenticated
tandrii@chromium.org28253532016-04-14 13:46:56 +0000902
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000903 def GetGitEditor(self):
904 """Returns the editor specified in the git config, or None if none is."""
905 if self.git_editor is None:
906 # Git requires single quotes for paths with spaces. We need to
907 # replace them with double quotes for Windows to treat such paths as
908 # a single path.
909 self.git_editor = self._GetConfig('core.editor').replace('\'', '"')
910 return self.git_editor or None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000911
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000912 def GetLintRegex(self):
913 return self._GetConfig('rietveld.cpplint-regex', DEFAULT_LINT_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000914
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000915 def GetLintIgnoreRegex(self):
916 return self._GetConfig('rietveld.cpplint-ignore-regex',
917 DEFAULT_LINT_IGNORE_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000918
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000919 def GetFormatFullByDefault(self):
920 if self.format_full_by_default is None:
921 self._LazyUpdateIfNeeded()
922 result = (RunGit(
923 ['config', '--bool', 'rietveld.format-full-by-default'],
924 error_ok=True).strip())
925 self.format_full_by_default = (result == 'true')
926 return self.format_full_by_default
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000927
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000928 def IsStatusCommitOrderByDate(self):
929 if self.is_status_commit_order_by_date is None:
930 result = (RunGit(['config', '--bool', 'cl.date-order'],
931 error_ok=True).strip())
932 self.is_status_commit_order_by_date = (result == 'true')
933 return self.is_status_commit_order_by_date
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +0000934
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000935 def _GetConfig(self, key, default=''):
936 self._LazyUpdateIfNeeded()
937 return scm.GIT.GetConfig(self.GetRoot(), key, default)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938
939
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000940class _CQState(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000941 """Enum for states of CL with respect to CQ."""
942 NONE = 'none'
943 DRY_RUN = 'dry_run'
944 COMMIT = 'commit'
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000945
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000946 ALL_STATES = [NONE, DRY_RUN, COMMIT]
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000947
948
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000949class _ParsedIssueNumberArgument(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000950 def __init__(self, issue=None, patchset=None, hostname=None):
951 self.issue = issue
952 self.patchset = patchset
953 self.hostname = hostname
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000954
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000955 @property
956 def valid(self):
957 return self.issue is not None
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000958
959
Edward Lemurf38bc172019-09-03 21:02:13 +0000960def ParseIssueNumberArgument(arg):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000961 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
962 fail_result = _ParsedIssueNumberArgument()
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000963
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000964 if isinstance(arg, int):
965 return _ParsedIssueNumberArgument(issue=arg)
966 if not isinstance(arg, str):
967 return fail_result
Edward Lemur678a6842019-10-03 22:25:05 +0000968
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000969 if arg.isdigit():
970 return _ParsedIssueNumberArgument(issue=int(arg))
Aaron Gableaee6c852017-06-26 12:49:01 -0700971
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000972 url = gclient_utils.UpgradeToHttps(arg)
973 if not url.startswith('http'):
974 return fail_result
975 for gerrit_url, short_url in _KNOWN_GERRIT_TO_SHORT_URLS.items():
976 if url.startswith(short_url):
977 url = gerrit_url + url[len(short_url):]
978 break
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000979
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000980 try:
981 parsed_url = urllib.parse.urlparse(url)
982 except ValueError:
983 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +0200984
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000985 # If "https://" was automatically added, fail if `arg` looks unlikely to be
986 # a URL.
987 if not arg.startswith('http') and '.' not in parsed_url.netloc:
988 return fail_result
Alex Turner30ae6372022-01-04 02:32:52 +0000989
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000990 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
991 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
992 # Short urls like https://domain/<issue_number> can be used, but don't allow
993 # specifying the patchset (you'd 404), but we allow that here.
994 if parsed_url.path == '/':
995 part = parsed_url.fragment
996 else:
997 part = parsed_url.path
Edward Lemur678a6842019-10-03 22:25:05 +0000998
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000999 match = re.match(r'(/c(/.*/\+)?)?/(?P<issue>\d+)(/(?P<patchset>\d+)?/?)?$',
1000 part)
1001 if not match:
1002 return fail_result
Edward Lemur678a6842019-10-03 22:25:05 +00001003
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001004 issue = int(match.group('issue'))
1005 patchset = match.group('patchset')
1006 return _ParsedIssueNumberArgument(
1007 issue=issue,
1008 patchset=int(patchset) if patchset else None,
1009 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001010
1011
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001012def _create_description_from_log(args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001013 """Pulls out the commit log to use as a base for the CL description."""
1014 log_args = []
1015 if len(args) == 1 and args[0] == None:
1016 # Handle the case where None is passed as the branch.
1017 return ''
1018 if len(args) == 1 and not args[0].endswith('.'):
1019 log_args = [args[0] + '..']
1020 elif len(args) == 1 and args[0].endswith('...'):
1021 log_args = [args[0][:-1]]
1022 elif len(args) == 2:
1023 log_args = [args[0] + '..' + args[1]]
1024 else:
1025 log_args = args[:] # Hope for the best!
1026 return RunGit(['log', '--pretty=format:%B%n'] + log_args)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001027
1028
Aaron Gablea45ee112016-11-22 15:14:38 -08001029class GerritChangeNotExists(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001030 def __init__(self, issue, url):
1031 self.issue = issue
1032 self.url = url
1033 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001034
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001035 def __str__(self):
1036 return 'change %s at %s does not exist or you have no access to it' % (
1037 self.issue, self.url)
tandriic2405f52016-10-10 08:13:15 -07001038
1039
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001040_CommentSummary = collections.namedtuple(
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001041 '_CommentSummary',
1042 [
1043 'date',
1044 'message',
1045 'sender',
1046 'autogenerated',
1047 # TODO(tandrii): these two aren't known in Gerrit.
1048 'approval',
1049 'disapproval'
1050 ])
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001051
Joanna Wang6215dd02023-02-07 15:58:03 +00001052# TODO(b/265929888): Change `parent` to `pushed_commit_base`.
Joanna Wange8523912023-01-21 02:05:40 +00001053_NewUpload = collections.namedtuple('NewUpload', [
Joanna Wang40497912023-01-24 21:18:16 +00001054 'reviewers', 'ccs', 'commit_to_push', 'new_last_uploaded_commit', 'parent',
Joanna Wang7603f042023-03-01 22:17:36 +00001055 'change_desc', 'prev_patchset'
Joanna Wange8523912023-01-21 02:05:40 +00001056])
1057
1058
Daniel Chengabf48472023-08-30 15:45:13 +00001059class ChangeDescription(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001060 """Contains a parsed form of the change description."""
1061 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
1062 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
1063 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
1064 FIXED_LINE = r'^[ \t]*Fixed[ \t]*:[ \t]*(.*?)[ \t]*$'
1065 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
1066 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
1067 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
1068 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):($|[^:])'
1069 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
Daniel Chengabf48472023-08-30 15:45:13 +00001070
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001071 def __init__(self, description, bug=None, fixed=None):
1072 self._description_lines = (description or '').strip().splitlines()
1073 if bug:
1074 regexp = re.compile(self.BUG_LINE)
1075 prefix = settings.GetBugPrefix()
1076 if not any(
1077 (regexp.match(line) for line in self._description_lines)):
1078 values = list(_get_bug_line_values(prefix, bug))
1079 self.append_footer('Bug: %s' % ', '.join(values))
1080 if fixed:
1081 regexp = re.compile(self.FIXED_LINE)
1082 prefix = settings.GetBugPrefix()
1083 if not any(
1084 (regexp.match(line) for line in self._description_lines)):
1085 values = list(_get_bug_line_values(prefix, fixed))
1086 self.append_footer('Fixed: %s' % ', '.join(values))
Daniel Chengabf48472023-08-30 15:45:13 +00001087
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001088 @property # www.logilab.org/ticket/89786
1089 def description(self): # pylint: disable=method-hidden
1090 return '\n'.join(self._description_lines)
Daniel Chengabf48472023-08-30 15:45:13 +00001091
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001092 def set_description(self, desc):
1093 if isinstance(desc, str):
1094 lines = desc.splitlines()
1095 else:
1096 lines = [line.rstrip() for line in desc]
1097 while lines and not lines[0]:
1098 lines.pop(0)
1099 while lines and not lines[-1]:
1100 lines.pop(-1)
1101 self._description_lines = lines
Daniel Chengabf48472023-08-30 15:45:13 +00001102
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001103 def ensure_change_id(self, change_id):
1104 description = self.description
1105 footer_change_ids = git_footers.get_footer_change_id(description)
1106 # Make sure that the Change-Id in the description matches the given one.
1107 if footer_change_ids != [change_id]:
1108 if footer_change_ids:
1109 # Remove any existing Change-Id footers since they don't match
1110 # the expected change_id footer.
1111 description = git_footers.remove_footer(description,
1112 'Change-Id')
1113 print(
1114 'WARNING: Change-Id has been set to %s. Use `git cl issue 0` '
1115 'if you want to set a new one.')
1116 # Add the expected Change-Id footer.
1117 description = git_footers.add_footer_change_id(
1118 description, change_id)
1119 self.set_description(description)
Daniel Chengabf48472023-08-30 15:45:13 +00001120
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001121 def update_reviewers(self, reviewers):
1122 """Rewrites the R= line(s) as a single line each.
Daniel Chengabf48472023-08-30 15:45:13 +00001123
1124 Args:
1125 reviewers (list(str)) - list of additional emails to use for reviewers.
1126 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001127 if not reviewers:
1128 return
Daniel Chengabf48472023-08-30 15:45:13 +00001129
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001130 reviewers = set(reviewers)
Daniel Chengabf48472023-08-30 15:45:13 +00001131
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001132 # Get the set of R= lines and remove them from the description.
1133 regexp = re.compile(self.R_LINE)
1134 matches = [regexp.match(line) for line in self._description_lines]
1135 new_desc = [
1136 l for i, l in enumerate(self._description_lines) if not matches[i]
1137 ]
1138 self.set_description(new_desc)
Daniel Chengabf48472023-08-30 15:45:13 +00001139
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001140 # Construct new unified R= lines.
Daniel Chengabf48472023-08-30 15:45:13 +00001141
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001142 # First, update reviewers with names from the R= lines (if any).
1143 for match in matches:
1144 if not match:
1145 continue
1146 reviewers.update(cleanup_list([match.group(2).strip()]))
Daniel Chengabf48472023-08-30 15:45:13 +00001147
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001148 new_r_line = 'R=' + ', '.join(sorted(reviewers))
Daniel Chengabf48472023-08-30 15:45:13 +00001149
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001150 # Put the new lines in the description where the old first R= line was.
1151 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1152 if 0 <= line_loc < len(self._description_lines):
1153 self._description_lines.insert(line_loc, new_r_line)
1154 else:
1155 self.append_footer(new_r_line)
Daniel Chengabf48472023-08-30 15:45:13 +00001156
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001157 def set_preserve_tryjobs(self):
1158 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
1159 footers = git_footers.parse_footers(self.description)
1160 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
1161 if v.lower() == 'true':
1162 return
1163 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
Daniel Chengabf48472023-08-30 15:45:13 +00001164
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001165 def prompt(self):
1166 """Asks the user to update the description."""
1167 self.set_description([
1168 '# Enter a description of the change.',
1169 '# This will be displayed on the codereview site.',
1170 '# The first line will also be used as the subject of the review.',
1171 '#--------------------This line is 72 characters long'
1172 '--------------------',
1173 ] + self._description_lines)
1174 bug_regexp = re.compile(self.BUG_LINE)
1175 fixed_regexp = re.compile(self.FIXED_LINE)
1176 prefix = settings.GetBugPrefix()
1177 has_issue = lambda l: bug_regexp.match(l) or fixed_regexp.match(l)
Daniel Chengabf48472023-08-30 15:45:13 +00001178
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001179 if not any((has_issue(line) for line in self._description_lines)):
1180 self.append_footer('Bug: %s' % prefix)
Daniel Chengabf48472023-08-30 15:45:13 +00001181
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001182 print('Waiting for editor...')
1183 content = gclient_utils.RunEditor(self.description,
1184 True,
1185 git_editor=settings.GetGitEditor())
1186 if not content:
1187 DieWithError('Running editor failed')
1188 lines = content.splitlines()
Daniel Chengabf48472023-08-30 15:45:13 +00001189
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001190 # Strip off comments and default inserted "Bug:" line.
1191 clean_lines = [
1192 line.rstrip() for line in lines
1193 if not (line.startswith('#') or line.rstrip() == "Bug:"
1194 or line.rstrip() == "Bug: " + prefix)
1195 ]
1196 if not clean_lines:
1197 DieWithError('No CL description, aborting')
1198 self.set_description(clean_lines)
Daniel Chengabf48472023-08-30 15:45:13 +00001199
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001200 def append_footer(self, line):
1201 """Adds a footer line to the description.
Daniel Chengabf48472023-08-30 15:45:13 +00001202
1203 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
1204 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
1205 that Gerrit footers are always at the end.
1206 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001207 parsed_footer_line = git_footers.parse_footer(line)
1208 if parsed_footer_line:
1209 # Line is a gerrit footer in the form: Footer-Key: any value.
1210 # Thus, must be appended observing Gerrit footer rules.
1211 self.set_description(
1212 git_footers.add_footer(self.description,
1213 key=parsed_footer_line[0],
1214 value=parsed_footer_line[1]))
1215 return
Daniel Chengabf48472023-08-30 15:45:13 +00001216
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001217 if not self._description_lines:
1218 self._description_lines.append(line)
1219 return
Daniel Chengabf48472023-08-30 15:45:13 +00001220
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001221 top_lines, gerrit_footers, _ = git_footers.split_footers(
1222 self.description)
1223 if gerrit_footers:
1224 # git_footers.split_footers ensures that there is an empty line
1225 # before actual (gerrit) footers, if any. We have to keep it that
1226 # way.
1227 assert top_lines and top_lines[-1] == ''
1228 top_lines, separator = top_lines[:-1], top_lines[-1:]
1229 else:
1230 separator = [
1231 ] # No need for separator if there are no gerrit_footers.
Daniel Chengabf48472023-08-30 15:45:13 +00001232
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001233 prev_line = top_lines[-1] if top_lines else ''
1234 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line)
1235 or not presubmit_support.Change.TAG_LINE_RE.match(line)):
1236 top_lines.append('')
1237 top_lines.append(line)
1238 self._description_lines = top_lines + separator + gerrit_footers
Daniel Chengabf48472023-08-30 15:45:13 +00001239
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001240 def get_reviewers(self, tbr_only=False):
1241 """Retrieves the list of reviewers."""
1242 matches = [
1243 re.match(self.R_LINE, line) for line in self._description_lines
1244 ]
1245 reviewers = [
1246 match.group(2).strip() for match in matches
1247 if match and (not tbr_only or match.group(1).upper() == 'TBR')
1248 ]
1249 return cleanup_list(reviewers)
Daniel Chengabf48472023-08-30 15:45:13 +00001250
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001251 def get_cced(self):
1252 """Retrieves the list of reviewers."""
1253 matches = [
1254 re.match(self.CC_LINE, line) for line in self._description_lines
1255 ]
1256 cced = [match.group(2).strip() for match in matches if match]
1257 return cleanup_list(cced)
Daniel Chengabf48472023-08-30 15:45:13 +00001258
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001259 def get_hash_tags(self):
1260 """Extracts and sanitizes a list of Gerrit hashtags."""
1261 subject = (self._description_lines or ('', ))[0]
1262 subject = re.sub(self.STRIP_HASH_TAG_PREFIX,
1263 '',
1264 subject,
1265 flags=re.IGNORECASE)
Daniel Chengabf48472023-08-30 15:45:13 +00001266
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001267 tags = []
1268 start = 0
1269 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
1270 while True:
1271 m = bracket_exp.match(subject, start)
1272 if not m:
1273 break
1274 tags.append(self.sanitize_hash_tag(m.group(1)))
1275 start = m.end()
Daniel Chengabf48472023-08-30 15:45:13 +00001276
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001277 if not tags:
1278 # Try "Tag: " prefix.
1279 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
1280 if m:
1281 tags.append(self.sanitize_hash_tag(m.group(1)))
1282 return tags
Daniel Chengabf48472023-08-30 15:45:13 +00001283
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001284 @classmethod
1285 def sanitize_hash_tag(cls, tag):
1286 """Returns a sanitized Gerrit hash tag.
Daniel Chengabf48472023-08-30 15:45:13 +00001287
1288 A sanitized hashtag can be used as a git push refspec parameter value.
1289 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001290 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
Daniel Chengabf48472023-08-30 15:45:13 +00001291
1292
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293class Changelist(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001294 """Changelist works with one changelist in local branch.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001295
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001296 Notes:
1297 * Not safe for concurrent multi-{thread,process} use.
1298 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001299 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001300 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001301 def __init__(self,
1302 branchref=None,
1303 issue=None,
1304 codereview_host=None,
1305 commit_date=None):
1306 """Create a new ChangeList instance.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001307
Edward Lemurf38bc172019-09-03 21:02:13 +00001308 **kwargs will be passed directly to Gerrit implementation.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001309 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001310 # Poke settings so we get the "configure your server" message if
1311 # necessary.
1312 global settings
1313 if not settings:
1314 # Happens when git_cl.py is used as a utility library.
1315 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001316
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001317 self.branchref = branchref
1318 if self.branchref:
1319 assert branchref.startswith('refs/heads/')
1320 self.branch = scm.GIT.ShortBranchName(self.branchref)
1321 else:
1322 self.branch = None
1323 self.commit_date = commit_date
1324 self.upstream_branch = None
1325 self.lookedup_issue = False
1326 self.issue = issue or None
1327 self.description = None
1328 self.lookedup_patchset = False
1329 self.patchset = None
1330 self.cc = None
1331 self.more_cc = []
1332 self._remote = None
1333 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001334
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001335 # Lazily cached values.
1336 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1337 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1338 self._owners_client = None
1339 # Map from change number (issue) to its detail cache.
1340 self._detail_cache = {}
Edward Lemur125d60a2019-09-13 18:25:41 +00001341
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001342 if codereview_host is not None:
1343 assert not codereview_host.startswith('https://'), codereview_host
1344 self._gerrit_host = codereview_host
1345 self._gerrit_server = 'https://%s' % codereview_host
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001346
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001347 @property
1348 def owners_client(self):
1349 if self._owners_client is None:
1350 remote, remote_branch = self.GetRemoteBranch()
1351 branch = GetTargetRef(remote, remote_branch, None)
1352 self._owners_client = owners_client.GetCodeOwnersClient(
1353 host=self.GetGerritHost(),
1354 project=self.GetGerritProject(),
1355 branch=branch)
1356 return self._owners_client
Edward Lesmese1576912021-02-16 21:53:34 +00001357
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001358 def GetCCList(self):
1359 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001360
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001361 The return value is a string suitable for passing to git cl with the --cc
1362 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001363 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001364 if self.cc is None:
1365 base_cc = settings.GetDefaultCCList()
1366 more_cc = ','.join(self.more_cc)
1367 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1368 return self.cc
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001369
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001370 def ExtendCC(self, more_cc):
1371 """Extends the list of users to cc on this CL based on the changed files."""
1372 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001373
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001374 def GetCommitDate(self):
1375 """Returns the commit date as provided in the constructor"""
1376 return self.commit_date
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00001377
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001378 def GetBranch(self):
1379 """Returns the short branch name, e.g. 'main'."""
1380 if not self.branch:
1381 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
1382 if not branchref:
1383 return None
1384 self.branchref = branchref
1385 self.branch = scm.GIT.ShortBranchName(self.branchref)
1386 return self.branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001387
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001388 def GetBranchRef(self):
1389 """Returns the full branch name, e.g. 'refs/heads/main'."""
1390 self.GetBranch() # Poke the lazy loader.
1391 return self.branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001392
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001393 def _GitGetBranchConfigValue(self, key, default=None):
1394 return scm.GIT.GetBranchConfig(settings.GetRoot(), self.GetBranch(),
1395 key, default)
tandrii5d48c322016-08-18 16:19:37 -07001396
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001397 def _GitSetBranchConfigValue(self, key, value):
1398 action = 'set %s to %r' % (key, value)
1399 if not value:
1400 action = 'unset %s' % key
1401 assert self.GetBranch(), 'a branch is needed to ' + action
1402 return scm.GIT.SetBranchConfig(settings.GetRoot(), self.GetBranch(),
1403 key, value)
tandrii5d48c322016-08-18 16:19:37 -07001404
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001405 @staticmethod
1406 def FetchUpstreamTuple(branch):
1407 """Returns a tuple containing remote and remote ref,
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001408 e.g. 'origin', 'refs/heads/main'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001410 remote, upstream_branch = scm.GIT.FetchUpstreamTuple(
1411 settings.GetRoot(), branch)
1412 if not remote or not upstream_branch:
1413 DieWithError(
1414 'Unable to determine default branch to diff against.\n'
1415 'Verify this branch is set up to track another \n'
1416 '(via the --track argument to "git checkout -b ..."). \n'
1417 'or pass complete "git diff"-style arguments if supported, like\n'
1418 ' git cl upload origin/main\n')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001420 return remote, upstream_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001421
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001422 def GetCommonAncestorWithUpstream(self):
1423 upstream_branch = self.GetUpstreamBranch()
1424 if not scm.GIT.IsValidRevision(settings.GetRoot(), upstream_branch):
1425 DieWithError(
Joanna Wangd4dfff02023-09-13 17:44:31 +00001426 'The current branch (%s) has an upstream (%s) that does not exist '
1427 'anymore.\nPlease fix it and try again.' %
1428 (self.GetBranch(), upstream_branch))
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001429 return git_common.get_or_create_merge_base(self.GetBranch(),
1430 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001431
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001432 def GetUpstreamBranch(self):
1433 if self.upstream_branch is None:
1434 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1435 if remote != '.':
1436 upstream_branch = upstream_branch.replace(
1437 'refs/heads/', 'refs/remotes/%s/' % remote)
1438 upstream_branch = upstream_branch.replace(
1439 'refs/branch-heads/', 'refs/remotes/branch-heads/')
1440 self.upstream_branch = upstream_branch
1441 return self.upstream_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001442
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001443 def GetRemoteBranch(self):
1444 if not self._remote:
1445 remote, branch = None, self.GetBranch()
1446 seen_branches = set()
1447 while branch not in seen_branches:
1448 seen_branches.add(branch)
1449 remote, branch = self.FetchUpstreamTuple(branch)
1450 branch = scm.GIT.ShortBranchName(branch)
1451 if remote != '.' or branch.startswith('refs/remotes'):
1452 break
1453 else:
1454 remotes = RunGit(['remote'], error_ok=True).split()
1455 if len(remotes) == 1:
1456 remote, = remotes
1457 elif 'origin' in remotes:
1458 remote = 'origin'
1459 logging.warning(
1460 'Could not determine which remote this change is '
1461 'associated with, so defaulting to "%s".' %
1462 self._remote)
1463 else:
1464 logging.warning(
1465 'Could not determine which remote this change is '
1466 'associated with.')
1467 branch = 'HEAD'
1468 if branch.startswith('refs/remotes'):
1469 self._remote = (remote, branch)
1470 elif branch.startswith('refs/branch-heads/'):
1471 self._remote = (remote, branch.replace('refs/',
1472 'refs/remotes/'))
1473 else:
1474 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
1475 return self._remote
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001476
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001477 def GetRemoteUrl(self) -> Optional[str]:
1478 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001479
1480 Returns None if there is no remote.
1481 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001482 is_cached, value = self._cached_remote_url
1483 if is_cached:
1484 return value
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001485
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001486 remote, _ = self.GetRemoteBranch()
1487 url = scm.GIT.GetConfig(settings.GetRoot(), 'remote.%s.url' % remote,
1488 '')
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001489
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001490 # Check if the remote url can be parsed as an URL.
1491 host = urllib.parse.urlparse(url).netloc
1492 if host:
1493 self._cached_remote_url = (True, url)
1494 return url
Edward Lemur298f2cf2019-02-22 21:40:39 +00001495
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001496 # If it cannot be parsed as an url, assume it is a local directory,
1497 # probably a git cache.
1498 logging.warning(
1499 '"%s" doesn\'t appear to point to a git host. '
1500 'Interpreting it as a local directory.', url)
1501 if not os.path.isdir(url):
1502 logging.error(
1503 'Remote "%(remote)s" for branch "%(branch)s" points to "%(url)s", '
1504 'but it doesn\'t exist.', {
1505 'remote': remote,
1506 'branch': self.GetBranch(),
1507 'url': url
1508 })
1509 return None
Edward Lemur298f2cf2019-02-22 21:40:39 +00001510
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001511 cache_path = url
1512 url = scm.GIT.GetConfig(url, 'remote.%s.url' % remote, '')
Edward Lemur298f2cf2019-02-22 21:40:39 +00001513
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001514 host = urllib.parse.urlparse(url).netloc
1515 if not host:
1516 logging.error(
1517 'Remote "%(remote)s" for branch "%(branch)s" points to '
1518 '"%(cache_path)s", but it is misconfigured.\n'
1519 '"%(cache_path)s" must be a git repo and must have a remote named '
1520 '"%(remote)s" pointing to the git host.', {
1521 'remote': remote,
1522 'cache_path': cache_path,
1523 'branch': self.GetBranch()
1524 })
1525 return None
Edward Lemur298f2cf2019-02-22 21:40:39 +00001526
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001527 self._cached_remote_url = (True, url)
1528 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001529
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001530 def GetIssue(self):
1531 """Returns the issue number as a int or None if not set."""
1532 if self.issue is None and not self.lookedup_issue:
1533 if self.GetBranch():
1534 self.issue = self._GitGetBranchConfigValue(ISSUE_CONFIG_KEY)
1535 if self.issue is not None:
1536 self.issue = int(self.issue)
1537 self.lookedup_issue = True
1538 return self.issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001539
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001540 def GetIssueURL(self, short=False):
1541 """Get the URL for a particular issue."""
1542 issue = self.GetIssue()
1543 if not issue:
1544 return None
1545 server = self.GetCodereviewServer()
1546 if short:
1547 server = _KNOWN_GERRIT_TO_SHORT_URLS.get(server, server)
1548 return '%s/%s' % (server, issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001549
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001550 def FetchDescription(self, pretty=False):
1551 assert self.GetIssue(), 'issue is required to query Gerrit'
Edward Lemur6c6827c2020-02-06 21:15:18 +00001552
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001553 if self.description is None:
1554 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
1555 current_rev = data['current_revision']
1556 self.description = data['revisions'][current_rev]['commit'][
1557 'message']
Edward Lemur6c6827c2020-02-06 21:15:18 +00001558
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001559 if not pretty:
1560 return self.description
Edward Lemur6c6827c2020-02-06 21:15:18 +00001561
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001562 # Set width to 72 columns + 2 space indent.
1563 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
1564 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1565 lines = self.description.splitlines()
1566 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001567
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001568 def GetPatchset(self):
1569 """Returns the patchset number as a int or None if not set."""
1570 if self.patchset is None and not self.lookedup_patchset:
1571 if self.GetBranch():
1572 self.patchset = self._GitGetBranchConfigValue(
1573 PATCHSET_CONFIG_KEY)
1574 if self.patchset is not None:
1575 self.patchset = int(self.patchset)
1576 self.lookedup_patchset = True
1577 return self.patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001578
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001579 def GetAuthor(self):
1580 return scm.GIT.GetConfig(settings.GetRoot(), 'user.email')
Edward Lemur9aa1a962020-02-25 00:58:38 +00001581
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001582 def SetPatchset(self, patchset):
1583 """Set this branch's patchset. If patchset=0, clears the patchset."""
1584 assert self.GetBranch()
1585 if not patchset:
1586 self.patchset = None
1587 else:
1588 self.patchset = int(patchset)
1589 self._GitSetBranchConfigValue(PATCHSET_CONFIG_KEY, str(self.patchset))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001590
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001591 def SetIssue(self, issue=None):
1592 """Set this branch's issue. If issue isn't given, clears the issue."""
1593 assert self.GetBranch()
1594 if issue:
1595 issue = int(issue)
1596 self._GitSetBranchConfigValue(ISSUE_CONFIG_KEY, str(issue))
1597 self.issue = issue
1598 codereview_server = self.GetCodereviewServer()
1599 if codereview_server:
1600 self._GitSetBranchConfigValue(CODEREVIEW_SERVER_CONFIG_KEY,
1601 codereview_server)
1602 else:
1603 # Reset all of these just to be clean.
1604 reset_suffixes = [
1605 LAST_UPLOAD_HASH_CONFIG_KEY,
1606 ISSUE_CONFIG_KEY,
1607 PATCHSET_CONFIG_KEY,
1608 CODEREVIEW_SERVER_CONFIG_KEY,
1609 GERRIT_SQUASH_HASH_CONFIG_KEY,
1610 ]
1611 for prop in reset_suffixes:
1612 try:
1613 self._GitSetBranchConfigValue(prop, None)
1614 except subprocess2.CalledProcessError:
1615 pass
1616 msg = RunGit(['log', '-1', '--format=%B']).strip()
1617 if msg and git_footers.get_footer_change_id(msg):
1618 print(
1619 'WARNING: The change patched into this branch has a Change-Id. '
1620 'Removing it.')
1621 RunGit([
1622 'commit', '--amend', '-m',
1623 git_footers.remove_footer(msg, 'Change-Id')
1624 ])
1625 self.lookedup_issue = True
1626 self.issue = None
1627 self.patchset = None
1628
1629 def GetAffectedFiles(self,
1630 upstream: str,
1631 end_commit: Optional[str] = None) -> Sequence[str]:
1632 """Returns the list of affected files for the given commit range."""
Edward Lemur85153282020-02-14 22:06:29 +00001633 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001634 return [
1635 f for _, f in scm.GIT.CaptureStatus(
1636 settings.GetRoot(), upstream, end_commit=end_commit)
1637 ]
Edward Lemur85153282020-02-14 22:06:29 +00001638 except subprocess2.CalledProcessError:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001639 DieWithError(
1640 ('\nFailed to diff against upstream branch %s\n\n'
1641 'This branch probably doesn\'t exist anymore. To reset the\n'
1642 'tracking branch, please run\n'
1643 ' git branch --set-upstream-to origin/main %s\n'
1644 'or replace origin/main with the relevant branch') %
1645 (upstream, self.GetBranch()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001646
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001647 def UpdateDescription(self, description, force=False):
1648 assert self.GetIssue(), 'issue is required to update description'
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001649
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001650 if gerrit_util.HasPendingChangeEdit(self.GetGerritHost(),
1651 self._GerritChangeIdentifier()):
1652 if not force:
1653 confirm_or_exit(
1654 'The description cannot be modified while the issue has a pending '
1655 'unpublished edit. Either publish the edit in the Gerrit web UI '
1656 'or delete it.\n\n',
1657 action='delete the unpublished edit')
Edward Lemur6c6827c2020-02-06 21:15:18 +00001658
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001659 gerrit_util.DeletePendingChangeEdit(self.GetGerritHost(),
1660 self._GerritChangeIdentifier())
1661 gerrit_util.SetCommitMessage(self.GetGerritHost(),
1662 self._GerritChangeIdentifier(),
1663 description,
1664 notify='NONE')
Edward Lemur6c6827c2020-02-06 21:15:18 +00001665
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001666 self.description = description
Edward Lemur6c6827c2020-02-06 21:15:18 +00001667
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001668 def _GetCommonPresubmitArgs(self, verbose, upstream):
1669 args = [
1670 '--root',
1671 settings.GetRoot(),
1672 '--upstream',
1673 upstream,
1674 ]
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001675
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001676 args.extend(['--verbose'] * verbose)
Edward Lemur227d5102020-02-25 23:45:35 +00001677
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001678 remote, remote_branch = self.GetRemoteBranch()
1679 target_ref = GetTargetRef(remote, remote_branch, None)
1680 if settings.GetIsGerrit():
1681 args.extend(['--gerrit_url', self.GetCodereviewServer()])
1682 args.extend(['--gerrit_project', self.GetGerritProject()])
1683 args.extend(['--gerrit_branch', target_ref])
Edward Lemur227d5102020-02-25 23:45:35 +00001684
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001685 author = self.GetAuthor()
1686 issue = self.GetIssue()
1687 patchset = self.GetPatchset()
1688 if author:
1689 args.extend(['--author', author])
1690 if issue:
1691 args.extend(['--issue', str(issue)])
1692 if patchset:
1693 args.extend(['--patchset', str(patchset)])
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001694
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001695 return args
Edward Lemur227d5102020-02-25 23:45:35 +00001696
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001697 def RunHook(self,
1698 committing,
1699 may_prompt,
1700 verbose,
1701 parallel,
1702 upstream,
1703 description,
1704 all_files,
1705 files=None,
1706 resultdb=False,
1707 realm=None):
1708 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1709 args = self._GetCommonPresubmitArgs(verbose, upstream)
1710 args.append('--commit' if committing else '--upload')
1711 if may_prompt:
1712 args.append('--may_prompt')
1713 if parallel:
1714 args.append('--parallel')
1715 if all_files:
1716 args.append('--all_files')
1717 if files:
1718 args.extend(files.split(';'))
1719 args.append('--source_controlled_only')
1720 if files or all_files:
1721 args.append('--no_diffs')
Edward Lemur75526302020-02-27 22:31:05 +00001722
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001723 if resultdb and not realm:
1724 # TODO (crbug.com/1113463): store realm somewhere and look it up so
1725 # it is not required to pass the realm flag
1726 print(
1727 'Note: ResultDB reporting will NOT be performed because --realm'
1728 ' was not specified. To enable ResultDB, please run the command'
1729 ' again with the --realm argument to specify the LUCI realm.')
Edward Lemur227d5102020-02-25 23:45:35 +00001730
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001731 return self._RunPresubmit(args,
1732 description,
1733 resultdb=resultdb,
1734 realm=realm)
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001735
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001736 def _RunPresubmit(self,
1737 args: Sequence[str],
1738 description: str,
1739 resultdb: bool = False,
1740 realm: Optional[str] = None) -> Mapping[str, Any]:
1741 args = list(args)
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001742
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001743 with gclient_utils.temporary_file() as description_file:
1744 with gclient_utils.temporary_file() as json_output:
1745 gclient_utils.FileWrite(description_file, description)
1746 args.extend(['--json_output', json_output])
1747 args.extend(['--description_file', description_file])
1748 start = time_time()
1749 cmd = ['vpython3', PRESUBMIT_SUPPORT] + args
1750 if resultdb and realm:
1751 cmd = ['rdb', 'stream', '-new', '-realm', realm, '--'] + cmd
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001752
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001753 p = subprocess2.Popen(cmd)
1754 exit_code = p.wait()
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001755
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001756 metrics.collector.add_repeated(
1757 'sub_commands', {
1758 'command': 'presubmit',
1759 'execution_time': time_time() - start,
1760 'exit_code': exit_code,
1761 })
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001762
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001763 if exit_code:
1764 sys.exit(exit_code)
Edward Lemur227d5102020-02-25 23:45:35 +00001765
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001766 json_results = gclient_utils.FileRead(json_output)
1767 return json.loads(json_results)
Edward Lemur227d5102020-02-25 23:45:35 +00001768
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001769 def RunPostUploadHook(self, verbose, upstream, description):
1770 args = self._GetCommonPresubmitArgs(verbose, upstream)
1771 args.append('--post_upload')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001772
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001773 with gclient_utils.temporary_file() as description_file:
1774 gclient_utils.FileWrite(description_file, description)
1775 args.extend(['--description_file', description_file])
1776 subprocess2.Popen(['vpython3', PRESUBMIT_SUPPORT] + args).wait()
Edward Lemur75526302020-02-27 22:31:05 +00001777
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001778 def _GetDescriptionForUpload(self, options: optparse.Values,
1779 git_diff_args: Sequence[str],
1780 files: Sequence[str]) -> ChangeDescription:
1781 """Get description message for upload."""
1782 if self.GetIssue():
1783 description = self.FetchDescription()
1784 elif options.message:
1785 description = options.message
1786 else:
1787 description = _create_description_from_log(git_diff_args)
1788 if options.title and options.squash:
1789 description = options.title + '\n\n' + description
Edward Lemur75526302020-02-27 22:31:05 +00001790
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001791 bug = options.bug
1792 fixed = options.fixed
1793 if not self.GetIssue():
1794 # Extract bug number from branch name, but only if issue is being
1795 # created. It must start with bug or fix, followed by _ or - and
1796 # number. Optionally, it may contain _ or - after number with
1797 # arbitrary text. Examples: bug-123 bug_123 fix-123
1798 # fix-123-some-description
1799 branch = self.GetBranch()
1800 if branch is not None:
1801 match = re.match(
1802 r'^(?P<type>bug|fix(?:e[sd])?)[_-]?(?P<bugnum>\d+)([-_]|$)',
1803 branch)
1804 if not bug and not fixed and match:
1805 if match.group('type') == 'bug':
1806 bug = match.group('bugnum')
1807 else:
1808 fixed = match.group('bugnum')
Edward Lemur5a644f82020-03-18 16:44:57 +00001809
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001810 change_description = ChangeDescription(description, bug, fixed)
Edward Lemur5a644f82020-03-18 16:44:57 +00001811
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001812 # Fill gaps in OWNERS coverage to reviewers if requested.
1813 if options.add_owners_to:
1814 assert options.add_owners_to in ('R'), options.add_owners_to
1815 status = self.owners_client.GetFilesApprovalStatus(
1816 files, [], options.reviewers)
1817 missing_files = [
1818 f for f in files
1819 if status[f] == self._owners_client.INSUFFICIENT_REVIEWERS
1820 ]
1821 owners = self.owners_client.SuggestOwners(
1822 missing_files, exclude=[self.GetAuthor()])
1823 assert isinstance(options.reviewers, list), options.reviewers
1824 options.reviewers.extend(owners)
Edward Lemur5a644f82020-03-18 16:44:57 +00001825
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001826 # Set the reviewer list now so that presubmit checks can access it.
1827 if options.reviewers:
1828 change_description.update_reviewers(options.reviewers)
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001829
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001830 return change_description
Edward Lemur5a644f82020-03-18 16:44:57 +00001831
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001832 def _GetTitleForUpload(self, options, multi_change_upload=False):
1833 # type: (optparse.Values, Optional[bool]) -> str
Edward Lemur5a644f82020-03-18 16:44:57 +00001834
Eli Trexler86093452023-09-26 22:33:02 +00001835 # Getting titles for multipl commits is not supported so we return the
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001836 # default.
1837 if not options.squash or multi_change_upload or options.title:
1838 return options.title
Joanna Wanga1abbed2023-01-24 01:41:05 +00001839
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001840 # On first upload, patchset title is always this string, while
1841 # options.title gets converted to first line of message.
1842 if not self.GetIssue():
1843 return 'Initial upload'
Edward Lemur5a644f82020-03-18 16:44:57 +00001844
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001845 # When uploading subsequent patchsets, options.message is taken as the
1846 # title if options.title is not provided.
1847 if options.message:
1848 return options.message.strip()
Edward Lemur5a644f82020-03-18 16:44:57 +00001849
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001850 # Use the subject of the last commit as title by default.
1851 title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
1852 if options.force or options.skip_title:
1853 return title
1854 user_title = gclient_utils.AskForData('Title for patchset [%s]: ' %
1855 title)
Edward Lemur5a644f82020-03-18 16:44:57 +00001856
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001857 # Use the default title if the user confirms the default with a 'y'.
1858 if user_title.lower() == 'y':
1859 return title
1860 return user_title or title
mlcui3da91712021-05-05 10:00:30 +00001861
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001862 def _GetRefSpecOptions(self,
1863 options: optparse.Values,
1864 change_desc: ChangeDescription,
1865 multi_change_upload: bool = False,
1866 dogfood_path: bool = False) -> List[str]:
1867 # Extra options that can be specified at push time. Doc:
1868 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
1869 refspec_opts = []
Edward Lemur5a644f82020-03-18 16:44:57 +00001870
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001871 # By default, new changes are started in WIP mode, and subsequent
1872 # patchsets don't send email. At any time, passing --send-mail or
1873 # --send-email will mark the change ready and send email for that
1874 # particular patch.
1875 if options.send_mail:
1876 refspec_opts.append('ready')
1877 refspec_opts.append('notify=ALL')
1878 elif (not self.GetIssue() and options.squash and not dogfood_path):
1879 refspec_opts.append('wip')
Joanna Wanga1abbed2023-01-24 01:41:05 +00001880
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001881 # TODO(tandrii): options.message should be posted as a comment if
1882 # --send-mail or --send-email is set on non-initial upload as Rietveld
1883 # used to do it.
Joanna Wanga1abbed2023-01-24 01:41:05 +00001884
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001885 # Set options.title in case user was prompted in _GetTitleForUpload and
1886 # _CMDUploadChange needs to be called again.
1887 options.title = self._GetTitleForUpload(
1888 options, multi_change_upload=multi_change_upload)
Joanna Wanga1abbed2023-01-24 01:41:05 +00001889
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001890 if options.title:
1891 # Punctuation and whitespace in |title| must be percent-encoded.
1892 refspec_opts.append(
1893 'm=' + gerrit_util.PercentEncodeForGitRef(options.title))
Joanna Wanga1abbed2023-01-24 01:41:05 +00001894
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001895 if options.private:
1896 refspec_opts.append('private')
Joanna Wanga1abbed2023-01-24 01:41:05 +00001897
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001898 if options.topic:
1899 # Documentation on Gerrit topics is here:
1900 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
1901 refspec_opts.append('topic=%s' % options.topic)
Joanna Wanga1abbed2023-01-24 01:41:05 +00001902
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001903 if options.enable_auto_submit:
1904 refspec_opts.append('l=Auto-Submit+1')
1905 if options.set_bot_commit:
1906 refspec_opts.append('l=Bot-Commit+1')
1907 if options.use_commit_queue:
1908 refspec_opts.append('l=Commit-Queue+2')
1909 elif options.cq_dry_run:
1910 refspec_opts.append('l=Commit-Queue+1')
Joanna Wanga1abbed2023-01-24 01:41:05 +00001911
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001912 if change_desc.get_reviewers(tbr_only=True):
1913 score = gerrit_util.GetCodeReviewTbrScore(self.GetGerritHost(),
1914 self.GetGerritProject())
1915 refspec_opts.append('l=Code-Review+%s' % score)
Joanna Wanga1abbed2023-01-24 01:41:05 +00001916
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001917 # Gerrit sorts hashtags, so order is not important.
1918 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
1919 # We check GetIssue because we only add hashtags from the
1920 # description on the first upload.
1921 # TODO(b/265929888): When we fully launch the new path:
1922 # 1) remove fetching hashtags from description alltogether
1923 # 2) Or use descrtiption hashtags for:
1924 # `not (self.GetIssue() and multi_change_upload)`
1925 # 3) Or enabled change description tags for multi and single changes
1926 # by adding them post `git push`.
1927 if not (self.GetIssue() and dogfood_path):
1928 hashtags.update(change_desc.get_hash_tags())
1929 refspec_opts.extend(['hashtag=%s' % t for t in hashtags])
Joanna Wanga1abbed2023-01-24 01:41:05 +00001930
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001931 # Note: Reviewers, and ccs are handled individually for each
1932 # branch/change.
1933 return refspec_opts
Joanna Wang40497912023-01-24 21:18:16 +00001934
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001935 def PrepareSquashedCommit(self,
1936 options: optparse.Values,
1937 parent: str,
1938 orig_parent: str,
1939 end_commit: Optional[str] = None) -> _NewUpload:
1940 """Create a squashed commit to upload.
Joanna Wang05b60342023-03-29 20:25:57 +00001941
1942
1943 Args:
1944 parent: The commit to use as the parent for the new squashed.
1945 orig_parent: The commit that is an actual ancestor of `end_commit`. It
1946 is part of the same original tree as end_commit, which does not
1947 contain squashed commits. This is used to create the change
1948 description for the new squashed commit with:
1949 `git log orig_parent..end_commit`.
1950 end_commit: The commit to use as the end of the new squashed commit.
1951 """
Joanna Wangb88a4342023-01-24 01:28:22 +00001952
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001953 if end_commit is None:
1954 end_commit = RunGit(['rev-parse', self.branchref]).strip()
Joanna Wangb88a4342023-01-24 01:28:22 +00001955
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001956 reviewers, ccs, change_desc = self._PrepareChange(
1957 options, orig_parent, end_commit)
1958 latest_tree = RunGit(['rev-parse', end_commit + ':']).strip()
1959 with gclient_utils.temporary_file() as desc_tempfile:
1960 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
1961 commit_to_push = RunGit(
1962 ['commit-tree', latest_tree, '-p', parent, '-F',
1963 desc_tempfile]).strip()
Joanna Wangb88a4342023-01-24 01:28:22 +00001964
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001965 # Gerrit may or may not update fast enough to return the correct
1966 # patchset number after we push. Get the pre-upload patchset and
1967 # increment later.
1968 prev_patchset = self.GetMostRecentPatchset(update=False) or 0
1969 return _NewUpload(reviewers, ccs, commit_to_push, end_commit, parent,
1970 change_desc, prev_patchset)
Joanna Wangb88a4342023-01-24 01:28:22 +00001971
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001972 def PrepareCherryPickSquashedCommit(self, options: optparse.Values,
1973 parent: str) -> _NewUpload:
1974 """Create a commit cherry-picked on parent to push."""
Joanna Wange8523912023-01-21 02:05:40 +00001975
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001976 # The `parent` is what we will cherry-pick on top of.
1977 # The `cherry_pick_base` is the beginning range of what
1978 # we are cherry-picking.
1979 cherry_pick_base = self.GetCommonAncestorWithUpstream()
1980 reviewers, ccs, change_desc = self._PrepareChange(
1981 options, cherry_pick_base, self.branchref)
Joanna Wange8523912023-01-21 02:05:40 +00001982
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001983 new_upload_hash = RunGit(['rev-parse', self.branchref]).strip()
1984 latest_tree = RunGit(['rev-parse', self.branchref + ':']).strip()
1985 with gclient_utils.temporary_file() as desc_tempfile:
1986 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
1987 commit_to_cp = RunGit([
1988 'commit-tree', latest_tree, '-p', cherry_pick_base, '-F',
1989 desc_tempfile
1990 ]).strip()
Joanna Wange8523912023-01-21 02:05:40 +00001991
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001992 RunGit(['checkout', '-q', parent])
1993 ret, _out = RunGitWithCode(['cherry-pick', commit_to_cp])
1994 if ret:
1995 RunGit(['cherry-pick', '--abort'])
1996 RunGit(['checkout', '-q', self.branch])
1997 DieWithError('Could not cleanly cherry-pick')
Joanna Wange8523912023-01-21 02:05:40 +00001998
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001999 commit_to_push = RunGit(['rev-parse', 'HEAD']).strip()
2000 RunGit(['checkout', '-q', self.branch])
Joanna Wange8523912023-01-21 02:05:40 +00002001
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002002 # Gerrit may or may not update fast enough to return the correct
2003 # patchset number after we push. Get the pre-upload patchset and
2004 # increment later.
2005 prev_patchset = self.GetMostRecentPatchset(update=False) or 0
2006 return _NewUpload(reviewers, ccs, commit_to_push, new_upload_hash,
2007 cherry_pick_base, change_desc, prev_patchset)
Joanna Wange8523912023-01-21 02:05:40 +00002008
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002009 def _PrepareChange(
2010 self, options: optparse.Values, parent: str, end_commit: str
2011 ) -> Tuple[Sequence[str], Sequence[str], ChangeDescription]:
2012 """Prepares the change to be uploaded."""
2013 self.EnsureCanUploadPatchset(options.force)
Joanna Wangb46232e2023-01-21 01:58:46 +00002014
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002015 files = self.GetAffectedFiles(parent, end_commit=end_commit)
2016 change_desc = self._GetDescriptionForUpload(options,
2017 [parent, end_commit], files)
Joanna Wangb46232e2023-01-21 01:58:46 +00002018
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002019 watchlist = watchlists.Watchlists(settings.GetRoot())
2020 self.ExtendCC(watchlist.GetWatchersForPaths(files))
2021 if not options.bypass_hooks:
2022 hook_results = self.RunHook(committing=False,
2023 may_prompt=not options.force,
2024 verbose=options.verbose,
2025 parallel=options.parallel,
2026 upstream=parent,
2027 description=change_desc.description,
2028 all_files=False)
2029 self.ExtendCC(hook_results['more_cc'])
Joanna Wangb46232e2023-01-21 01:58:46 +00002030
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002031 # Update the change description and ensure we have a Change Id.
2032 if self.GetIssue():
2033 if options.edit_description:
2034 change_desc.prompt()
2035 change_detail = self._GetChangeDetail(['CURRENT_REVISION'])
2036 change_id = change_detail['change_id']
2037 change_desc.ensure_change_id(change_id)
Joanna Wangb46232e2023-01-21 01:58:46 +00002038
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002039 else: # No change issue. First time uploading
2040 if not options.force and not options.message_file:
2041 change_desc.prompt()
Joanna Wangb46232e2023-01-21 01:58:46 +00002042
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002043 # Check if user added a change_id in the descripiton.
2044 change_ids = git_footers.get_footer_change_id(
2045 change_desc.description)
2046 if len(change_ids) == 1:
2047 change_id = change_ids[0]
2048 else:
2049 change_id = GenerateGerritChangeId(change_desc.description)
2050 change_desc.ensure_change_id(change_id)
Joanna Wangb46232e2023-01-21 01:58:46 +00002051
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002052 if options.preserve_tryjobs:
2053 change_desc.set_preserve_tryjobs()
Joanna Wangb46232e2023-01-21 01:58:46 +00002054
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002055 SaveDescriptionBackup(change_desc)
Joanna Wangb46232e2023-01-21 01:58:46 +00002056
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002057 # Add ccs
2058 ccs = []
2059 # Add default, watchlist, presubmit ccs if this is the initial upload
2060 # and CL is not private and auto-ccing has not been disabled.
2061 if not options.private and not options.no_autocc and not self.GetIssue(
2062 ):
2063 ccs = self.GetCCList().split(',')
2064 if len(ccs) > 100:
2065 lsc = (
2066 'https://chromium.googlesource.com/chromium/src/+/HEAD/docs/'
2067 'process/lsc/lsc_workflow.md')
2068 print('WARNING: This will auto-CC %s users.' % len(ccs))
2069 print('LSC may be more appropriate: %s' % lsc)
2070 print(
2071 'You can also use the --no-autocc flag to disable auto-CC.')
2072 confirm_or_exit(action='continue')
Joanna Wangb46232e2023-01-21 01:58:46 +00002073
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002074 # Add ccs from the --cc flag.
2075 if options.cc:
2076 ccs.extend(options.cc)
Joanna Wangb46232e2023-01-21 01:58:46 +00002077
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002078 ccs = [email.strip() for email in ccs if email.strip()]
2079 if change_desc.get_cced():
2080 ccs.extend(change_desc.get_cced())
Joanna Wangb46232e2023-01-21 01:58:46 +00002081
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002082 return change_desc.get_reviewers(), ccs, change_desc
Joanna Wangb46232e2023-01-21 01:58:46 +00002083
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002084 def PostUploadUpdates(self, options: optparse.Values,
2085 new_upload: _NewUpload, change_number: str) -> None:
2086 """Makes necessary post upload changes to the local and remote cl."""
2087 if not self.GetIssue():
2088 self.SetIssue(change_number)
Joanna Wang40497912023-01-24 21:18:16 +00002089
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002090 self.SetPatchset(new_upload.prev_patchset + 1)
Joanna Wang7603f042023-03-01 22:17:36 +00002091
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002092 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY,
2093 new_upload.commit_to_push)
2094 self._GitSetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY,
2095 new_upload.new_last_uploaded_commit)
Joanna Wang40497912023-01-24 21:18:16 +00002096
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002097 if settings.GetRunPostUploadHook():
2098 self.RunPostUploadHook(options.verbose, new_upload.parent,
2099 new_upload.change_desc.description)
Joanna Wang40497912023-01-24 21:18:16 +00002100
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002101 if new_upload.reviewers or new_upload.ccs:
2102 gerrit_util.AddReviewers(self.GetGerritHost(),
2103 self._GerritChangeIdentifier(),
2104 reviewers=new_upload.reviewers,
2105 ccs=new_upload.ccs,
2106 notify=bool(options.send_mail))
Joanna Wang40497912023-01-24 21:18:16 +00002107
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002108 def CMDUpload(self, options, git_diff_args, orig_args):
2109 """Uploads a change to codereview."""
2110 custom_cl_base = None
2111 if git_diff_args:
2112 custom_cl_base = base_branch = git_diff_args[0]
2113 else:
2114 if self.GetBranch() is None:
2115 DieWithError(
2116 'Can\'t upload from detached HEAD state. Get on a branch!')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002117
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002118 # Default to diffing against common ancestor of upstream branch
2119 base_branch = self.GetCommonAncestorWithUpstream()
2120 git_diff_args = [base_branch, 'HEAD']
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002121
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002122 # Fast best-effort checks to abort before running potentially expensive
2123 # hooks if uploading is likely to fail anyway. Passing these checks does
2124 # not guarantee that uploading will not fail.
2125 self.EnsureAuthenticated(force=options.force)
2126 self.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002127
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002128 print(f'Processing {_GetCommitCountSummary(*git_diff_args)}...')
Daniel Cheng66d0f152023-08-29 23:21:58 +00002129
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002130 # Apply watchlists on upload.
2131 watchlist = watchlists.Watchlists(settings.GetRoot())
2132 files = self.GetAffectedFiles(base_branch)
2133 if not options.bypass_watchlists:
2134 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002135
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002136 change_desc = self._GetDescriptionForUpload(options, git_diff_args,
2137 files)
2138 if not options.bypass_hooks:
2139 hook_results = self.RunHook(committing=False,
2140 may_prompt=not options.force,
2141 verbose=options.verbose,
2142 parallel=options.parallel,
2143 upstream=base_branch,
2144 description=change_desc.description,
2145 all_files=False)
2146 self.ExtendCC(hook_results['more_cc'])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002147
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002148 print_stats(git_diff_args)
2149 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base,
2150 change_desc)
2151 if not ret:
2152 if self.GetBranch() is not None:
2153 self._GitSetBranchConfigValue(
2154 LAST_UPLOAD_HASH_CONFIG_KEY,
2155 scm.GIT.ResolveCommit(settings.GetRoot(), 'HEAD'))
2156 # Run post upload hooks, if specified.
2157 if settings.GetRunPostUploadHook():
2158 self.RunPostUploadHook(options.verbose, base_branch,
2159 change_desc.description)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002160
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002161 # Upload all dependencies if specified.
2162 if options.dependencies:
2163 print()
2164 print('--dependencies has been specified.')
2165 print('All dependent local branches will be re-uploaded.')
2166 print()
2167 # Remove the dependencies flag from args so that we do not end
2168 # up in a loop.
2169 orig_args.remove('--dependencies')
2170 ret = upload_branch_deps(self, orig_args, options.force)
2171 return ret
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002172
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002173 def SetCQState(self, new_state):
2174 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002175
Struan Shrimpton8b2072b2023-07-31 21:01:26 +00002176 Issue must have been already uploaded and known.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002177 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002178 assert new_state in _CQState.ALL_STATES
2179 assert self.GetIssue()
2180 try:
2181 vote_map = {
2182 _CQState.NONE: 0,
2183 _CQState.DRY_RUN: 1,
2184 _CQState.COMMIT: 2,
2185 }
2186 labels = {'Commit-Queue': vote_map[new_state]}
2187 notify = False if new_state == _CQState.DRY_RUN else None
2188 gerrit_util.SetReview(self.GetGerritHost(),
2189 self._GerritChangeIdentifier(),
2190 labels=labels,
2191 notify=notify)
2192 return 0
2193 except KeyboardInterrupt:
2194 raise
2195 except:
2196 print(
2197 'WARNING: Failed to %s.\n'
2198 'Either:\n'
2199 ' * Your project has no CQ,\n'
2200 ' * You don\'t have permission to change the CQ state,\n'
2201 ' * There\'s a bug in this code (see stack trace below).\n'
2202 'Consider specifying which bots to trigger manually or asking your '
2203 'project owners for permissions or contacting Chrome Infra at:\n'
2204 'https://www.chromium.org/infra\n\n' %
2205 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
2206 # Still raise exception so that stack trace is printed.
2207 raise
qyearsley1fdfcb62016-10-24 13:22:03 -07002208
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002209 def GetGerritHost(self):
2210 # Lazy load of configs.
2211 self.GetCodereviewServer()
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002212
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002213 if self._gerrit_host and '.' not in self._gerrit_host:
2214 # Abbreviated domain like "chromium" instead of
2215 # chromium.googlesource.com.
2216 parsed = urllib.parse.urlparse(self.GetRemoteUrl())
2217 if parsed.scheme == 'sso':
2218 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2219 self._gerrit_server = 'https://%s' % self._gerrit_host
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002220
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002221 return self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002222
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002223 def _GetGitHost(self):
2224 """Returns git host to be used when uploading change to Gerrit."""
2225 remote_url = self.GetRemoteUrl()
2226 if not remote_url:
2227 return None
2228 return urllib.parse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002229
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002230 def GetCodereviewServer(self):
2231 if not self._gerrit_server:
2232 # If we're on a branch then get the server potentially associated
2233 # with that branch.
2234 if self.GetIssue() and self.GetBranch():
2235 self._gerrit_server = self._GitGetBranchConfigValue(
2236 CODEREVIEW_SERVER_CONFIG_KEY)
2237 if self._gerrit_server:
2238 self._gerrit_host = urllib.parse.urlparse(
2239 self._gerrit_server).netloc
2240 if not self._gerrit_server:
2241 url = urllib.parse.urlparse(self.GetRemoteUrl())
2242 parts = url.netloc.split('.')
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002243
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002244 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2245 # has "-review" suffix for lowest level subdomain.
2246 parts[0] = parts[0] + '-review'
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002247
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002248 if url.scheme == 'sso' and len(parts) == 1:
2249 # sso:// uses abbreivated hosts, eg. sso://chromium instead
2250 # of chromium.googlesource.com. Hence, for code review
2251 # server, they need to be expanded.
2252 parts[0] += '.googlesource.com'
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002253
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002254 self._gerrit_host = '.'.join(parts)
2255 self._gerrit_server = 'https://%s' % self._gerrit_host
2256 return self._gerrit_server
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002257
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002258 def GetGerritProject(self):
2259 """Returns Gerrit project name based on remote git URL."""
2260 remote_url = self.GetRemoteUrl()
2261 if remote_url is None:
2262 logging.warning('can\'t detect Gerrit project.')
2263 return None
2264 project = urllib.parse.urlparse(remote_url).path.strip('/')
2265 if project.endswith('.git'):
2266 project = project[:-len('.git')]
2267 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start
2268 # with 'a/' prefix, because 'a/' prefix is used to force authentication
2269 # in gitiles/git-over-https protocol. E.g.,
2270 # https://chromium.googlesource.com/a/v8/v8 refers to the same
2271 # repo/project as https://chromium.googlesource.com/v8/v8
2272 if project.startswith('a/'):
2273 project = project[len('a/'):]
2274 return project
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002275
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002276 def _GerritChangeIdentifier(self):
2277 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002278
2279 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002280 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002281 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002282 project = self.GetGerritProject()
2283 if project:
2284 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
2285 # Fall back on still unique, but less efficient change number.
2286 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002287
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002288 def EnsureAuthenticated(self, force, refresh=None):
2289 """Best effort check that user is authenticated with Gerrit server."""
2290 if settings.GetGerritSkipEnsureAuthenticated():
2291 # For projects with unusual authentication schemes.
2292 # See http://crbug.com/603378.
2293 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002294
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002295 # Check presence of cookies only if using cookies-based auth method.
2296 cookie_auth = gerrit_util.Authenticator.get()
2297 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2298 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002299
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002300 remote_url = self.GetRemoteUrl()
2301 if remote_url is None:
2302 logging.warning('invalid remote')
2303 return
2304 if urllib.parse.urlparse(remote_url).scheme not in ['https', 'sso']:
2305 logging.warning(
2306 'Ignoring branch %(branch)s with non-https/sso remote '
2307 '%(remote)s', {
2308 'branch': self.branch,
2309 'remote': self.GetRemoteUrl()
2310 })
2311 return
Daniel Chengcf6269b2019-05-18 01:02:12 +00002312
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002313 # Lazy-loader to identify Gerrit and Git hosts.
2314 self.GetCodereviewServer()
2315 git_host = self._GetGitHost()
2316 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002317
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002318 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2319 git_auth = cookie_auth.get_auth_header(git_host)
2320 if gerrit_auth and git_auth:
2321 if gerrit_auth == git_auth:
2322 return
2323 all_gsrc = cookie_auth.get_auth_header(
2324 'd0esN0tEx1st.googlesource.com')
2325 print(
2326 'WARNING: You have different credentials for Gerrit and git hosts:\n'
2327 ' %s\n'
2328 ' %s\n'
2329 ' Consider running the following command:\n'
2330 ' git cl creds-check\n'
2331 ' %s\n'
2332 ' %s' %
2333 (git_host, self._gerrit_host,
2334 ('Hint: delete creds for .googlesource.com' if all_gsrc else
2335 ''), cookie_auth.get_new_password_message(git_host)))
2336 if not force:
2337 confirm_or_exit('If you know what you are doing',
2338 action='continue')
2339 return
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002340
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002341 missing = (([] if gerrit_auth else [self._gerrit_host]) +
2342 ([] if git_auth else [git_host]))
2343 DieWithError('Credentials for the following hosts are required:\n'
2344 ' %s\n'
2345 'These are read from %s (or legacy %s)\n'
2346 '%s' %
2347 ('\n '.join(missing), cookie_auth.get_gitcookies_path(),
2348 cookie_auth.get_netrc_path(),
2349 cookie_auth.get_new_password_message(git_host)))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002350
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002351 def EnsureCanUploadPatchset(self, force):
2352 if not self.GetIssue():
2353 return
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002354
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002355 status = self._GetChangeDetail()['status']
2356 if status == 'ABANDONED':
2357 DieWithError(
2358 'Change %s has been abandoned, new uploads are not allowed' %
2359 (self.GetIssueURL()))
2360 if status == 'MERGED':
2361 answer = gclient_utils.AskForData(
2362 'Change %s has been submitted, new uploads are not allowed. '
2363 'Would you like to start a new change (Y/n)?' %
2364 self.GetIssueURL()).lower()
2365 if answer not in ('y', ''):
2366 DieWithError('New uploads are not allowed.')
2367 self.SetIssue()
2368 return
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002369
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002370 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2371 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2372 # Apparently this check is not very important? Otherwise get_auth_email
2373 # could have been added to other implementations of Authenticator.
2374 cookies_auth = gerrit_util.Authenticator.get()
2375 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
2376 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002377
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002378 cookies_user = cookies_auth.get_auth_email(self.GetGerritHost())
2379 if self.GetIssueOwner() == cookies_user:
2380 return
2381 logging.debug('change %s owner is %s, cookies user is %s',
2382 self.GetIssue(), self.GetIssueOwner(), cookies_user)
2383 # Maybe user has linked accounts or something like that,
2384 # so ask what Gerrit thinks of this user.
2385 details = gerrit_util.GetAccountDetails(self.GetGerritHost(), 'self')
2386 if details['email'] == self.GetIssueOwner():
2387 return
2388 if not force:
2389 print(
2390 'WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
2391 'as %s.\n'
2392 'Uploading may fail due to lack of permissions.' %
2393 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2394 confirm_or_exit(action='upload')
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002395
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002396 def GetStatus(self):
2397 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002398 or CQ status, assuming adherence to a common workflow.
2399
2400 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002401 * 'error' - error from review tool (including deleted issues)
2402 * 'unsent' - no reviewers added
2403 * 'waiting' - waiting for review
2404 * 'reply' - waiting for uploader to reply to review
2405 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002406 * 'dry-run' - dry-running in the CQ
2407 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07002408 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002409 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002410 if not self.GetIssue():
2411 return None
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002412
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002413 try:
2414 data = self._GetChangeDetail(
2415 ['DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
2416 except GerritChangeNotExists:
2417 return 'error'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002418
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002419 if data['status'] in ('ABANDONED', 'MERGED'):
2420 return 'closed'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002421
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002422 cq_label = data['labels'].get('Commit-Queue', {})
2423 max_cq_vote = 0
2424 for vote in cq_label.get('all', []):
2425 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
2426 if max_cq_vote == 2:
2427 return 'commit'
2428 if max_cq_vote == 1:
2429 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002430
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002431 if data['labels'].get('Code-Review', {}).get('approved'):
2432 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002433
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002434 if not data.get('reviewers', {}).get('REVIEWER', []):
2435 return 'unsent'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002436
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002437 owner = data['owner'].get('_account_id')
2438 messages = sorted(data.get('messages', []), key=lambda m: m.get('date'))
2439 while messages:
2440 m = messages.pop()
2441 if (m.get('tag', '').startswith('autogenerated:cq')
2442 or m.get('tag', '').startswith('autogenerated:cv')):
2443 # Ignore replies from LUCI CV/CQ.
2444 continue
2445 if m.get('author', {}).get('_account_id') == owner:
2446 # Most recent message was by owner.
2447 return 'waiting'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002448
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002449 # Some reply from non-owner.
2450 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002451
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002452 # Somehow there are no messages even though there are reviewers.
2453 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002454
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002455 def GetMostRecentPatchset(self, update=True):
2456 if not self.GetIssue():
2457 return None
Edward Lemur6c6827c2020-02-06 21:15:18 +00002458
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002459 data = self._GetChangeDetail(['CURRENT_REVISION'])
2460 patchset = data['revisions'][data['current_revision']]['_number']
2461 if update:
2462 self.SetPatchset(patchset)
2463 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002464
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002465 def _IsPatchsetRangeSignificant(self, lower, upper):
2466 """Returns True if the inclusive range of patchsets contains any reworks or
Gavin Makf35a9eb2022-11-17 18:34:36 +00002467 rebases."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002468 if not self.GetIssue():
2469 return False
Gavin Makf35a9eb2022-11-17 18:34:36 +00002470
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002471 data = self._GetChangeDetail(['ALL_REVISIONS'])
2472 ps_kind = {}
2473 for rev_info in data.get('revisions', {}).values():
2474 ps_kind[rev_info['_number']] = rev_info.get('kind', '')
Gavin Makf35a9eb2022-11-17 18:34:36 +00002475
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002476 for ps in range(lower, upper + 1):
2477 assert ps in ps_kind, 'expected patchset %d in change detail' % ps
2478 if ps_kind[ps] not in ('NO_CHANGE', 'NO_CODE_CHANGE'):
2479 return True
2480 return False
Gavin Makf35a9eb2022-11-17 18:34:36 +00002481
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002482 def GetMostRecentDryRunPatchset(self):
2483 """Get patchsets equivalent to the most recent patchset and return
Gavin Make61ccc52020-11-13 00:12:57 +00002484 the patchset with the latest dry run. If none have been dry run, return
2485 the latest patchset."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002486 if not self.GetIssue():
2487 return None
Gavin Make61ccc52020-11-13 00:12:57 +00002488
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002489 data = self._GetChangeDetail(['ALL_REVISIONS'])
2490 patchset = data['revisions'][data['current_revision']]['_number']
2491 dry_run = {
2492 int(m['_revision_number'])
2493 for m in data.get('messages', [])
2494 if m.get('tag', '').endswith('dry-run')
2495 }
Gavin Make61ccc52020-11-13 00:12:57 +00002496
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002497 for revision_info in sorted(data.get('revisions', {}).values(),
2498 key=lambda c: c['_number'],
2499 reverse=True):
2500 if revision_info['_number'] in dry_run:
2501 patchset = revision_info['_number']
2502 break
2503 if revision_info.get('kind', '') not in \
2504 ('NO_CHANGE', 'NO_CODE_CHANGE', 'TRIVIAL_REBASE'):
2505 break
2506 self.SetPatchset(patchset)
2507 return patchset
Gavin Make61ccc52020-11-13 00:12:57 +00002508
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002509 def AddComment(self, message, publish=None):
2510 gerrit_util.SetReview(self.GetGerritHost(),
2511 self._GerritChangeIdentifier(),
2512 msg=message,
2513 ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002514
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002515 def GetCommentsSummary(self, readable=True):
2516 # DETAILED_ACCOUNTS is to get emails in accounts.
2517 # CURRENT_REVISION is included to get the latest patchset so that
2518 # only the robot comments from the latest patchset can be shown.
2519 messages = self._GetChangeDetail(
2520 options=['MESSAGES', 'DETAILED_ACCOUNTS', 'CURRENT_REVISION']).get(
2521 'messages', [])
2522 file_comments = gerrit_util.GetChangeComments(
2523 self.GetGerritHost(), self._GerritChangeIdentifier())
2524 robot_file_comments = gerrit_util.GetChangeRobotComments(
2525 self.GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002526
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002527 # Add the robot comments onto the list of comments, but only
2528 # keep those that are from the latest patchset.
2529 latest_patch_set = self.GetMostRecentPatchset()
2530 for path, robot_comments in robot_file_comments.items():
2531 line_comments = file_comments.setdefault(path, [])
2532 line_comments.extend([
2533 c for c in robot_comments if c['patch_set'] == latest_patch_set
2534 ])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002535
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002536 # Build dictionary of file comments for easy access and sorting later.
2537 # {author+date: {path: {patchset: {line: url+message}}}}
2538 comments = collections.defaultdict(lambda: collections.defaultdict(
2539 lambda: collections.defaultdict(dict)))
Andrii Shyshkalova3762a92020-11-25 10:20:42 +00002540
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002541 server = self.GetCodereviewServer()
2542 if server in _KNOWN_GERRIT_TO_SHORT_URLS:
2543 # /c/ is automatically added by short URL server.
2544 url_prefix = '%s/%s' % (_KNOWN_GERRIT_TO_SHORT_URLS[server],
2545 self.GetIssue())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002546 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002547 url_prefix = '%s/c/%s' % (server, self.GetIssue())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002548
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002549 for path, line_comments in file_comments.items():
2550 for comment in line_comments:
2551 tag = comment.get('tag', '')
2552 if tag.startswith(
2553 'autogenerated') and 'robot_id' not in comment:
2554 continue
2555 key = (comment['author']['email'], comment['updated'])
2556 if comment.get('side', 'REVISION') == 'PARENT':
2557 patchset = 'Base'
2558 else:
2559 patchset = 'PS%d' % comment['patch_set']
2560 line = comment.get('line', 0)
2561 url = ('%s/%s/%s#%s%s' %
2562 (url_prefix, comment['patch_set'],
2563 path, 'b' if comment.get('side') == 'PARENT' else '',
2564 str(line) if line else ''))
2565 comments[key][path][patchset][line] = (url, comment['message'])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002566
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002567 summaries = []
2568 for msg in messages:
2569 summary = self._BuildCommentSummary(msg, comments, readable)
2570 if summary:
2571 summaries.append(summary)
2572 return summaries
Josip Sokcevic266129c2021-11-09 00:22:00 +00002573
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002574 @staticmethod
2575 def _BuildCommentSummary(msg, comments, readable):
2576 if 'email' not in msg['author']:
2577 # Some bot accounts may not have an email associated.
2578 return None
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002579
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002580 key = (msg['author']['email'], msg['date'])
2581 # Don't bother showing autogenerated messages that don't have associated
2582 # file or line comments. this will filter out most autogenerated
2583 # messages, but will keep robot comments like those from Tricium.
2584 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
2585 if is_autogenerated and not comments.get(key):
2586 return None
2587 message = msg['message']
2588 # Gerrit spits out nanoseconds.
2589 assert len(msg['date'].split('.')[-1]) == 9
2590 date = datetime.datetime.strptime(msg['date'][:-3],
2591 '%Y-%m-%d %H:%M:%S.%f')
2592 if key in comments:
2593 message += '\n'
2594 for path, patchsets in sorted(comments.get(key, {}).items()):
2595 if readable:
2596 message += '\n%s' % path
2597 for patchset, lines in sorted(patchsets.items()):
2598 for line, (url, content) in sorted(lines.items()):
2599 if line:
2600 line_str = 'Line %d' % line
2601 path_str = '%s:%d:' % (path, line)
2602 else:
2603 line_str = 'File comment'
2604 path_str = '%s:0:' % path
2605 if readable:
2606 message += '\n %s, %s: %s' % (patchset, line_str, url)
2607 message += '\n %s\n' % content
2608 else:
2609 message += '\n%s ' % path_str
2610 message += '\n%s\n' % content
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002611
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002612 return _CommentSummary(
2613 date=date,
2614 message=message,
2615 sender=msg['author']['email'],
2616 autogenerated=is_autogenerated,
2617 # These could be inferred from the text messages and correlated with
2618 # Code-Review label maximum, however this is not reliable.
2619 # Leaving as is until the need arises.
2620 approval=False,
2621 disapproval=False,
2622 )
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002623
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002624 def CloseIssue(self):
2625 gerrit_util.AbandonChange(self.GetGerritHost(),
2626 self._GerritChangeIdentifier(),
2627 msg='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002628
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002629 def SubmitIssue(self):
2630 gerrit_util.SubmitChange(self.GetGerritHost(),
2631 self._GerritChangeIdentifier())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002632
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002633 def _GetChangeDetail(self, options=None):
2634 """Returns details of associated Gerrit change and caching results."""
2635 options = options or []
2636 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002637
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002638 # Optimization to avoid multiple RPCs:
2639 if 'CURRENT_REVISION' in options or 'ALL_REVISIONS' in options:
2640 options.append('CURRENT_COMMIT')
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002641
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002642 # Normalize issue and options for consistent keys in cache.
2643 cache_key = str(self.GetIssue())
2644 options_set = frozenset(o.upper() for o in options)
2645
2646 for cached_options_set, data in self._detail_cache.get(cache_key, []):
2647 # Assumption: data fetched before with extra options is suitable
2648 # for return for a smaller set of options.
2649 # For example, if we cached data for
2650 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2651 # and request is for options=[CURRENT_REVISION],
2652 # THEN we can return prior cached data.
2653 if options_set.issubset(cached_options_set):
2654 return data
2655
2656 try:
2657 data = gerrit_util.GetChangeDetail(self.GetGerritHost(),
2658 self._GerritChangeIdentifier(),
2659 options_set)
2660 except gerrit_util.GerritError as e:
2661 if e.http_status == 404:
2662 raise GerritChangeNotExists(self.GetIssue(),
2663 self.GetCodereviewServer())
2664 raise
2665
2666 self._detail_cache.setdefault(cache_key, []).append((options_set, data))
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002667 return data
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002668
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002669 def _GetChangeCommit(self, revision='current'):
2670 assert self.GetIssue(), 'issue must be set to query Gerrit'
2671 try:
2672 data = gerrit_util.GetChangeCommit(self.GetGerritHost(),
2673 self._GerritChangeIdentifier(),
2674 revision)
2675 except gerrit_util.GerritError as e:
2676 if e.http_status == 404:
2677 raise GerritChangeNotExists(self.GetIssue(),
2678 self.GetCodereviewServer())
2679 raise
2680 return data
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002681
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002682 def _IsCqConfigured(self):
2683 detail = self._GetChangeDetail(['LABELS'])
2684 return u'Commit-Queue' in detail.get('labels', {})
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002685
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002686 def CMDLand(self, force, bypass_hooks, verbose, parallel, resultdb, realm):
2687 if git_common.is_dirty_git_tree('land'):
2688 return 1
agable32978d92016-11-01 12:55:02 -07002689
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002690 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2691 if not force and self._IsCqConfigured():
2692 confirm_or_exit(
2693 '\nIt seems this repository has a CQ, '
2694 'which can test and land changes for you. '
2695 'Are you sure you wish to bypass it?\n',
2696 action='bypass CQ')
2697 differs = True
2698 last_upload = self._GitGetBranchConfigValue(
Gavin Mak4e5e3992022-11-14 22:40:12 +00002699 GERRIT_SQUASH_HASH_CONFIG_KEY)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002700 # Note: git diff outputs nothing if there is no diff.
2701 if not last_upload or RunGit(['diff', last_upload]).strip():
2702 print(
2703 'WARNING: Some changes from local branch haven\'t been uploaded.'
2704 )
Edward Lemur5a644f82020-03-18 16:44:57 +00002705 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002706 if detail['current_revision'] == last_upload:
2707 differs = False
2708 else:
2709 print(
2710 'WARNING: Local branch contents differ from latest uploaded '
2711 'patchset.')
2712 if differs:
2713 if not force:
2714 confirm_or_exit(
2715 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2716 action='submit')
2717 print(
2718 'WARNING: Bypassing hooks and submitting latest uploaded patchset.'
2719 )
2720 elif not bypass_hooks:
2721 upstream = self.GetCommonAncestorWithUpstream()
2722 if self.GetIssue():
2723 description = self.FetchDescription()
2724 else:
2725 description = _create_description_from_log([upstream])
2726 self.RunHook(committing=True,
2727 may_prompt=not force,
2728 verbose=verbose,
2729 parallel=parallel,
2730 upstream=upstream,
2731 description=description,
2732 all_files=False,
2733 resultdb=resultdb,
2734 realm=realm)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002735
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002736 self.SubmitIssue()
2737 print('Issue %s has been submitted.' % self.GetIssueURL())
2738 links = self._GetChangeCommit().get('web_links', [])
2739 for link in links:
2740 if link.get('name') in ['gitiles', 'browse'] and link.get('url'):
2741 print('Landed as: %s' % link.get('url'))
2742 break
2743 return 0
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002744
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002745 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force,
2746 newbranch):
2747 assert parsed_issue_arg.valid
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002748
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002749 self.issue = parsed_issue_arg.issue
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002750
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002751 if parsed_issue_arg.hostname:
2752 self._gerrit_host = parsed_issue_arg.hostname
2753 self._gerrit_server = 'https://%s' % self._gerrit_host
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002754
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002755 try:
2756 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2757 except GerritChangeNotExists as e:
2758 DieWithError(str(e))
agablec6787972016-09-09 16:13:34 -07002759
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002760 if not parsed_issue_arg.patchset:
2761 # Use current revision by default.
2762 revision_info = detail['revisions'][detail['current_revision']]
2763 patchset = int(revision_info['_number'])
2764 else:
2765 patchset = parsed_issue_arg.patchset
2766 for revision_info in detail['revisions'].values():
2767 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2768 break
2769 else:
2770 DieWithError('Couldn\'t find patchset %i in change %i' %
2771 (parsed_issue_arg.patchset, self.GetIssue()))
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002772
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002773 remote_url = self.GetRemoteUrl()
2774 if remote_url.endswith('.git'):
2775 remote_url = remote_url[:-len('.git')]
2776 remote_url = remote_url.rstrip('/')
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002777
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002778 fetch_info = revision_info['fetch']['http']
2779 fetch_info['url'] = fetch_info['url'].rstrip('/')
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002780
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002781 if remote_url != fetch_info['url']:
2782 DieWithError(
2783 'Trying to patch a change from %s but this repo appears '
2784 'to be %s.' % (fetch_info['url'], remote_url))
Gavin Mak4e5e3992022-11-14 22:40:12 +00002785
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002786 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002787
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002788 # Set issue immediately in case the cherry-pick fails, which happens
2789 # when resolving conflicts.
2790 if self.GetBranch():
2791 self.SetIssue(parsed_issue_arg.issue)
tandrii88189772016-09-29 04:29:57 -07002792
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002793 if force:
2794 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2795 print('Checked out commit for change %i patchset %i locally' %
2796 (parsed_issue_arg.issue, patchset))
2797 elif nocommit:
2798 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2799 print('Patch applied to index.')
2800 else:
2801 RunGit(['cherry-pick', 'FETCH_HEAD'])
2802 print('Committed patch for change %i patchset %i locally.' %
2803 (parsed_issue_arg.issue, patchset))
2804 print(
2805 'Note: this created a local commit which does not have '
2806 'the same hash as the one uploaded for review. This will make '
2807 'uploading changes based on top of this branch difficult.\n'
2808 'If you want to do that, use "git cl patch --force" instead.')
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002809
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002810 if self.GetBranch():
2811 self.SetPatchset(patchset)
2812 fetched_hash = scm.GIT.ResolveCommit(settings.GetRoot(),
2813 'FETCH_HEAD')
2814 self._GitSetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY,
2815 fetched_hash)
2816 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY,
2817 fetched_hash)
2818 else:
2819 print(
2820 'WARNING: You are in detached HEAD state.\n'
2821 'The patch has been applied to your checkout, but you will not be '
2822 'able to upload a new patch set to the gerrit issue.\n'
2823 'Try using the \'-b\' option if you would like to work on a '
2824 'branch and/or upload a new patch set.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002825
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002826 return 0
2827
2828 @staticmethod
2829 def _GerritCommitMsgHookCheck(offer_removal):
2830 # type: (bool) -> None
2831 """Checks for the gerrit's commit-msg hook and removes it if necessary."""
2832 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2833 if not os.path.exists(hook):
2834 return
2835 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
2836 # custom developer-made one.
2837 data = gclient_utils.FileRead(hook)
2838 if not ('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2839 return
2840 print('WARNING: You have Gerrit commit-msg hook installed.\n'
2841 'It is not necessary for uploading with git cl in squash mode, '
2842 'and may interfere with it in subtle ways.\n'
2843 'We recommend you remove the commit-msg hook.')
2844 if offer_removal:
2845 if ask_for_explicit_yes('Do you want to remove it now?'):
2846 gclient_utils.rm_file_or_tree(hook)
2847 print('Gerrit commit-msg hook removed.')
2848 else:
2849 print('OK, will keep Gerrit commit-msg hook in place.')
2850
2851 def _CleanUpOldTraces(self):
2852 """Keep only the last |MAX_TRACES| traces."""
2853 try:
2854 traces = sorted([
2855 os.path.join(TRACES_DIR, f) for f in os.listdir(TRACES_DIR)
2856 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2857 and not f.startswith('tmp'))
2858 ])
2859 traces_to_delete = traces[:-MAX_TRACES]
2860 for trace in traces_to_delete:
2861 os.remove(trace)
2862 except OSError:
2863 print('WARNING: Failed to remove old git traces from\n'
2864 ' %s'
2865 'Consider removing them manually.' % TRACES_DIR)
2866
2867 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
2868 """Zip and write the git push traces stored in traces_dir."""
2869 gclient_utils.safe_makedirs(TRACES_DIR)
2870 traces_zip = trace_name + '-traces'
2871 traces_readme = trace_name + '-README'
2872 # Create a temporary dir to store git config and gitcookies in. It will
2873 # be compressed and stored next to the traces.
2874 git_info_dir = tempfile.mkdtemp()
2875 git_info_zip = trace_name + '-git-info'
2876
2877 git_push_metadata['now'] = datetime_now().strftime(
2878 '%Y-%m-%dT%H:%M:%S.%f')
2879
2880 git_push_metadata['trace_name'] = trace_name
2881 gclient_utils.FileWrite(traces_readme,
2882 TRACES_README_FORMAT % git_push_metadata)
2883
2884 # Keep only the first 6 characters of the git hashes on the packet
2885 # trace. This greatly decreases size after compression.
2886 packet_traces = os.path.join(traces_dir, 'trace-packet')
2887 if os.path.isfile(packet_traces):
2888 contents = gclient_utils.FileRead(packet_traces)
2889 gclient_utils.FileWrite(packet_traces,
2890 GIT_HASH_RE.sub(r'\1', contents))
2891 shutil.make_archive(traces_zip, 'zip', traces_dir)
2892
2893 # Collect and compress the git config and gitcookies.
2894 git_config = RunGit(['config', '-l'])
2895 gclient_utils.FileWrite(os.path.join(git_info_dir, 'git-config'),
2896 git_config)
2897
2898 cookie_auth = gerrit_util.Authenticator.get()
2899 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2900 gitcookies_path = cookie_auth.get_gitcookies_path()
2901 if os.path.isfile(gitcookies_path):
2902 gitcookies = gclient_utils.FileRead(gitcookies_path)
2903 gclient_utils.FileWrite(
2904 os.path.join(git_info_dir, 'gitcookies'),
2905 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2906 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2907
2908 gclient_utils.rmtree(git_info_dir)
2909
2910 def _RunGitPushWithTraces(self,
2911 refspec,
2912 refspec_opts,
2913 git_push_metadata,
2914 git_push_options=None):
2915 """Run git push and collect the traces resulting from the execution."""
2916 # Create a temporary directory to store traces in. Traces will be
2917 # compressed and stored in a 'traces' dir inside depot_tools.
2918 traces_dir = tempfile.mkdtemp()
2919 trace_name = os.path.join(TRACES_DIR,
2920 datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
2921
2922 env = os.environ.copy()
2923 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2924 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
2925 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
2926 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2927 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2928 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2929
2930 push_returncode = 0
2931 before_push = time_time()
2932 try:
2933 remote_url = self.GetRemoteUrl()
2934 push_cmd = ['git', 'push', remote_url, refspec]
2935 if git_push_options:
2936 for opt in git_push_options:
2937 push_cmd.extend(['-o', opt])
2938
2939 push_stdout = gclient_utils.CheckCallAndFilter(
2940 push_cmd,
2941 env=env,
2942 print_stdout=True,
2943 # Flush after every line: useful for seeing progress when
2944 # running as recipe.
2945 filter_fn=lambda _: sys.stdout.flush())
2946 push_stdout = push_stdout.decode('utf-8', 'replace')
2947 except subprocess2.CalledProcessError as e:
2948 push_returncode = e.returncode
2949 if 'blocked keyword' in str(e.stdout) or 'banned word' in str(
2950 e.stdout):
2951 raise GitPushError(
2952 'Failed to create a change, very likely due to blocked keyword. '
2953 'Please examine output above for the reason of the failure.\n'
2954 'If this is a false positive, you can try to bypass blocked '
2955 'keyword by using push option '
2956 '-o banned-words~skip, e.g.:\n'
2957 'git cl upload -o banned-words~skip\n\n'
2958 'If git-cl is not working correctly, file a bug under the '
2959 'Infra>SDK component.')
2960 if 'git push -o nokeycheck' in str(e.stdout):
2961 raise GitPushError(
2962 'Failed to create a change, very likely due to a private key being '
2963 'detected. Please examine output above for the reason of the '
2964 'failure.\n'
2965 'If this is a false positive, you can try to bypass private key '
2966 'detection by using push option '
2967 '-o nokeycheck, e.g.:\n'
2968 'git cl upload -o nokeycheck\n\n'
2969 'If git-cl is not working correctly, file a bug under the '
2970 'Infra>SDK component.')
2971
2972 raise GitPushError(
2973 'Failed to create a change. Please examine output above for the '
2974 'reason of the failure.\n'
2975 'For emergencies, Googlers can escalate to '
2976 'go/gob-support or go/notify#gob\n'
2977 'Hint: run command below to diagnose common Git/Gerrit '
2978 'credential problems:\n'
2979 ' git cl creds-check\n'
2980 '\n'
2981 'If git-cl is not working correctly, file a bug under the Infra>SDK '
2982 'component including the files below.\n'
2983 'Review the files before upload, since they might contain sensitive '
2984 'information.\n'
2985 'Set the Restrict-View-Google label so that they are not publicly '
2986 'accessible.\n' + TRACES_MESSAGE % {'trace_name': trace_name})
2987 finally:
2988 execution_time = time_time() - before_push
2989 metrics.collector.add_repeated(
2990 'sub_commands', {
2991 'command':
2992 'git push',
2993 'execution_time':
2994 execution_time,
2995 'exit_code':
2996 push_returncode,
2997 'arguments':
2998 metrics_utils.extract_known_subcommand_args(refspec_opts),
2999 })
3000
3001 git_push_metadata['execution_time'] = execution_time
3002 git_push_metadata['exit_code'] = push_returncode
3003 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
3004
3005 self._CleanUpOldTraces()
3006 gclient_utils.rmtree(traces_dir)
3007
3008 return push_stdout
3009
3010 def CMDUploadChange(self, options, git_diff_args, custom_cl_base,
3011 change_desc):
3012 """Upload the current branch to Gerrit, retry if new remote HEAD is
3013 found. options and change_desc may be mutated."""
3014 remote, remote_branch = self.GetRemoteBranch()
3015 branch = GetTargetRef(remote, remote_branch, options.target_branch)
3016
3017 try:
3018 return self._CMDUploadChange(options, git_diff_args, custom_cl_base,
3019 change_desc, branch)
3020 except GitPushError as e:
3021 # Repository might be in the middle of transition to main branch as
3022 # default, and uploads to old default might be blocked.
3023 if remote_branch not in [DEFAULT_OLD_BRANCH, DEFAULT_NEW_BRANCH]:
3024 DieWithError(str(e), change_desc)
3025
3026 project_head = gerrit_util.GetProjectHead(self._gerrit_host,
3027 self.GetGerritProject())
3028 if project_head == branch:
3029 DieWithError(str(e), change_desc)
3030 branch = project_head
3031
3032 print("WARNING: Fetching remote state and retrying upload to default "
3033 "branch...")
3034 RunGit(['fetch', '--prune', remote])
3035 options.edit_description = False
3036 options.force = True
3037 try:
3038 self._CMDUploadChange(options, git_diff_args, custom_cl_base,
3039 change_desc, branch)
3040 except GitPushError as e:
3041 DieWithError(str(e), change_desc)
3042
3043 def _CMDUploadChange(self, options, git_diff_args, custom_cl_base,
3044 change_desc, branch):
3045 """Upload the current branch to Gerrit."""
3046 if options.squash:
3047 Changelist._GerritCommitMsgHookCheck(
3048 offer_removal=not options.force)
3049 external_parent = None
3050 if self.GetIssue():
3051 # User requested to change description
3052 if options.edit_description:
3053 change_desc.prompt()
3054 change_detail = self._GetChangeDetail(['CURRENT_REVISION'])
3055 change_id = change_detail['change_id']
3056 change_desc.ensure_change_id(change_id)
3057
3058 # Check if changes outside of this workspace have been uploaded.
3059 current_rev = change_detail['current_revision']
3060 last_uploaded_rev = self._GitGetBranchConfigValue(
3061 GERRIT_SQUASH_HASH_CONFIG_KEY)
3062 if last_uploaded_rev and current_rev != last_uploaded_rev:
3063 external_parent = self._UpdateWithExternalChanges()
3064 else: # if not self.GetIssue()
3065 if not options.force and not options.message_file:
3066 change_desc.prompt()
3067 change_ids = git_footers.get_footer_change_id(
3068 change_desc.description)
3069 if len(change_ids) == 1:
3070 change_id = change_ids[0]
3071 else:
3072 change_id = GenerateGerritChangeId(change_desc.description)
3073 change_desc.ensure_change_id(change_id)
3074
3075 if options.preserve_tryjobs:
3076 change_desc.set_preserve_tryjobs()
3077
3078 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
3079 parent = external_parent or self._ComputeParent(
3080 remote, upstream_branch, custom_cl_base, options.force,
3081 change_desc)
3082 tree = RunGit(['rev-parse', 'HEAD:']).strip()
3083 with gclient_utils.temporary_file() as desc_tempfile:
3084 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
3085 ref_to_push = RunGit(
3086 ['commit-tree', tree, '-p', parent, '-F',
3087 desc_tempfile]).strip()
3088 else: # if not options.squash
3089 if options.no_add_changeid:
3090 pass
3091 else: # adding Change-Ids is okay.
3092 if not git_footers.get_footer_change_id(
3093 change_desc.description):
3094 DownloadGerritHook(False)
3095 change_desc.set_description(
3096 self._AddChangeIdToCommitMessage(
3097 change_desc.description, git_diff_args))
3098 ref_to_push = 'HEAD'
3099 # For no-squash mode, we assume the remote called "origin" is the
3100 # one we want. It is not worthwhile to support different workflows
3101 # for no-squash mode.
3102 parent = 'origin/%s' % branch
3103 # attempt to extract the changeid from the current description
3104 # fail informatively if not possible.
3105 change_id_candidates = git_footers.get_footer_change_id(
3106 change_desc.description)
3107 if not change_id_candidates:
3108 DieWithError("Unable to extract change-id from message.")
3109 change_id = change_id_candidates[0]
3110
3111 SaveDescriptionBackup(change_desc)
3112 commits = RunGitSilent(['rev-list',
3113 '%s..%s' % (parent, ref_to_push)]).splitlines()
3114 if len(commits) > 1:
3115 print(
3116 'WARNING: This will upload %d commits. Run the following command '
3117 'to see which commits will be uploaded: ' % len(commits))
3118 print('git log %s..%s' % (parent, ref_to_push))
3119 print('You can also use `git squash-branch` to squash these into a '
3120 'single commit.')
3121 confirm_or_exit(action='upload')
3122
3123 reviewers = sorted(change_desc.get_reviewers())
3124 cc = []
3125 # Add default, watchlist, presubmit ccs if this is the initial upload
3126 # and CL is not private and auto-ccing has not been disabled.
3127 if not options.private and not options.no_autocc and not self.GetIssue(
3128 ):
3129 cc = self.GetCCList().split(',')
3130 if len(cc) > 100:
3131 lsc = ('https://chromium.googlesource.com/chromium/src/+/HEAD/docs/'
3132 'process/lsc/lsc_workflow.md')
3133 print('WARNING: This will auto-CC %s users.' % len(cc))
3134 print('LSC may be more appropriate: %s' % lsc)
3135 print('You can also use the --no-autocc flag to disable auto-CC.')
3136 confirm_or_exit(action='continue')
3137 # Add cc's from the --cc flag.
3138 if options.cc:
3139 cc.extend(options.cc)
3140 cc = [email.strip() for email in cc if email.strip()]
3141 if change_desc.get_cced():
3142 cc.extend(change_desc.get_cced())
3143 if self.GetGerritHost() == 'chromium-review.googlesource.com':
3144 valid_accounts = set(reviewers + cc)
3145 # TODO(crbug/877717): relax this for all hosts.
3146 else:
3147 valid_accounts = gerrit_util.ValidAccounts(self.GetGerritHost(),
3148 reviewers + cc)
3149 logging.info('accounts %s are recognized, %s invalid',
3150 sorted(valid_accounts),
3151 set(reviewers + cc).difference(set(valid_accounts)))
3152
3153 # Extra options that can be specified at push time. Doc:
3154 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
3155 refspec_opts = self._GetRefSpecOptions(options, change_desc)
3156
3157 for r in sorted(reviewers):
3158 if r in valid_accounts:
3159 refspec_opts.append('r=%s' % r)
3160 reviewers.remove(r)
3161 else:
3162 # TODO(tandrii): this should probably be a hard failure.
3163 print(
3164 'WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
3165 % r)
3166 for c in sorted(cc):
3167 # refspec option will be rejected if cc doesn't correspond to an
3168 # account, even though REST call to add such arbitrary cc may
3169 # succeed.
3170 if c in valid_accounts:
3171 refspec_opts.append('cc=%s' % c)
3172 cc.remove(c)
3173
3174 refspec_suffix = ''
3175 if refspec_opts:
3176 refspec_suffix = '%' + ','.join(refspec_opts)
3177 assert ' ' not in refspec_suffix, (
3178 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3179 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3180
3181 git_push_metadata = {
3182 'gerrit_host': self.GetGerritHost(),
3183 'title': options.title or '<untitled>',
3184 'change_id': change_id,
3185 'description': change_desc.description,
3186 }
3187
3188 # Gerrit may or may not update fast enough to return the correct
3189 # patchset number after we push. Get the pre-upload patchset and
3190 # increment later.
3191 latest_ps = self.GetMostRecentPatchset(update=False) or 0
3192
3193 push_stdout = self._RunGitPushWithTraces(refspec, refspec_opts,
3194 git_push_metadata,
3195 options.push_options)
3196
3197 if options.squash:
3198 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
3199 change_numbers = [
3200 m.group(1) for m in map(regex.match, push_stdout.splitlines())
3201 if m
3202 ]
3203 if len(change_numbers) != 1:
3204 DieWithError((
3205 'Created|Updated %d issues on Gerrit, but only 1 expected.\n'
3206 'Change-Id: %s') % (len(change_numbers), change_id),
3207 change_desc)
3208 self.SetIssue(change_numbers[0])
3209 self.SetPatchset(latest_ps + 1)
3210 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY,
3211 ref_to_push)
3212
3213 if self.GetIssue() and (reviewers or cc):
3214 # GetIssue() is not set in case of non-squash uploads according to
3215 # tests. TODO(crbug.com/751901): non-squash uploads in git cl should
3216 # be removed.
3217 gerrit_util.AddReviewers(self.GetGerritHost(),
3218 self._GerritChangeIdentifier(),
3219 reviewers,
3220 cc,
3221 notify=bool(options.send_mail))
3222
3223 return 0
3224
3225 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3226 change_desc):
3227 """Computes parent of the generated commit to be uploaded to Gerrit.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003228
3229 Returns revision or a ref name.
3230 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003231 if custom_cl_base:
3232 # Try to avoid creating additional unintended CLs when uploading,
3233 # unless user wants to take this risk.
3234 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3235 code, _ = RunGitWithCode([
3236 'merge-base', '--is-ancestor', custom_cl_base,
3237 local_ref_of_target_remote
3238 ])
3239 if code == 1:
3240 print(
3241 '\nWARNING: Manually specified base of this CL `%s` '
3242 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3243 'If you proceed with upload, more than 1 CL may be created by '
3244 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3245 'If you are certain that specified base `%s` has already been '
3246 'uploaded to Gerrit as another CL, you may proceed.\n' %
3247 (custom_cl_base, local_ref_of_target_remote,
3248 custom_cl_base))
3249 if not force:
3250 confirm_or_exit(
3251 'Do you take responsibility for cleaning up potential mess '
3252 'resulting from proceeding with upload?',
3253 action='upload')
3254 return custom_cl_base
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003255
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003256 if remote != '.':
3257 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003258
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003259 # If our upstream branch is local, we base our squashed commit on its
3260 # squashed version.
3261 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
Aaron Gablef97e33d2017-03-30 15:44:27 -07003262
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003263 if upstream_branch_name == 'master':
3264 return self.GetCommonAncestorWithUpstream()
3265 if upstream_branch_name == 'main':
3266 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003267
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003268 # Check the squashed hash of the parent.
3269 # TODO(tandrii): consider checking parent change in Gerrit and using its
3270 # hash if tree hash of latest parent revision (patchset) in Gerrit
3271 # matches the tree hash of the parent branch. The upside is less likely
3272 # bogus requests to reupload parent change just because it's uploadhash
3273 # is missing, yet the downside likely exists, too (albeit unknown to me
3274 # yet).
3275 parent = scm.GIT.GetBranchConfig(settings.GetRoot(),
3276 upstream_branch_name,
3277 GERRIT_SQUASH_HASH_CONFIG_KEY)
3278 # Verify that the upstream branch has been uploaded too, otherwise
3279 # Gerrit will create additional CLs when uploading.
3280 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3281 RunGitSilent(['rev-parse', parent + ':'])):
3282 DieWithError(
3283 '\nUpload upstream branch %s first.\n'
3284 'It is likely that this branch has been rebased since its last '
3285 'upload, so you just need to upload it again.\n'
3286 '(If you uploaded it with --no-squash, then branch dependencies '
3287 'are not supported, and you should reupload with --squash.)' %
3288 upstream_branch_name, change_desc)
3289 return parent
Aaron Gablef97e33d2017-03-30 15:44:27 -07003290
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003291 def _UpdateWithExternalChanges(self):
3292 """Updates workspace with external changes.
Gavin Mak4e5e3992022-11-14 22:40:12 +00003293
3294 Returns the commit hash that should be used as the merge base on upload.
3295 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003296 local_ps = self.GetPatchset()
3297 if local_ps is None:
3298 return
Gavin Mak4e5e3992022-11-14 22:40:12 +00003299
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003300 external_ps = self.GetMostRecentPatchset(update=False)
3301 if external_ps is None or local_ps == external_ps or \
3302 not self._IsPatchsetRangeSignificant(local_ps + 1, external_ps):
3303 return
Gavin Mak4e5e3992022-11-14 22:40:12 +00003304
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003305 num_changes = external_ps - local_ps
3306 if num_changes > 1:
3307 change_words = 'changes were'
3308 else:
3309 change_words = 'change was'
3310 print('\n%d external %s published to %s:\n' %
3311 (num_changes, change_words, self.GetIssueURL(short=True)))
Gavin Mak6f905472023-01-06 21:01:36 +00003312
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003313 # Print an overview of external changes.
3314 ps_to_commit = {}
3315 ps_to_info = {}
3316 revisions = self._GetChangeDetail(['ALL_REVISIONS'])
3317 for commit_id, revision_info in revisions.get('revisions', {}).items():
3318 ps_num = revision_info['_number']
3319 ps_to_commit[ps_num] = commit_id
3320 ps_to_info[ps_num] = revision_info
Gavin Mak6f905472023-01-06 21:01:36 +00003321
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003322 for ps in range(external_ps, local_ps, -1):
3323 commit = ps_to_commit[ps][:8]
3324 desc = ps_to_info[ps].get('description', '')
3325 print('Patchset %d [%s] %s' % (ps, commit, desc))
Gavin Mak6f905472023-01-06 21:01:36 +00003326
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003327 print('\nSee diff at: %s/%d..%d' %
3328 (self.GetIssueURL(short=True), local_ps, external_ps))
3329 print('\nUploading without applying patches will override them.')
Josip Sokcevic43ceaf02023-05-25 15:56:00 +00003330
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003331 if not ask_for_explicit_yes('Get the latest changes and apply on top?'):
3332 return
Gavin Mak4e5e3992022-11-14 22:40:12 +00003333
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003334 # Get latest Gerrit merge base. Use the first parent even if multiple
3335 # exist.
3336 external_parent = self._GetChangeCommit(
3337 revision=external_ps)['parents'][0]
3338 external_base = external_parent['commit']
Gavin Mak4e5e3992022-11-14 22:40:12 +00003339
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003340 branch = git_common.current_branch()
3341 local_base = self.GetCommonAncestorWithUpstream()
3342 if local_base != external_base:
3343 print('\nLocal merge base %s is different from Gerrit %s.\n' %
3344 (local_base, external_base))
3345 if git_common.upstream(branch):
3346 confirm_or_exit(
3347 'Can\'t apply the latest changes from Gerrit.\n'
3348 'Continue with upload and override the latest changes?')
3349 return
3350 print(
3351 'No upstream branch set. Continuing upload with Gerrit merge base.'
3352 )
Gavin Mak4e5e3992022-11-14 22:40:12 +00003353
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003354 external_parent_last_uploaded = self._GetChangeCommit(
3355 revision=local_ps)['parents'][0]
3356 external_base_last_uploaded = external_parent_last_uploaded['commit']
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003357
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003358 if external_base != external_base_last_uploaded:
3359 print('\nPatch set merge bases are different (%s, %s).\n' %
3360 (external_base_last_uploaded, external_base))
3361 confirm_or_exit(
3362 'Can\'t apply the latest changes from Gerrit.\n'
3363 'Continue with upload and override the latest changes?')
3364 return
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003365
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003366 # Fetch Gerrit's CL base if it doesn't exist locally.
3367 remote, _ = self.GetRemoteBranch()
3368 if not scm.GIT.IsValidRevision(settings.GetRoot(), external_base):
3369 RunGitSilent(['fetch', remote, external_base])
Gavin Mak4e5e3992022-11-14 22:40:12 +00003370
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003371 # Get the diff between local_ps and external_ps.
3372 print('Fetching changes...')
3373 issue = self.GetIssue()
3374 changes_ref = 'refs/changes/%02d/%d/' % (issue % 100, issue)
3375 RunGitSilent(['fetch', remote, changes_ref + str(local_ps)])
3376 last_uploaded = RunGitSilent(['rev-parse', 'FETCH_HEAD']).strip()
3377 RunGitSilent(['fetch', remote, changes_ref + str(external_ps)])
3378 latest_external = RunGitSilent(['rev-parse', 'FETCH_HEAD']).strip()
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003379
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003380 # If the commit parents are different, don't apply the diff as it very
3381 # likely contains many more changes not relevant to this CL.
3382 parents = RunGitSilent(
3383 ['rev-parse',
3384 '%s~1' % (last_uploaded),
3385 '%s~1' % (latest_external)]).strip().split()
3386 assert len(parents) == 2, 'Expected two parents.'
3387 if parents[0] != parents[1]:
3388 confirm_or_exit(
3389 'Can\'t apply the latest changes from Gerrit (parent mismatch '
3390 'between PS).\n'
3391 'Continue with upload and override the latest changes?')
3392 return
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003393
Joanna Wangbcba1782023-09-12 22:48:05 +00003394 diff = RunGitSilent([
3395 'diff', '--no-ext-diff',
3396 '%s..%s' % (last_uploaded, latest_external)
3397 ])
Gavin Mak4e5e3992022-11-14 22:40:12 +00003398
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003399 # Diff can be empty in the case of trivial rebases.
3400 if not diff:
3401 return external_base
Gavin Mak4e5e3992022-11-14 22:40:12 +00003402
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003403 # Apply the diff.
3404 with gclient_utils.temporary_file() as diff_tempfile:
3405 gclient_utils.FileWrite(diff_tempfile, diff)
3406 clean_patch = RunGitWithCode(['apply', '--check',
3407 diff_tempfile])[0] == 0
3408 RunGitSilent(['apply', '-3', '--intent-to-add', diff_tempfile])
3409 if not clean_patch:
3410 # Normally patchset is set after upload. But because we exit,
3411 # that never happens. Updating here makes sure that subsequent
3412 # uploads don't need to fetch/apply the same diff again.
3413 self.SetPatchset(external_ps)
3414 DieWithError(
3415 '\nPatch did not apply cleanly. Please resolve any '
3416 'conflicts and reupload.')
Gavin Mak4e5e3992022-11-14 22:40:12 +00003417
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003418 message = 'Incorporate external changes from '
3419 if num_changes == 1:
3420 message += 'patchset %d' % external_ps
3421 else:
3422 message += 'patchsets %d to %d' % (local_ps + 1, external_ps)
3423 RunGitSilent(['commit', '-am', message])
3424 # TODO(crbug.com/1382528): Use the previous commit's message as a
3425 # default patchset title instead of this 'Incorporate' message.
3426 return external_base
Gavin Mak4e5e3992022-11-14 22:40:12 +00003427
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003428 def _AddChangeIdToCommitMessage(self, log_desc, args):
3429 """Re-commits using the current message, assumes the commit hook is in
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003430 place.
3431 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003432 RunGit(['commit', '--amend', '-m', log_desc])
3433 new_log_desc = _create_description_from_log(args)
3434 if git_footers.get_footer_change_id(new_log_desc):
3435 print('git-cl: Added Change-Id to commit message.')
3436 return new_log_desc
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003437
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003438 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003439
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003440 def CannotTriggerTryJobReason(self):
3441 try:
3442 data = self._GetChangeDetail()
3443 except GerritChangeNotExists:
3444 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003445
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003446 if data['status'] in ('ABANDONED', 'MERGED'):
3447 return 'CL %s is closed' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003448
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003449 def GetGerritChange(self, patchset=None):
3450 """Returns a buildbucket.v2.GerritChange message for the current issue."""
3451 host = urllib.parse.urlparse(self.GetCodereviewServer()).hostname
3452 issue = self.GetIssue()
3453 patchset = int(patchset or self.GetPatchset())
3454 data = self._GetChangeDetail(['ALL_REVISIONS'])
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003455
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003456 assert host and issue and patchset, 'CL must be uploaded first'
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003457
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003458 has_patchset = any(
3459 int(revision_data['_number']) == patchset
3460 for revision_data in data['revisions'].values())
3461 if not has_patchset:
3462 raise Exception('Patchset %d is not known in Gerrit change %d' %
3463 (patchset, self.GetIssue()))
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003464
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003465 return {
3466 'host': host,
3467 'change': issue,
3468 'project': data['project'],
3469 'patchset': patchset,
3470 }
tandriie113dfd2016-10-11 10:20:12 -07003471
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003472 def GetIssueOwner(self):
3473 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003474
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003475 def GetReviewers(self):
3476 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3477 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01003478
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003479
Lei Zhang8a0efc12020-08-05 19:58:45 +00003480def _get_bug_line_values(default_project_prefix, bugs):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003481 """Given default_project_prefix and comma separated list of bugs, yields bug
Lei Zhang8a0efc12020-08-05 19:58:45 +00003482 line values.
tandriif9aefb72016-07-01 09:06:51 -07003483
3484 Each bug can be either:
Lei Zhang8a0efc12020-08-05 19:58:45 +00003485 * a number, which is combined with default_project_prefix
tandriif9aefb72016-07-01 09:06:51 -07003486 * string, which is left as is.
3487
3488 This function may produce more than one line, because bugdroid expects one
3489 project per line.
3490
Lei Zhang8a0efc12020-08-05 19:58:45 +00003491 >>> list(_get_bug_line_values('v8:', '123,chromium:789'))
tandriif9aefb72016-07-01 09:06:51 -07003492 ['v8:123', 'chromium:789']
3493 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003494 default_bugs = []
3495 others = []
3496 for bug in bugs.split(','):
3497 bug = bug.strip()
3498 if bug:
3499 try:
3500 default_bugs.append(int(bug))
3501 except ValueError:
3502 others.append(bug)
tandriif9aefb72016-07-01 09:06:51 -07003503
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003504 if default_bugs:
3505 default_bugs = ','.join(map(str, default_bugs))
3506 if default_project_prefix:
3507 if not default_project_prefix.endswith(':'):
3508 default_project_prefix += ':'
3509 yield '%s%s' % (default_project_prefix, default_bugs)
3510 else:
3511 yield default_bugs
3512 for other in sorted(others):
3513 # Don't bother finding common prefixes, CLs with >2 bugs are very very
3514 # rare.
3515 yield other
tandriif9aefb72016-07-01 09:06:51 -07003516
3517
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003518def FindCodereviewSettingsFile(filename='codereview.settings'):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003519 """Finds the given file starting in the cwd and going up.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003520
3521 Only looks up to the top of the repository unless an
3522 'inherit-review-settings-ok' file exists in the root of the repository.
3523 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003524 inherit_ok_file = 'inherit-review-settings-ok'
3525 cwd = os.getcwd()
3526 root = settings.GetRoot()
3527 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3528 root = None
3529 while True:
3530 if os.path.isfile(os.path.join(cwd, filename)):
3531 return open(os.path.join(cwd, filename))
3532 if cwd == root:
3533 break
3534 parent_dir = os.path.dirname(cwd)
3535 if parent_dir == cwd:
3536 # We hit the system root directory.
3537 break
3538 cwd = parent_dir
3539 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003540
3541
3542def LoadCodereviewSettingsFromFile(fileobj):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003543 """Parses a codereview.settings file and updates hooks."""
3544 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003545
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003546 def SetProperty(name, setting, unset_error_ok=False):
3547 fullname = 'rietveld.' + name
3548 if setting in keyvals:
3549 RunGit(['config', fullname, keyvals[setting]])
3550 else:
3551 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003552
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003553 if not keyvals.get('GERRIT_HOST', False):
3554 SetProperty('server', 'CODE_REVIEW_SERVER')
3555 # Only server setting is required. Other settings can be absent.
3556 # In that case, we ignore errors raised during option deletion attempt.
3557 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3558 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3559 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
3560 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
3561 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3562 SetProperty('cpplint-ignore-regex',
3563 'LINT_IGNORE_REGEX',
3564 unset_error_ok=True)
3565 SetProperty('run-post-upload-hook',
3566 'RUN_POST_UPLOAD_HOOK',
3567 unset_error_ok=True)
3568 SetProperty('format-full-by-default',
3569 'FORMAT_FULL_BY_DEFAULT',
3570 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003571
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003572 if 'GERRIT_HOST' in keyvals:
3573 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003574
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003575 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
3576 RunGit([
3577 'config', 'gerrit.squash-uploads', keyvals['GERRIT_SQUASH_UPLOADS']
3578 ])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003579
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003580 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
3581 RunGit([
3582 'config', 'gerrit.skip-ensure-authenticated',
3583 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']
3584 ])
tandrii@chromium.org28253532016-04-14 13:46:56 +00003585
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003586 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3587 # should be of the form
3588 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3589 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
3590 RunGit([
3591 'config', keyvals['PUSH_URL_CONFIG'], keyvals['ORIGIN_URL_CONFIG']
3592 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003593
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003594
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003595def urlretrieve(source, destination):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003596 """Downloads a network object to a local file, like urllib.urlretrieve.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003597
3598 This is necessary because urllib is broken for SSL connections via a proxy.
3599 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003600 with open(destination, 'wb') as f:
3601 f.write(urllib.request.urlopen(source).read())
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003602
3603
ukai@chromium.org712d6102013-11-27 00:52:58 +00003604def hasSheBang(fname):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003605 """Checks fname is a #! script."""
3606 with open(fname) as f:
3607 return f.read(2).startswith('#!')
ukai@chromium.org712d6102013-11-27 00:52:58 +00003608
3609
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003610def DownloadGerritHook(force):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003611 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003612
3613 Args:
3614 force: True to update hooks. False to install hooks if not present.
3615 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003616 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
3617 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3618 if not os.access(dst, os.X_OK):
3619 if os.path.exists(dst):
3620 if not force:
3621 return
3622 try:
3623 urlretrieve(src, dst)
3624 if not hasSheBang(dst):
3625 DieWithError('Not a script: %s\n'
3626 'You need to download from\n%s\n'
3627 'into .git/hooks/commit-msg and '
3628 'chmod +x .git/hooks/commit-msg' % (dst, src))
3629 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3630 except Exception:
3631 if os.path.exists(dst):
3632 os.remove(dst)
3633 DieWithError('\nFailed to download hooks.\n'
3634 'You need to download from\n%s\n'
3635 'into .git/hooks/commit-msg and '
3636 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003637
3638
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003639class _GitCookiesChecker(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003640 """Provides facilities for validating and suggesting fixes to .gitcookies."""
3641 def __init__(self):
3642 # Cached list of [host, identity, source], where source is either
3643 # .gitcookies or .netrc.
3644 self._all_hosts = None
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003645
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003646 def ensure_configured_gitcookies(self):
3647 """Runs checks and suggests fixes to make git use .gitcookies from default
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003648 path."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003649 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3650 configured_path = RunGitSilent(
3651 ['config', '--global', 'http.cookiefile']).strip()
3652 configured_path = os.path.expanduser(configured_path)
3653 if configured_path:
3654 self._ensure_default_gitcookies_path(configured_path, default)
3655 else:
3656 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003657
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003658 @staticmethod
3659 def _ensure_default_gitcookies_path(configured_path, default_path):
3660 assert configured_path
3661 if configured_path == default_path:
3662 print('git is already configured to use your .gitcookies from %s' %
3663 configured_path)
3664 return
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003665
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003666 print('WARNING: You have configured custom path to .gitcookies: %s\n'
3667 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3668 (configured_path, default_path))
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003669
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003670 if not os.path.exists(configured_path):
3671 print('However, your configured .gitcookies file is missing.')
3672 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3673 action='reconfigure')
3674 RunGit(['config', '--global', 'http.cookiefile', default_path])
3675 return
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003676
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003677 if os.path.exists(default_path):
3678 print('WARNING: default .gitcookies file already exists %s' %
3679 default_path)
3680 DieWithError(
3681 'Please delete %s manually and re-run git cl creds-check' %
3682 default_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003683
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003684 confirm_or_exit('Move existing .gitcookies to default location?',
3685 action='move')
3686 shutil.move(configured_path, default_path)
3687 RunGit(['config', '--global', 'http.cookiefile', default_path])
3688 print('Moved and reconfigured git to use .gitcookies from %s' %
3689 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003690
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003691 @staticmethod
3692 def _configure_gitcookies_path(default_path):
3693 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3694 if os.path.exists(netrc_path):
3695 print(
3696 'You seem to be using outdated .netrc for git credentials: %s' %
3697 netrc_path)
3698 print(
3699 'This tool will guide you through setting up recommended '
3700 '.gitcookies store for git credentials.\n'
3701 '\n'
3702 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3703 ' git config --global --unset http.cookiefile\n'
3704 ' mv %s %s.backup\n\n' % (default_path, default_path))
3705 confirm_or_exit(action='setup .gitcookies')
3706 RunGit(['config', '--global', 'http.cookiefile', default_path])
3707 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003708
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003709 def get_hosts_with_creds(self, include_netrc=False):
3710 if self._all_hosts is None:
3711 a = gerrit_util.CookiesAuthenticator()
3712 self._all_hosts = [(h, u, s) for h, u, s in itertools.chain((
3713 (h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.items()), (
3714 (h, u, '.gitcookies')
3715 for h, (u, _) in a.gitcookies.items()))
3716 if h.endswith(_GOOGLESOURCE)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003717
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003718 if include_netrc:
3719 return self._all_hosts
3720 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003721
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003722 def print_current_creds(self, include_netrc=False):
3723 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3724 if not hosts:
3725 print('No Git/Gerrit credentials found')
3726 return
3727 lengths = [max(map(len, (row[i] for row in hosts))) for i in range(3)]
3728 header = [('Host', 'User', 'Which file'), ['=' * l for l in lengths]]
3729 for row in (header + hosts):
3730 print('\t'.join((('%%+%ds' % l) % s) for l, s in zip(lengths, row)))
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003731
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003732 @staticmethod
3733 def _parse_identity(identity):
3734 """Parses identity "git-<username>.domain" into <username> and domain."""
3735 # Special case: usernames that contain ".", which are generally not
3736 # distinguishable from sub-domains. But we do know typical domains:
3737 if identity.endswith('.chromium.org'):
3738 domain = 'chromium.org'
3739 username = identity[:-len('.chromium.org')]
3740 else:
3741 username, domain = identity.split('.', 1)
3742 if username.startswith('git-'):
3743 username = username[len('git-'):]
3744 return username, domain
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003745
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003746 def has_generic_host(self):
3747 """Returns whether generic .googlesource.com has been configured.
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003748
3749 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3750 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003751 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3752 if host == '.' + _GOOGLESOURCE:
3753 return True
3754 return False
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003755
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003756 def _get_git_gerrit_identity_pairs(self):
3757 """Returns map from canonic host to pair of identities (Git, Gerrit).
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003758
3759 One of identities might be None, meaning not configured.
3760 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003761 host_to_identity_pairs = {}
3762 for host, identity, _ in self.get_hosts_with_creds():
3763 canonical = _canonical_git_googlesource_host(host)
3764 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3765 idx = 0 if canonical == host else 1
3766 pair[idx] = identity
3767 return host_to_identity_pairs
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003768
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003769 def get_partially_configured_hosts(self):
3770 return set(
3771 (host if i1 else _canonical_gerrit_googlesource_host(host))
3772 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
3773 if None in (i1, i2) and host != '.' + _GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003774
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003775 def get_conflicting_hosts(self):
3776 return set(
3777 host
3778 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
3779 if None not in (i1, i2) and i1 != i2)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003780
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003781 def get_duplicated_hosts(self):
3782 counters = collections.Counter(
3783 h for h, _, _ in self.get_hosts_with_creds())
3784 return set(host for host, count in counters.items() if count > 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003785
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003786 @staticmethod
3787 def _format_hosts(hosts, extra_column_func=None):
3788 hosts = sorted(hosts)
3789 assert hosts
3790 if extra_column_func is None:
3791 extras = [''] * len(hosts)
3792 else:
3793 extras = [extra_column_func(host) for host in hosts]
3794 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len,
3795 extras)))
3796 lines = []
3797 for he in zip(hosts, extras):
3798 lines.append(tmpl % he)
3799 return lines
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003800
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003801 def _find_problems(self):
3802 if self.has_generic_host():
3803 yield ('.googlesource.com wildcard record detected', [
3804 'Chrome Infrastructure team recommends to list full host names '
3805 'explicitly.'
3806 ], None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003807
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003808 dups = self.get_duplicated_hosts()
3809 if dups:
3810 yield ('The following hosts were defined twice',
3811 self._format_hosts(dups), None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003812
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003813 partial = self.get_partially_configured_hosts()
3814 if partial:
3815 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3816 'These hosts are missing',
3817 self._format_hosts(
3818 partial, lambda host: 'but %s defined' %
3819 _get_counterpart_host(host)), partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003820
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003821 conflicting = self.get_conflicting_hosts()
3822 if conflicting:
3823 yield (
3824 'The following Git hosts have differing credentials from their '
3825 'Gerrit counterparts',
3826 self._format_hosts(
3827 conflicting, lambda host: '%s vs %s' % tuple(
3828 self._get_git_gerrit_identity_pairs()[host])),
3829 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003830
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003831 def find_and_report_problems(self):
3832 """Returns True if there was at least one problem, else False."""
3833 found = False
3834 bad_hosts = set()
3835 for title, sublines, hosts in self._find_problems():
3836 if not found:
3837 found = True
3838 print('\n\n.gitcookies problem report:\n')
3839 bad_hosts.update(hosts or [])
3840 print(' %s%s' % (title, (':' if sublines else '')))
3841 if sublines:
3842 print()
3843 print(' %s' % '\n '.join(sublines))
3844 print()
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003845
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003846 if bad_hosts:
3847 assert found
3848 print(
3849 ' You can manually remove corresponding lines in your %s file and '
3850 'visit the following URLs with correct account to generate '
3851 'correct credential lines:\n' %
3852 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3853 print(' %s' % '\n '.join(
3854 sorted(
3855 set(gerrit_util.CookiesAuthenticator().get_new_password_url(
3856 _canonical_git_googlesource_host(host))
3857 for host in bad_hosts))))
3858 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003859
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003860
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003861@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003862def CMDcreds_check(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003863 """Checks credentials and suggests changes."""
3864 _, _ = parser.parse_args(args)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003865
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003866 # Code below checks .gitcookies. Abort if using something else.
3867 authn = gerrit_util.Authenticator.get()
3868 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3869 message = (
3870 'This command is not designed for bot environment. It checks '
3871 '~/.gitcookies file not generally used on bots.')
3872 # TODO(crbug.com/1059384): Automatically detect when running on
3873 # cloudtop.
3874 if isinstance(authn, gerrit_util.GceAuthenticator):
3875 message += (
3876 '\n'
3877 'If you need to run this on GCE or a cloudtop instance, '
3878 'export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
3879 DieWithError(message)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003880
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003881 checker = _GitCookiesChecker()
3882 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003883
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003884 print('Your .netrc and .gitcookies have credentials for these hosts:')
3885 checker.print_current_creds(include_netrc=True)
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003886
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003887 if not checker.find_and_report_problems():
3888 print('\nNo problems detected in your .gitcookies file.')
3889 return 0
3890 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003891
3892
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003893@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003894def CMDbaseurl(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003895 """Gets or sets base-url for this branch."""
3896 _, args = parser.parse_args(args)
3897 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
3898 branch = scm.GIT.ShortBranchName(branchref)
3899 if not args:
3900 print('Current base-url:')
3901 return RunGit(['config', 'branch.%s.base-url' % branch],
3902 error_ok=False).strip()
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003903
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003904 print('Setting base-url to %s' % args[0])
3905 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3906 error_ok=False).strip()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003907
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003908
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003909def color_for_status(status):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003910 """Maps a Changelist status to color, for CMDstatus and other tools."""
3911 BOLD = '\033[1m'
3912 return {
3913 'unsent': BOLD + Fore.YELLOW,
3914 'waiting': BOLD + Fore.RED,
3915 'reply': BOLD + Fore.YELLOW,
3916 'not lgtm': BOLD + Fore.RED,
3917 'lgtm': BOLD + Fore.GREEN,
3918 'commit': BOLD + Fore.MAGENTA,
3919 'closed': BOLD + Fore.CYAN,
3920 'error': BOLD + Fore.WHITE,
3921 }.get(status, Fore.WHITE)
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003922
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003923
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003924def get_cl_statuses(changes, fine_grained, max_processes=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003925 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003926
3927 If fine_grained is true, this will fetch CL statuses from the server.
3928 Otherwise, simply indicate if there's a matching url for the given branches.
3929
3930 If max_processes is specified, it is used as the maximum number of processes
3931 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3932 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003933
3934 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003935 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003936 if not changes:
3937 return
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003938
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003939 if not fine_grained:
3940 # Fast path which doesn't involve querying codereview servers.
3941 # Do not use get_approving_reviewers(), since it requires an HTTP
3942 # request.
3943 for cl in changes:
3944 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
3945 return
3946
3947 # First, sort out authentication issues.
3948 logging.debug('ensuring credentials exist')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003949 for cl in changes:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003950 cl.EnsureAuthenticated(force=False, refresh=True)
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003951
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003952 def fetch(cl):
3953 try:
3954 return (cl, cl.GetStatus())
3955 except:
3956 # See http://crbug.com/629863.
3957 logging.exception('failed to fetch status for cl %s:',
3958 cl.GetIssue())
3959 raise
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003960
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003961 threads_count = len(changes)
3962 if max_processes:
3963 threads_count = max(1, min(threads_count, max_processes))
3964 logging.debug('querying %d CLs using %d threads', len(changes),
3965 threads_count)
3966
3967 pool = multiprocessing.pool.ThreadPool(threads_count)
3968 fetched_cls = set()
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003969 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003970 it = pool.imap_unordered(fetch, changes).__iter__()
3971 while True:
3972 try:
3973 cl, status = it.next(timeout=5)
3974 except (multiprocessing.TimeoutError, StopIteration):
3975 break
3976 fetched_cls.add(cl)
3977 yield cl, status
3978 finally:
3979 pool.close()
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003980
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003981 # Add any branches that failed to fetch.
3982 for cl in set(changes) - fetched_cls:
3983 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003984
rmistry@google.com2dd99862015-06-22 12:22:18 +00003985
Jose Lopes3863fc52020-04-07 17:00:25 +00003986def upload_branch_deps(cl, args, force=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003987 """Uploads CLs of local branches that are dependents of the current branch.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003988
3989 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003990
3991 test1 -> test2.1 -> test3.1
3992 -> test3.2
3993 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003994
3995 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3996 run on the dependent branches in this order:
3997 test2.1, test3.1, test3.2, test2.2, test3.3
3998
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003999 Note: This function does not rebase your local dependent branches. Use it
4000 when you make a change to the parent branch that will not conflict
4001 with its dependent branches, and you would like their dependencies
4002 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00004003 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004004 if git_common.is_dirty_git_tree('upload-branch-deps'):
4005 return 1
rmistry@google.com2dd99862015-06-22 12:22:18 +00004006
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004007 root_branch = cl.GetBranch()
4008 if root_branch is None:
4009 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4010 'Get on a branch!')
4011 if not cl.GetIssue():
4012 DieWithError(
4013 'Current branch does not have an uploaded CL. We cannot set '
4014 'patchset dependencies without an uploaded CL.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004015
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004016 branches = RunGit([
4017 'for-each-ref', '--format=%(refname:short) %(upstream:short)',
4018 'refs/heads'
4019 ])
4020 if not branches:
4021 print('No local branches found.')
4022 return 0
rmistry@google.com2dd99862015-06-22 12:22:18 +00004023
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004024 # Create a dictionary of all local branches to the branches that are
4025 # dependent on it.
4026 tracked_to_dependents = collections.defaultdict(list)
4027 for b in branches.splitlines():
4028 tokens = b.split()
4029 if len(tokens) == 2:
4030 branch_name, tracked = tokens
4031 tracked_to_dependents[tracked].append(branch_name)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004032
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004033 print()
4034 print('The dependent local branches of %s are:' % root_branch)
4035 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004036
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004037 def traverse_dependents_preorder(branch, padding=''):
4038 dependents_to_process = tracked_to_dependents.get(branch, [])
4039 padding += ' '
4040 for dependent in dependents_to_process:
4041 print('%s%s' % (padding, dependent))
4042 dependents.append(dependent)
4043 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004044
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004045 traverse_dependents_preorder(root_branch)
4046 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004047
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004048 if not dependents:
4049 print('There are no dependent local branches for %s' % root_branch)
4050 return 0
rmistry@google.com2dd99862015-06-22 12:22:18 +00004051
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004052 # Record all dependents that failed to upload.
4053 failures = {}
4054 # Go through all dependents, checkout the branch and upload.
4055 try:
4056 for dependent_branch in dependents:
4057 print()
4058 print('--------------------------------------')
4059 print('Running "git cl upload" from %s:' % dependent_branch)
4060 RunGit(['checkout', '-q', dependent_branch])
4061 print()
4062 try:
4063 if CMDupload(OptionParser(), args) != 0:
4064 print('Upload failed for %s!' % dependent_branch)
4065 failures[dependent_branch] = 1
4066 except: # pylint: disable=bare-except
4067 failures[dependent_branch] = 1
4068 print()
4069 finally:
4070 # Swap back to the original root branch.
4071 RunGit(['checkout', '-q', root_branch])
4072
4073 print()
4074 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004075 for dependent_branch in dependents:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004076 upload_status = 'failed' if failures.get(
4077 dependent_branch) else 'succeeded'
4078 print(' %s : %s' % (dependent_branch, upload_status))
4079 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004080
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004081 return 0
rmistry@google.com2dd99862015-06-22 12:22:18 +00004082
4083
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00004084def GetArchiveTagForBranch(issue_num, branch_name, existing_tags, pattern):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004085 """Given a proposed tag name, returns a tag name that is guaranteed to be
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00004086 unique. If 'foo' is proposed but already exists, then 'foo-2' is used,
4087 or 'foo-3', and so on."""
4088
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004089 proposed_tag = pattern.format(**{'issue': issue_num, 'branch': branch_name})
4090 for suffix_num in itertools.count(1):
4091 if suffix_num == 1:
4092 to_check = proposed_tag
4093 else:
4094 to_check = '%s-%d' % (proposed_tag, suffix_num)
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00004095
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004096 if to_check not in existing_tags:
4097 return to_check
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00004098
4099
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004100@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004101def CMDarchive(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004102 """Archives and deletes branches associated with closed changelists."""
4103 parser.add_option(
4104 '-j',
4105 '--maxjobs',
4106 action='store',
4107 type=int,
4108 help='The maximum number of jobs to use when retrieving review status.')
4109 parser.add_option('-f',
4110 '--force',
4111 action='store_true',
4112 help='Bypasses the confirmation prompt.')
4113 parser.add_option('-d',
4114 '--dry-run',
4115 action='store_true',
4116 help='Skip the branch tagging and removal steps.')
4117 parser.add_option('-t',
4118 '--notags',
4119 action='store_true',
4120 help='Do not tag archived branches. '
4121 'Note: local commit history may be lost.')
4122 parser.add_option('-p',
4123 '--pattern',
4124 default='git-cl-archived-{issue}-{branch}',
4125 help='Format string for archive tags. '
4126 'E.g. \'archived-{issue}-{branch}\'.')
kmarshall3bff56b2016-06-06 18:31:47 -07004127
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004128 options, args = parser.parse_args(args)
4129 if args:
4130 parser.error('Unsupported args: %s' % ' '.join(args))
kmarshall3bff56b2016-06-06 18:31:47 -07004131
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004132 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4133 if not branches:
4134 return 0
4135
4136 tags = RunGit(['for-each-ref', '--format=%(refname)', 'refs/tags'
4137 ]).splitlines() or []
4138 tags = [t.split('/')[-1] for t in tags]
4139
4140 print('Finding all branches associated with closed issues...')
4141 changes = [Changelist(branchref=b) for b in branches.splitlines()]
4142 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4143 statuses = get_cl_statuses(changes,
4144 fine_grained=True,
4145 max_processes=options.maxjobs)
4146 proposal = [(cl.GetBranch(),
4147 GetArchiveTagForBranch(cl.GetIssue(), cl.GetBranch(), tags,
4148 options.pattern))
4149 for cl, status in statuses
4150 if status in ('closed', 'rietveld-not-supported')]
4151 proposal.sort()
4152
4153 if not proposal:
4154 print('No branches with closed codereview issues found.')
4155 return 0
4156
4157 current_branch = scm.GIT.GetBranch(settings.GetRoot())
4158
4159 print('\nBranches with closed issues that will be archived:\n')
4160 if options.notags:
4161 for next_item in proposal:
4162 print(' ' + next_item[0])
4163 else:
4164 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4165 for next_item in proposal:
4166 print('%*s %s' % (alignment, next_item[0], next_item[1]))
4167
4168 # Quit now on precondition failure or if instructed by the user, either
4169 # via an interactive prompt or by command line flags.
4170 if options.dry_run:
4171 print('\nNo changes were made (dry run).\n')
4172 return 0
4173
4174 if any(branch == current_branch for branch, _ in proposal):
4175 print('You are currently on a branch \'%s\' which is associated with a '
4176 'closed codereview issue, so archive cannot proceed. Please '
4177 'checkout another branch and run this command again.' %
4178 current_branch)
4179 return 1
4180
4181 if not options.force:
4182 answer = gclient_utils.AskForData(
4183 '\nProceed with deletion (Y/n)? ').lower()
4184 if answer not in ('y', ''):
4185 print('Aborted.')
4186 return 1
4187
4188 for branch, tagname in proposal:
4189 if not options.notags:
4190 RunGit(['tag', tagname, branch])
4191
4192 if RunGitWithCode(['branch', '-D', branch])[0] != 0:
4193 # Clean up the tag if we failed to delete the branch.
4194 RunGit(['tag', '-d', tagname])
4195
4196 print('\nJob\'s done!')
4197
kmarshall3bff56b2016-06-06 18:31:47 -07004198 return 0
4199
kmarshall3bff56b2016-06-06 18:31:47 -07004200
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004201@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004202def CMDstatus(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004203 """Show status of changelists.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004204
4205 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004206 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004207 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004208 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004209 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004210 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004211 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004212 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004213
4214 Also see 'git cl comments'.
4215 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004216 parser.add_option('--no-branch-color',
4217 action='store_true',
4218 help='Disable colorized branch names')
4219 parser.add_option(
4220 '--field', help='print only specific field (desc|id|patch|status|url)')
4221 parser.add_option('-f',
4222 '--fast',
4223 action='store_true',
4224 help='Do not retrieve review status')
4225 parser.add_option(
4226 '-j',
4227 '--maxjobs',
4228 action='store',
4229 type=int,
4230 help='The maximum number of jobs to use when retrieving review status')
4231 parser.add_option(
4232 '-i',
4233 '--issue',
4234 type=int,
4235 help='Operate on this issue instead of the current branch\'s implicit '
4236 'issue. Requires --field to be set.')
4237 parser.add_option('-d',
4238 '--date-order',
4239 action='store_true',
4240 help='Order branches by committer date.')
4241 options, args = parser.parse_args(args)
4242 if args:
4243 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004244
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004245 if options.issue is not None and not options.field:
4246 parser.error('--field must be given when --issue is set.')
iannucci3c972b92016-08-17 13:24:10 -07004247
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004248 if options.field:
4249 cl = Changelist(issue=options.issue)
4250 if options.field.startswith('desc'):
4251 if cl.GetIssue():
4252 print(cl.FetchDescription())
4253 elif options.field == 'id':
4254 issueid = cl.GetIssue()
4255 if issueid:
4256 print(issueid)
4257 elif options.field == 'patch':
4258 patchset = cl.GetMostRecentPatchset()
4259 if patchset:
4260 print(patchset)
4261 elif options.field == 'status':
4262 print(cl.GetStatus())
4263 elif options.field == 'url':
4264 url = cl.GetIssueURL()
4265 if url:
4266 print(url)
4267 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004268
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004269 branches = RunGit([
4270 'for-each-ref', '--format=%(refname) %(committerdate:unix)',
4271 'refs/heads'
4272 ])
4273 if not branches:
4274 print('No local branch found.')
4275 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004276
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004277 changes = [
4278 Changelist(branchref=b, commit_date=ct)
4279 for b, ct in map(lambda line: line.split(' '), branches.splitlines())
4280 ]
4281 print('Branches associated with reviews:')
4282 output = get_cl_statuses(changes,
4283 fine_grained=not options.fast,
4284 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004285
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004286 current_branch = scm.GIT.GetBranch(settings.GetRoot())
Daniel McArdlea23bf592019-02-12 00:25:12 +00004287
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004288 def FormatBranchName(branch, colorize=False):
4289 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
Daniel McArdlea23bf592019-02-12 00:25:12 +00004290 an asterisk when it is the current branch."""
4291
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004292 asterisk = ""
4293 color = Fore.RESET
4294 if branch == current_branch:
4295 asterisk = "* "
4296 color = Fore.GREEN
4297 branch_name = scm.GIT.ShortBranchName(branch)
Daniel McArdlea23bf592019-02-12 00:25:12 +00004298
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004299 if colorize:
4300 return asterisk + color + branch_name + Fore.RESET
4301 return asterisk + branch_name
Daniel McArdle452a49f2019-02-14 17:28:31 +00004302
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004303 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00004304
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004305 alignment = max(5,
4306 max(len(FormatBranchName(c.GetBranch())) for c in changes))
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +00004307
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004308 if options.date_order or settings.IsStatusCommitOrderByDate():
4309 sorted_changes = sorted(changes,
4310 key=lambda c: c.GetCommitDate(),
4311 reverse=True)
4312 else:
4313 sorted_changes = sorted(changes, key=lambda c: c.GetBranch())
4314 for cl in sorted_changes:
4315 branch = cl.GetBranch()
4316 while branch not in branch_statuses:
4317 c, status = next(output)
4318 branch_statuses[c.GetBranch()] = status
4319 status = branch_statuses.pop(branch)
4320 url = cl.GetIssueURL(short=True)
4321 if url and (not status or status == 'error'):
4322 # The issue probably doesn't exist anymore.
4323 url += ' (broken)'
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004324
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004325 color = color_for_status(status)
4326 # Turn off bold as well as colors.
4327 END = '\033[0m'
4328 reset = Fore.RESET + END
4329 if not setup_color.IS_TTY:
4330 color = ''
4331 reset = ''
4332 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00004333
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004334 branch_display = FormatBranchName(branch)
4335 padding = ' ' * (alignment - len(branch_display))
4336 if not options.no_branch_color:
4337 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00004338
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004339 print(' %s : %s%s %s%s' %
4340 (padding + branch_display, color, url, status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004341
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004342 print()
4343 print('Current branch: %s' % current_branch)
4344 for cl in changes:
4345 if cl.GetBranch() == current_branch:
4346 break
4347 if not cl.GetIssue():
4348 print('No issue assigned.')
4349 return 0
4350 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4351 if not options.fast:
4352 print('Issue description:')
4353 print(cl.FetchDescription(pretty=True))
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004354 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004355
4356
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004357def colorize_CMDstatus_doc():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004358 """To be called once in main() to add colors to git cl status help."""
4359 colors = [i for i in dir(Fore) if i[0].isupper()]
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004360
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004361 def colorize_line(line):
4362 for color in colors:
4363 if color in line.upper():
4364 # Extract whitespace first and the leading '-'.
4365 indent = len(line) - len(line.lstrip(' ')) + 1
4366 return line[:indent] + getattr(
4367 Fore, color) + line[indent:] + Fore.RESET
4368 return line
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004369
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004370 lines = CMDstatus.__doc__.splitlines()
4371 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004372
4373
phajdan.jre328cf92016-08-22 04:12:17 -07004374def write_json(path, contents):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004375 if path == '-':
4376 json.dump(contents, sys.stdout)
4377 else:
4378 with open(path, 'w') as f:
4379 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004380
4381
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004382@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004383@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004384def CMDissue(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004385 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004386
4387 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004388 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004389 parser.add_option('-r',
4390 '--reverse',
4391 action='store_true',
4392 help='Lookup the branch(es) for the specified issues. If '
4393 'no issues are specified, all branches with mapped '
4394 'issues will be listed.')
4395 parser.add_option('--json',
4396 help='Path to JSON output file, or "-" for stdout.')
4397 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004398
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004399 if options.reverse:
4400 branches = RunGit(['for-each-ref', 'refs/heads',
4401 '--format=%(refname)']).splitlines()
4402 # Reverse issue lookup.
4403 issue_branch_map = {}
Arthur Milchior801a9752023-04-07 10:33:54 +00004404
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004405 git_config = {}
4406 for config in RunGit(['config', '--get-regexp',
4407 r'branch\..*issue']).splitlines():
4408 name, _space, val = config.partition(' ')
4409 git_config[name] = val
Arthur Milchior801a9752023-04-07 10:33:54 +00004410
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004411 for branch in branches:
4412 issue = git_config.get(
4413 'branch.%s.%s' %
4414 (scm.GIT.ShortBranchName(branch), ISSUE_CONFIG_KEY))
4415 if issue:
4416 issue_branch_map.setdefault(int(issue), []).append(branch)
4417 if not args:
4418 args = sorted(issue_branch_map.keys())
4419 result = {}
4420 for issue in args:
4421 try:
4422 issue_num = int(issue)
4423 except ValueError:
4424 print('ERROR cannot parse issue number: %s' % issue,
4425 file=sys.stderr)
4426 continue
4427 result[issue_num] = issue_branch_map.get(issue_num)
4428 print('Branch for issue number %s: %s' % (issue, ', '.join(
4429 issue_branch_map.get(issue_num) or ('None', ))))
4430 if options.json:
4431 write_json(options.json, result)
4432 return 0
4433
4434 if len(args) > 0:
4435 issue = ParseIssueNumberArgument(args[0])
4436 if not issue.valid:
4437 DieWithError(
4438 'Pass a url or number to set the issue, 0 to unset it, '
4439 'or no argument to list it.\n'
4440 'Maybe you want to run git cl status?')
4441 cl = Changelist()
4442 cl.SetIssue(issue.issue)
4443 else:
4444 cl = Changelist()
4445 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
Arthur Milchior801a9752023-04-07 10:33:54 +00004446 if options.json:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004447 write_json(
4448 options.json, {
4449 'gerrit_host': cl.GetGerritHost(),
4450 'gerrit_project': cl.GetGerritProject(),
4451 'issue_url': cl.GetIssueURL(),
4452 'issue': cl.GetIssue(),
4453 })
Arthur Milchior801a9752023-04-07 10:33:54 +00004454 return 0
Aaron Gable78753da2017-06-15 10:35:49 -07004455
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004456
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004457@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004458def CMDcomments(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004459 """Shows or posts review comments for any changelist."""
4460 parser.add_option('-a',
4461 '--add-comment',
4462 dest='comment',
4463 help='comment to add to an issue')
4464 parser.add_option('-p',
4465 '--publish',
4466 action='store_true',
4467 help='marks CL as ready and sends comment to reviewers')
4468 parser.add_option('-i',
4469 '--issue',
4470 dest='issue',
4471 help='review issue id (defaults to current issue).')
4472 parser.add_option('-m',
4473 '--machine-readable',
4474 dest='readable',
4475 action='store_false',
4476 default=True,
4477 help='output comments in a format compatible with '
4478 'editor parsing')
4479 parser.add_option('-j',
4480 '--json-file',
4481 help='File to write JSON summary to, or "-" for stdout')
4482 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004483
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004484 issue = None
4485 if options.issue:
4486 try:
4487 issue = int(options.issue)
4488 except ValueError:
4489 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004490
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004491 cl = Changelist(issue=issue)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004492
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004493 if options.comment:
4494 cl.AddComment(options.comment, options.publish)
4495 return 0
4496
4497 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4498 key=lambda c: c.date)
4499 for comment in summary:
4500 if comment.disapproval:
4501 color = Fore.RED
4502 elif comment.approval:
4503 color = Fore.GREEN
4504 elif comment.sender == cl.GetIssueOwner():
4505 color = Fore.MAGENTA
4506 elif comment.autogenerated:
4507 color = Fore.CYAN
4508 else:
4509 color = Fore.BLUE
4510 print('\n%s%s %s%s\n%s' %
4511 (color, comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4512 comment.sender, Fore.RESET, '\n'.join(
4513 ' ' + l for l in comment.message.strip().splitlines())))
4514
4515 if options.json_file:
4516
4517 def pre_serialize(c):
4518 dct = c._asdict().copy()
4519 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4520 return dct
4521
4522 write_json(options.json_file, [pre_serialize(x) for x in summary])
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004523 return 0
4524
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004525
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004526@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004527@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004528def CMDdescription(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004529 """Brings up the editor for the current CL's description."""
4530 parser.add_option(
4531 '-d',
4532 '--display',
4533 action='store_true',
4534 help='Display the description instead of opening an editor')
4535 parser.add_option(
4536 '-n',
4537 '--new-description',
4538 help='New description to set for this issue (- for stdin, '
4539 '+ to load from local commit HEAD)')
4540 parser.add_option('-f',
4541 '--force',
4542 action='store_true',
4543 help='Delete any unpublished Gerrit edits for this issue '
4544 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004545
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004546 options, args = parser.parse_args(args)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004547
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004548 target_issue_arg = None
4549 if len(args) > 0:
4550 target_issue_arg = ParseIssueNumberArgument(args[0])
4551 if not target_issue_arg.valid:
4552 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004553
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004554 kwargs = {}
4555 if target_issue_arg:
4556 kwargs['issue'] = target_issue_arg.issue
4557 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07004558
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004559 cl = Changelist(**kwargs)
4560 if not cl.GetIssue():
4561 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004562
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004563 if args and not args[0].isdigit():
4564 logging.info('canonical issue/change URL: %s\n', cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004565
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004566 description = ChangeDescription(cl.FetchDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004567
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004568 if options.display:
4569 print(description.description)
4570 return 0
4571
4572 if options.new_description:
4573 text = options.new_description
4574 if text == '-':
4575 text = '\n'.join(l.rstrip() for l in sys.stdin)
4576 elif text == '+':
4577 base_branch = cl.GetCommonAncestorWithUpstream()
4578 text = _create_description_from_log([base_branch])
4579
4580 description.set_description(text)
4581 else:
4582 description.prompt()
4583 if cl.FetchDescription().strip() != description.description:
4584 cl.UpdateDescription(description.description, force=options.force)
smut@google.com34fb6b12015-07-13 20:03:26 +00004585 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004586
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004587
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004588@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004589def CMDlint(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004590 """Runs cpplint on the current changelist."""
4591 parser.add_option(
4592 '--filter',
4593 action='append',
4594 metavar='-x,+y',
4595 help='Comma-separated list of cpplint\'s category-filters')
4596 options, args = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004597
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004598 # Access to a protected member _XX of a client class
4599 # pylint: disable=protected-access
4600 try:
4601 import cpplint
4602 import cpplint_chromium
4603 except ImportError:
4604 print(
4605 'Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.'
4606 )
4607 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004608
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004609 # Change the current working directory before calling lint so that it
4610 # shows the correct base.
4611 previous_cwd = os.getcwd()
4612 os.chdir(settings.GetRoot())
4613 try:
4614 cl = Changelist()
4615 files = cl.GetAffectedFiles(cl.GetCommonAncestorWithUpstream())
4616 if not files:
4617 print('Cannot lint an empty CL')
4618 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004619
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004620 # Process cpplint arguments, if any.
4621 filters = presubmit_canned_checks.GetCppLintFilters(options.filter)
4622 command = ['--filter=' + ','.join(filters)]
4623 command.extend(args)
4624 command.extend(files)
4625 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004626
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004627 include_regex = re.compile(settings.GetLintRegex())
4628 ignore_regex = re.compile(settings.GetLintIgnoreRegex())
4629 extra_check_functions = [
4630 cpplint_chromium.CheckPointerDeclarationWhitespace
4631 ]
4632 for filename in filenames:
4633 if not include_regex.match(filename):
4634 print('Skipping file %s' % filename)
4635 continue
Lei Zhang379d1ad2020-07-15 19:40:06 +00004636
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004637 if ignore_regex.match(filename):
4638 print('Ignoring file %s' % filename)
4639 continue
Lei Zhang379d1ad2020-07-15 19:40:06 +00004640
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004641 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4642 extra_check_functions)
4643 finally:
4644 os.chdir(previous_cwd)
4645 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
4646 if cpplint._cpplint_state.error_count != 0:
4647 return 1
4648 return 0
thestig@chromium.org44202a22014-03-11 19:22:18 +00004649
4650
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004651@metrics.collector.collect_metrics('git cl presubmit')
mlcuic601e362023-08-14 23:39:46 +00004652@subcommand.usage('[base branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004653def CMDpresubmit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004654 """Runs presubmit tests on the current changelist."""
4655 parser.add_option('-u',
4656 '--upload',
4657 action='store_true',
4658 help='Run upload hook instead of the push hook')
4659 parser.add_option('-f',
4660 '--force',
4661 action='store_true',
4662 help='Run checks even if tree is dirty')
4663 parser.add_option(
4664 '--all',
4665 action='store_true',
4666 help='Run checks against all files, not just modified ones')
4667 parser.add_option('--files',
4668 nargs=1,
4669 help='Semicolon-separated list of files to be marked as '
4670 'modified when executing presubmit or post-upload hooks. '
4671 'fnmatch wildcards can also be used.')
4672 parser.add_option(
4673 '--parallel',
4674 action='store_true',
4675 help='Run all tests specified by input_api.RunTests in all '
4676 'PRESUBMIT files in parallel.')
4677 parser.add_option('--resultdb',
4678 action='store_true',
4679 help='Run presubmit checks in the ResultSink environment '
4680 'and send results to the ResultDB database.')
4681 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
4682 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004683
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004684 if not options.force and git_common.is_dirty_git_tree('presubmit'):
4685 print('use --force to check even if tree is dirty.')
4686 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004687
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004688 cl = Changelist()
4689 if args:
4690 base_branch = args[0]
Bruce Dawsoneb8426e2022-08-05 23:58:15 +00004691 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004692 # Default to diffing against the common ancestor of the upstream branch.
4693 base_branch = cl.GetCommonAncestorWithUpstream()
Aaron Gable8076c282017-11-29 14:39:41 -08004694
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004695 start = time.time()
4696 try:
4697 if not 'PRESUBMIT_SKIP_NETWORK' in os.environ and cl.GetIssue():
4698 description = cl.FetchDescription()
4699 else:
4700 description = _create_description_from_log([base_branch])
4701 except Exception as e:
4702 print('Failed to fetch CL description - %s' % str(e))
4703 description = _create_description_from_log([base_branch])
4704 elapsed = time.time() - start
4705 if elapsed > 5:
4706 print('%.1f s to get CL description.' % elapsed)
Bruce Dawson13acea32022-05-03 22:13:08 +00004707
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004708 if not base_branch:
4709 if not options.force:
4710 print('use --force to check even when not on a branch.')
4711 return 1
4712 base_branch = 'HEAD'
4713
4714 cl.RunHook(committing=not options.upload,
4715 may_prompt=False,
4716 verbose=options.verbose,
4717 parallel=options.parallel,
4718 upstream=base_branch,
4719 description=description,
4720 all_files=options.all,
4721 files=options.files,
4722 resultdb=options.resultdb,
4723 realm=options.realm)
4724 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004725
4726
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004727def GenerateGerritChangeId(message):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004728 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004729
4730 Works the same way as
4731 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4732 but can be called on demand on all platforms.
4733
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004734 The basic idea is to generate git hash of a state of the tree, original
4735 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004736 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004737 lines = []
4738 tree_hash = RunGitSilent(['write-tree'])
4739 lines.append('tree %s' % tree_hash.strip())
4740 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'],
4741 suppress_stderr=False)
4742 if code == 0:
4743 lines.append('parent %s' % parent.strip())
4744 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4745 lines.append('author %s' % author.strip())
4746 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4747 lines.append('committer %s' % committer.strip())
4748 lines.append('')
4749 # Note: Gerrit's commit-hook actually cleans message of some lines and
4750 # whitespace. This code is not doing this, but it clearly won't decrease
4751 # entropy.
4752 lines.append(message)
4753 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4754 stdin=('\n'.join(lines)).encode())
4755 return 'I%s' % change_hash.strip()
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004756
4757
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004758def GetTargetRef(remote, remote_branch, target_branch):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004759 """Computes the remote branch ref to use for the CL.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004760
4761 Args:
4762 remote (str): The git remote for the CL.
4763 remote_branch (str): The git remote branch for the CL.
4764 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004765 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004766 if not (remote and remote_branch):
4767 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004768
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004769 if target_branch:
4770 # Canonicalize branch references to the equivalent local full symbolic
4771 # refs, which are then translated into the remote full symbolic refs
4772 # below.
4773 if '/' not in target_branch:
4774 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4775 else:
4776 prefix_replacements = (
4777 ('^((refs/)?remotes/)?branch-heads/',
4778 'refs/remotes/branch-heads/'),
4779 ('^((refs/)?remotes/)?%s/' % remote,
4780 'refs/remotes/%s/' % remote),
4781 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4782 )
4783 match = None
4784 for regex, replacement in prefix_replacements:
4785 match = re.search(regex, target_branch)
4786 if match:
4787 remote_branch = target_branch.replace(
4788 match.group(0), replacement)
4789 break
4790 if not match:
4791 # This is a branch path but not one we recognize; use as-is.
4792 remote_branch = target_branch
4793 # pylint: disable=consider-using-get
4794 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4795 # pylint: enable=consider-using-get
4796 # Handle the refs that need to land in different refs.
4797 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004798
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004799 # Create the true path to the remote branch.
4800 # Does the following translation:
4801 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4802 # * refs/remotes/origin/main -> refs/heads/main
4803 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4804 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4805 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4806 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4807 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4808 'refs/heads/')
4809 elif remote_branch.startswith('refs/remotes/branch-heads'):
4810 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004811
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004812 return remote_branch
wittman@chromium.org455dc922015-01-26 20:15:50 +00004813
4814
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004815def cleanup_list(l):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004816 """Fixes a list so that comma separated items are put as individual items.
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004817
4818 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4819 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4820 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004821 items = sum((i.split(',') for i in l), [])
4822 stripped_items = (i.strip() for i in items)
4823 return sorted(filter(None, stripped_items))
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004824
4825
Aaron Gable4db38df2017-11-03 14:59:07 -07004826@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004827@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004828def CMDupload(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004829 """Uploads the current changelist to codereview.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004830
4831 Can skip dependency patchset uploads for a branch by running:
4832 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004833 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00004834 git config --unset branch.branch_name.skip-deps-uploads
4835 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004836
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004837 If the name of the checked out branch starts with "bug-" or "fix-" followed
4838 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02004839 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004840
4841 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004842 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004843 [git-cl] add support for hashtags
4844 Foo bar: implement foo
4845 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004846 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004847 parser.add_option('--bypass-hooks',
4848 action='store_true',
4849 dest='bypass_hooks',
4850 help='bypass upload presubmit hook')
4851 parser.add_option('--bypass-watchlists',
4852 action='store_true',
4853 dest='bypass_watchlists',
4854 help='bypass watchlists auto CC-ing reviewers')
4855 parser.add_option('-f',
4856 '--force',
4857 action='store_true',
4858 dest='force',
4859 help="force yes to questions (don't prompt)")
4860 parser.add_option('--message',
4861 '-m',
4862 dest='message',
4863 help='message for patchset')
4864 parser.add_option('-b',
4865 '--bug',
4866 help='pre-populate the bug number(s) for this issue. '
4867 'If several, separate with commas')
4868 parser.add_option('--message-file',
4869 dest='message_file',
4870 help='file which contains message for patchset')
4871 parser.add_option('--title', '-t', dest='title', help='title for patchset')
4872 parser.add_option('-T',
4873 '--skip-title',
4874 action='store_true',
4875 dest='skip_title',
4876 help='Use the most recent commit message as the title of '
4877 'the patchset')
4878 parser.add_option('-r',
4879 '--reviewers',
4880 action='append',
4881 default=[],
4882 help='reviewer email addresses')
4883 parser.add_option('--cc',
4884 action='append',
4885 default=[],
4886 help='cc email addresses')
4887 parser.add_option('--hashtag',
4888 dest='hashtags',
4889 action='append',
4890 default=[],
4891 help=('Gerrit hashtag for new CL; '
4892 'can be applied multiple times'))
4893 parser.add_option('-s',
4894 '--send-mail',
4895 '--send-email',
4896 dest='send_mail',
4897 action='store_true',
4898 help='send email to reviewer(s) and cc(s) immediately')
4899 parser.add_option('--target_branch',
4900 '--target-branch',
4901 metavar='TARGET',
4902 help='Apply CL to remote ref TARGET. ' +
4903 'Default: remote branch head, or main')
4904 parser.add_option('--squash',
4905 action='store_true',
4906 help='Squash multiple commits into one')
4907 parser.add_option('--no-squash',
4908 action='store_false',
4909 dest='squash',
4910 help='Don\'t squash multiple commits into one')
4911 parser.add_option('--topic',
4912 default=None,
4913 help='Topic to specify when uploading')
4914 parser.add_option('--r-owners',
4915 dest='add_owners_to',
4916 action='store_const',
4917 const='R',
4918 help='add a set of OWNERS to R')
4919 parser.add_option('-c',
4920 '--use-commit-queue',
4921 action='store_true',
4922 default=False,
4923 help='tell the CQ to commit this patchset; '
4924 'implies --send-mail')
4925 parser.add_option('-d',
4926 '--cq-dry-run',
4927 action='store_true',
4928 default=False,
4929 help='Send the patchset to do a CQ dry run right after '
4930 'upload.')
4931 parser.add_option('--set-bot-commit',
4932 action='store_true',
4933 help=optparse.SUPPRESS_HELP)
4934 parser.add_option('--preserve-tryjobs',
4935 action='store_true',
4936 help='instruct the CQ to let tryjobs running even after '
4937 'new patchsets are uploaded instead of canceling '
4938 'prior patchset\' tryjobs')
4939 parser.add_option(
4940 '--dependencies',
4941 action='store_true',
4942 help='Uploads CLs of all the local branches that depend on '
4943 'the current branch')
4944 parser.add_option(
4945 '-a',
Thiago Perrotta853b29f2023-09-27 14:51:20 +00004946 '--auto-submit',
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004947 '--enable-auto-submit',
4948 action='store_true',
Thiago Perrotta853b29f2023-09-27 14:51:20 +00004949 dest='enable_auto_submit',
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004950 help='Sends your change to the CQ after an approval. Only '
4951 'works on repos that have the Auto-Submit label '
4952 'enabled')
4953 parser.add_option(
4954 '--parallel',
4955 action='store_true',
4956 help='Run all tests specified by input_api.RunTests in all '
4957 'PRESUBMIT files in parallel.')
4958 parser.add_option('--no-autocc',
4959 action='store_true',
4960 help='Disables automatic addition of CC emails')
4961 parser.add_option('--private',
4962 action='store_true',
4963 help='Set the review private. This implies --no-autocc.')
4964 parser.add_option('-R',
4965 '--retry-failed',
4966 action='store_true',
4967 help='Retry failed tryjobs from old patchset immediately '
4968 'after uploading new patchset. Cannot be used with '
4969 '--use-commit-queue or --cq-dry-run.')
4970 parser.add_option('--fixed',
4971 '-x',
4972 help='List of bugs that will be commented on and marked '
4973 'fixed (pre-populates "Fixed:" tag). Same format as '
4974 '-b option / "Bug:" tag. If fixing several issues, '
4975 'separate with commas.')
4976 parser.add_option('--edit-description',
4977 action='store_true',
4978 default=False,
4979 help='Modify description before upload. Cannot be used '
4980 'with --force. It is a noop when --no-squash is set '
4981 'or a new commit is created.')
4982 parser.add_option('--git-completion-helper',
4983 action="store_true",
4984 help=optparse.SUPPRESS_HELP)
4985 parser.add_option('-o',
4986 '--push-options',
4987 action='append',
4988 default=[],
4989 help='Transmit the given string to the server when '
4990 'performing git push (pass-through). See git-push '
4991 'documentation for more details.')
4992 parser.add_option('--no-add-changeid',
4993 action='store_true',
4994 dest='no_add_changeid',
4995 help='Do not add change-ids to messages.')
4996 parser.add_option('--cherry-pick-stacked',
4997 '--cp',
4998 dest='cherry_pick_stacked',
4999 action='store_true',
5000 help='If parent branch has un-uploaded updates, '
5001 'automatically skip parent branches and just upload '
5002 'the current branch cherry-pick on its parent\'s last '
5003 'uploaded commit. Allows users to skip the potential '
5004 'interactive confirmation step.')
5005 # TODO(b/265929888): Add --wip option of --cl-status option.
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00005006
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005007 orig_args = args
5008 (options, args) = parser.parse_args(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005009
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005010 if options.git_completion_helper:
5011 print(' '.join(opt.get_opt_string() for opt in parser.option_list
5012 if opt.help != optparse.SUPPRESS_HELP))
5013 return
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00005014
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005015 # TODO(crbug.com/1475405): Warn users if the project uses submodules and
5016 # they have fsmonitor enabled.
5017 if os.path.isfile('.gitmodules'):
5018 git_common.warn_submodule()
Aravind Vasudevanb8164182023-08-25 21:49:12 +00005019
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005020 if git_common.is_dirty_git_tree('upload'):
5021 return 1
ukai@chromium.orge8077812012-02-03 03:41:46 +00005022
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005023 options.reviewers = cleanup_list(options.reviewers)
5024 options.cc = cleanup_list(options.cc)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005025
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005026 if options.edit_description and options.force:
5027 parser.error('Only one of --force and --edit-description allowed')
Josipe827b0f2020-01-30 00:07:20 +00005028
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005029 if options.message_file:
5030 if options.message:
5031 parser.error('Only one of --message and --message-file allowed.')
5032 options.message = gclient_utils.FileRead(options.message_file)
tandriib80458a2016-06-23 12:20:07 -07005033
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005034 if ([options.cq_dry_run, options.use_commit_queue, options.retry_failed
5035 ].count(True) > 1):
5036 parser.error('Only one of --use-commit-queue, --cq-dry-run or '
5037 '--retry-failed is allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07005038
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005039 if options.skip_title and options.title:
5040 parser.error('Only one of --title and --skip-title allowed.')
Mario Bianuccicebfb4e2020-07-22 23:08:16 +00005041
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005042 if options.use_commit_queue:
5043 options.send_mail = True
Aaron Gableedbc4132017-09-11 13:22:28 -07005044
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005045 if options.squash is None:
5046 # Load default for user, repo, squash=true, in this order.
5047 options.squash = settings.GetSquashGerritUploads()
Edward Lesmes0dd54822020-03-26 18:24:25 +00005048
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005049 cl = Changelist(branchref=options.target_branch)
Joanna Wang5051ffe2023-03-01 22:24:07 +00005050
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005051 # Warm change details cache now to avoid RPCs later, reducing latency for
5052 # developers.
5053 if cl.GetIssue():
5054 cl._GetChangeDetail([
5055 'DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'
5056 ])
Joanna Wang5051ffe2023-03-01 22:24:07 +00005057
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005058 if options.retry_failed and not cl.GetIssue():
5059 print('No previous patchsets, so --retry-failed has no effect.')
5060 options.retry_failed = False
Joanna Wang5051ffe2023-03-01 22:24:07 +00005061
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005062 disable_dogfood_stacked_changes = os.environ.get(
5063 DOGFOOD_STACKED_CHANGES_VAR) == '0'
5064 dogfood_stacked_changes = os.environ.get(DOGFOOD_STACKED_CHANGES_VAR) == '1'
Joanna Wang5051ffe2023-03-01 22:24:07 +00005065
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005066 # Only print message for folks who don't have DOGFOOD_STACKED_CHANGES set
5067 # to an expected value.
5068 if (options.squash and not dogfood_stacked_changes
5069 and not disable_dogfood_stacked_changes):
5070 print(
5071 'This repo has been enrolled in the stacked changes dogfood.\n'
5072 '`git cl upload` now uploads the current branch and all upstream '
5073 'branches that have un-uploaded updates.\n'
5074 'Patches can now be reapplied with --force:\n'
5075 '`git cl patch --reapply --force`.\n'
5076 'Googlers may visit go/stacked-changes-dogfood for more information.\n'
5077 '\n'
5078 'Depot Tools no longer sets new uploads to "WIP". Please update the\n'
5079 '"Set new changes to "work in progress" by default" checkbox at\n'
5080 'https://<host>-review.googlesource.com/settings/\n'
5081 '\n'
5082 'To opt-out use `export DOGFOOD_STACKED_CHANGES=0`.\n'
5083 'To hide this message use `export DOGFOOD_STACKED_CHANGES=1`.\n'
5084 'File bugs at https://bit.ly/3Y6opoI\n')
Joanna Wang4786a412023-05-16 18:23:08 +00005085
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005086 if options.squash and not disable_dogfood_stacked_changes:
5087 if options.dependencies:
5088 parser.error(
5089 '--dependencies is not available for this dogfood workflow.')
Joanna Wang5051ffe2023-03-01 22:24:07 +00005090
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005091 if options.cherry_pick_stacked:
5092 try:
5093 orig_args.remove('--cherry-pick-stacked')
5094 except ValueError:
5095 orig_args.remove('--cp')
5096 UploadAllSquashed(options, orig_args)
5097 return 0
Joanna Wang18de1f62023-01-21 01:24:24 +00005098
Joanna Wangd75fc882023-03-01 21:53:34 +00005099 if options.cherry_pick_stacked:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005100 parser.error(
5101 '--cherry-pick-stacked is not available for this workflow.')
Joanna Wang18de1f62023-01-21 01:24:24 +00005102
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005103 # cl.GetMostRecentPatchset uses cached information, and can return the last
5104 # patchset before upload. Calling it here makes it clear that it's the
5105 # last patchset before upload. Note that GetMostRecentPatchset will fail
5106 # if no CL has been uploaded yet.
5107 if options.retry_failed:
5108 patchset = cl.GetMostRecentPatchset()
Joanna Wangd75fc882023-03-01 21:53:34 +00005109
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005110 ret = cl.CMDUpload(options, args, orig_args)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00005111
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005112 if options.retry_failed:
5113 if ret != 0:
5114 print('Upload failed, so --retry-failed has no effect.')
5115 return ret
5116 builds, _ = _fetch_latest_builds(cl,
5117 DEFAULT_BUILDBUCKET_HOST,
5118 latest_patchset=patchset)
5119 jobs = _filter_failed_for_retry(builds)
5120 if len(jobs) == 0:
5121 print('No failed tryjobs, so --retry-failed has no effect.')
5122 return ret
5123 _trigger_tryjobs(cl, jobs, options, patchset + 1)
Quinten Yearsleya19d3532019-09-30 21:54:39 +00005124
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005125 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00005126
5127
Daniel Cheng66d0f152023-08-29 23:21:58 +00005128def UploadAllSquashed(options: optparse.Values,
5129 orig_args: Sequence[str]) -> int:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005130 """Uploads the current and upstream branches (if necessary)."""
5131 cls, cherry_pick_current = _UploadAllPrecheck(options, orig_args)
Joanna Wang18de1f62023-01-21 01:24:24 +00005132
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005133 # Create commits.
5134 uploads_by_cl: List[Tuple[Changelist, _NewUpload]] = []
5135 if cherry_pick_current:
5136 parent = cls[1]._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
5137 new_upload = cls[0].PrepareCherryPickSquashedCommit(options, parent)
5138 uploads_by_cl.append((cls[0], new_upload))
5139 else:
5140 ordered_cls = list(reversed(cls))
Joanna Wangc710e2d2023-01-25 14:53:22 +00005141
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005142 cl = ordered_cls[0]
5143 # We can only support external changes when we're only uploading one
5144 # branch.
5145 parent = cl._UpdateWithExternalChanges() if len(
5146 ordered_cls) == 1 else None
5147 orig_parent = None
5148 if parent is None:
5149 origin = '.'
5150 branch = cl.GetBranch()
Joanna Wang74c53b62023-03-01 22:00:22 +00005151
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005152 while origin == '.':
5153 # Search for cl's closest ancestor with a gerrit hash.
5154 origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(
5155 branch)
5156 if origin == '.':
5157 upstream_branch = scm.GIT.ShortBranchName(
5158 upstream_branch_ref)
Joanna Wang7603f042023-03-01 22:17:36 +00005159
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005160 # Support the `git merge` and `git pull` workflow.
5161 if upstream_branch in ['master', 'main']:
5162 parent = cl.GetCommonAncestorWithUpstream()
5163 else:
5164 orig_parent = scm.GIT.GetBranchConfig(
5165 settings.GetRoot(), upstream_branch,
5166 LAST_UPLOAD_HASH_CONFIG_KEY)
5167 parent = scm.GIT.GetBranchConfig(
5168 settings.GetRoot(), upstream_branch,
5169 GERRIT_SQUASH_HASH_CONFIG_KEY)
5170 if parent:
5171 break
5172 branch = upstream_branch
5173 else:
5174 # Either the root of the tree is the cl's direct parent and the
5175 # while loop above only found empty branches between cl and the
5176 # root of the tree.
5177 parent = cl.GetCommonAncestorWithUpstream()
Joanna Wang6215dd02023-02-07 15:58:03 +00005178
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005179 if orig_parent is None:
5180 orig_parent = parent
5181 for i, cl in enumerate(ordered_cls):
5182 # If we're in the middle of the stack, set end_commit to
5183 # downstream's direct ancestor.
5184 if i + 1 < len(ordered_cls):
5185 child_base_commit = ordered_cls[
5186 i + 1].GetCommonAncestorWithUpstream()
5187 else:
5188 child_base_commit = None
5189 new_upload = cl.PrepareSquashedCommit(options,
5190 parent,
5191 orig_parent,
5192 end_commit=child_base_commit)
5193 uploads_by_cl.append((cl, new_upload))
5194 parent = new_upload.commit_to_push
5195 orig_parent = child_base_commit
Joanna Wangc710e2d2023-01-25 14:53:22 +00005196
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005197 # Create refspec options
5198 cl, new_upload = uploads_by_cl[-1]
5199 refspec_opts = cl._GetRefSpecOptions(
5200 options,
5201 new_upload.change_desc,
5202 multi_change_upload=len(uploads_by_cl) > 1,
5203 dogfood_path=True)
5204 refspec_suffix = ''
5205 if refspec_opts:
5206 refspec_suffix = '%' + ','.join(refspec_opts)
5207 assert ' ' not in refspec_suffix, (
5208 'spaces not allowed in refspec: "%s"' % refspec_suffix)
Joanna Wangc710e2d2023-01-25 14:53:22 +00005209
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005210 remote, remote_branch = cl.GetRemoteBranch()
5211 branch = GetTargetRef(remote, remote_branch, options.target_branch)
5212 refspec = '%s:refs/for/%s%s' % (new_upload.commit_to_push, branch,
5213 refspec_suffix)
Joanna Wangc710e2d2023-01-25 14:53:22 +00005214
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005215 # Git push
5216 git_push_metadata = {
5217 'gerrit_host':
5218 cl.GetGerritHost(),
5219 'title':
5220 options.title or '<untitled>',
5221 'change_id':
5222 git_footers.get_footer_change_id(new_upload.change_desc.description),
5223 'description':
5224 new_upload.change_desc.description,
5225 }
5226 push_stdout = cl._RunGitPushWithTraces(refspec, refspec_opts,
5227 git_push_metadata,
5228 options.push_options)
Joanna Wangc710e2d2023-01-25 14:53:22 +00005229
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005230 # Post push updates
5231 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
5232 change_numbers = [
5233 m.group(1) for m in map(regex.match, push_stdout.splitlines()) if m
5234 ]
Joanna Wangc710e2d2023-01-25 14:53:22 +00005235
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005236 for i, (cl, new_upload) in enumerate(uploads_by_cl):
5237 cl.PostUploadUpdates(options, new_upload, change_numbers[i])
Joanna Wangc710e2d2023-01-25 14:53:22 +00005238
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005239 return 0
Joanna Wang18de1f62023-01-21 01:24:24 +00005240
5241
5242def _UploadAllPrecheck(options, orig_args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005243 # type: (optparse.Values, Sequence[str]) -> Tuple[Sequence[Changelist],
5244 # bool]
5245 """Checks the state of the tree and gives the user uploading options
Joanna Wang18de1f62023-01-21 01:24:24 +00005246
5247 Returns: A tuple of the ordered list of changes that have new commits
5248 since their last upload and a boolean of whether the user wants to
5249 cherry-pick and upload the current branch instead of uploading all cls.
5250 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005251 cl = Changelist()
5252 if cl.GetBranch() is None:
5253 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
Joanna Wang6b98cdc2023-02-16 00:37:20 +00005254
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005255 branch_ref = None
5256 cls = []
5257 must_upload_upstream = False
5258 first_pass = True
Joanna Wang18de1f62023-01-21 01:24:24 +00005259
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005260 Changelist._GerritCommitMsgHookCheck(offer_removal=not options.force)
Joanna Wang18de1f62023-01-21 01:24:24 +00005261
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005262 while True:
5263 if len(cls) > _MAX_STACKED_BRANCHES_UPLOAD:
5264 DieWithError(
5265 'More than %s branches in the stack have not been uploaded.\n'
5266 'Are your branches in a misconfigured state?\n'
5267 'If not, please upload some upstream changes first.' %
5268 (_MAX_STACKED_BRANCHES_UPLOAD))
Joanna Wang18de1f62023-01-21 01:24:24 +00005269
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005270 cl = Changelist(branchref=branch_ref)
Joanna Wang18de1f62023-01-21 01:24:24 +00005271
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005272 # Only add CL if it has anything to commit.
5273 base_commit = cl.GetCommonAncestorWithUpstream()
5274 end_commit = RunGit(['rev-parse', cl.GetBranchRef()]).strip()
Joanna Wang6215dd02023-02-07 15:58:03 +00005275
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005276 commit_summary = _GetCommitCountSummary(base_commit, end_commit)
5277 if commit_summary:
5278 cls.append(cl)
5279 if (not first_pass and
5280 cl._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
5281 is None):
5282 # We are mid-stack and the user must upload their upstream
5283 # branches.
5284 must_upload_upstream = True
5285 print(f'Found change with {commit_summary}...')
5286 elif first_pass: # The current branch has nothing to commit. Exit.
5287 DieWithError('Branch %s has nothing to commit' % cl.GetBranch())
5288 # Else: A mid-stack branch has nothing to commit. We do not add it to
5289 # cls.
5290 first_pass = False
Joanna Wang6215dd02023-02-07 15:58:03 +00005291
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005292 # Cases below determine if we should continue to traverse up the tree.
5293 origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(
5294 cl.GetBranch())
5295 branch_ref = upstream_branch_ref # set branch for next run.
Joanna Wang18de1f62023-01-21 01:24:24 +00005296
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005297 upstream_branch = scm.GIT.ShortBranchName(upstream_branch_ref)
5298 upstream_last_upload = scm.GIT.GetBranchConfig(
5299 settings.GetRoot(), upstream_branch, LAST_UPLOAD_HASH_CONFIG_KEY)
Joanna Wang6215dd02023-02-07 15:58:03 +00005300
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005301 # Case 1: We've reached the beginning of the tree.
5302 if origin != '.':
5303 break
Joanna Wang18de1f62023-01-21 01:24:24 +00005304
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005305 # Case 2: If any upstream branches have never been uploaded,
5306 # the user MUST upload them unless they are empty. Continue to
5307 # next loop to add upstream if it is not empty.
5308 if not upstream_last_upload:
5309 continue
Joanna Wang18de1f62023-01-21 01:24:24 +00005310
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005311 # Case 3: If upstream's last_upload == cl.base_commit we do
5312 # not need to upload any more upstreams from this point on.
5313 # (Even if there may be diverged branches higher up the tree)
5314 if base_commit == upstream_last_upload:
5315 break
Joanna Wang18de1f62023-01-21 01:24:24 +00005316
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005317 # Case 4: If upstream's last_upload < cl.base_commit we are
5318 # uploading cl and upstream_cl.
5319 # Continue up the tree to check other branch relations.
5320 if scm.GIT.IsAncestor(upstream_last_upload, base_commit):
5321 continue
Joanna Wang18de1f62023-01-21 01:24:24 +00005322
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005323 # Case 5: If cl.base_commit < upstream's last_upload the user
5324 # must rebase before uploading.
5325 if scm.GIT.IsAncestor(base_commit, upstream_last_upload):
5326 DieWithError(
5327 'At least one branch in the stack has diverged from its upstream '
5328 'branch and does not contain its upstream\'s last upload.\n'
5329 'Please rebase the stack with `git rebase-update` before uploading.'
5330 )
Joanna Wang18de1f62023-01-21 01:24:24 +00005331
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005332 # The tree went through a rebase. LAST_UPLOAD_HASH_CONFIG_KEY no longer
5333 # has any relation to commits in the tree. Continue up the tree until we
5334 # hit the root.
Joanna Wang18de1f62023-01-21 01:24:24 +00005335
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005336 # We assume all cls in the stack have the same auth requirements and only
5337 # check this once.
5338 cls[0].EnsureAuthenticated(force=options.force)
Joanna Wang18de1f62023-01-21 01:24:24 +00005339
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005340 cherry_pick = False
5341 if len(cls) > 1:
5342 opt_message = ''
5343 branches = ', '.join([cl.branch for cl in cls])
5344 if len(orig_args):
5345 opt_message = ('options %s will be used for all uploads.\n' %
5346 orig_args)
5347 if must_upload_upstream:
5348 msg = ('At least one parent branch in `%s` has never been uploaded '
5349 'and must be uploaded before/with `%s`.\n' %
5350 (branches, cls[1].branch))
5351 if options.cherry_pick_stacked:
5352 DieWithError(msg)
5353 if not options.force:
5354 confirm_or_exit('\n' + opt_message + msg)
5355 else:
5356 if options.cherry_pick_stacked:
5357 print('cherry-picking `%s` on %s\'s last upload' %
5358 (cls[0].branch, cls[1].branch))
5359 cherry_pick = True
5360 elif not options.force:
5361 answer = gclient_utils.AskForData(
5362 '\n' + opt_message +
5363 'Press enter to update branches %s.\nOr type `n` to upload only '
5364 '`%s` cherry-picked on %s\'s last upload:' %
5365 (branches, cls[0].branch, cls[1].branch))
5366 if answer.lower() == 'n':
5367 cherry_pick = True
5368 return cls, cherry_pick
Joanna Wang18de1f62023-01-21 01:24:24 +00005369
5370
Francois Dorayd42c6812017-05-30 15:10:20 -04005371@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005372@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04005373def CMDsplit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005374 """Splits a branch into smaller branches and uploads CLs.
Francois Dorayd42c6812017-05-30 15:10:20 -04005375
5376 Creates a branch and uploads a CL for each group of files modified in the
5377 current branch that share a common OWNERS file. In the CL description and
Edward Lemurac5c55f2020-02-29 00:17:16 +00005378 comment, the string '$directory', is replaced with the directory containing
5379 the shared OWNERS file.
Francois Dorayd42c6812017-05-30 15:10:20 -04005380 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005381 parser.add_option('-d',
5382 '--description',
5383 dest='description_file',
5384 help='A text file containing a CL description in which '
5385 '$directory will be replaced by each CL\'s directory.')
5386 parser.add_option('-c',
5387 '--comment',
5388 dest='comment_file',
5389 help='A text file containing a CL comment.')
5390 parser.add_option(
5391 '-n',
5392 '--dry-run',
5393 dest='dry_run',
5394 action='store_true',
5395 default=False,
5396 help='List the files and reviewers for each CL that would '
5397 'be created, but don\'t create branches or CLs.')
5398 parser.add_option('--cq-dry-run',
5399 action='store_true',
5400 help='If set, will do a cq dry run for each uploaded CL. '
5401 'Please be careful when doing this; more than ~10 CLs '
5402 'has the potential to overload our build '
5403 'infrastructure. Try to upload these not during high '
5404 'load times (usually 11-3 Mountain View time). Email '
5405 'infra-dev@chromium.org with any questions.')
5406 parser.add_option(
5407 '-a',
Thiago Perrotta853b29f2023-09-27 14:51:20 +00005408 '--auto-submit',
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005409 '--enable-auto-submit',
5410 action='store_true',
5411 dest='enable_auto_submit',
5412 default=True,
5413 help='Sends your change to the CQ after an approval. Only '
5414 'works on repos that have the Auto-Submit label '
5415 'enabled')
5416 parser.add_option(
5417 '--disable-auto-submit',
5418 action='store_false',
5419 dest='enable_auto_submit',
5420 help='Disables automatic sending of the changes to the CQ '
5421 'after approval. Note that auto-submit only works for '
5422 'repos that have the Auto-Submit label enabled.')
5423 parser.add_option('--max-depth',
5424 type='int',
5425 default=0,
5426 help='The max depth to look for OWNERS files. Useful for '
5427 'controlling the granularity of the split CLs, e.g. '
5428 '--max-depth=1 will only split by top-level '
5429 'directory. Specifying a value less than 1 means no '
5430 'limit on max depth.')
5431 parser.add_option('--topic',
5432 default=None,
5433 help='Topic to specify when uploading')
5434 options, _ = parser.parse_args(args)
Francois Dorayd42c6812017-05-30 15:10:20 -04005435
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005436 if not options.description_file:
5437 parser.error('No --description flag specified.')
Francois Dorayd42c6812017-05-30 15:10:20 -04005438
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005439 def WrappedCMDupload(args):
5440 return CMDupload(OptionParser(), args)
Francois Dorayd42c6812017-05-30 15:10:20 -04005441
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005442 return split_cl.SplitCl(options.description_file, options.comment_file,
5443 Changelist, WrappedCMDupload, options.dry_run,
5444 options.cq_dry_run, options.enable_auto_submit,
5445 options.max_depth, options.topic,
5446 settings.GetRoot())
Francois Dorayd42c6812017-05-30 15:10:20 -04005447
5448
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005449@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005450@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005451def CMDdcommit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005452 """DEPRECATED: Used to commit the current changelist via git-svn."""
5453 message = ('git-cl no longer supports committing to SVN repositories via '
5454 'git-svn. You probably want to use `git cl land` instead.')
5455 print(message)
5456 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005457
5458
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005459@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005460@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005461def CMDland(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005462 """Commits the current changelist via git.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005463
5464 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5465 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005466 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005467 parser.add_option('--bypass-hooks',
5468 action='store_true',
5469 dest='bypass_hooks',
5470 help='bypass upload presubmit hook')
5471 parser.add_option('-f',
5472 '--force',
5473 action='store_true',
5474 dest='force',
5475 help="force yes to questions (don't prompt)")
5476 parser.add_option(
5477 '--parallel',
5478 action='store_true',
5479 help='Run all tests specified by input_api.RunTests in all '
5480 'PRESUBMIT files in parallel.')
5481 parser.add_option('--resultdb',
5482 action='store_true',
5483 help='Run presubmit checks in the ResultSink environment '
5484 'and send results to the ResultDB database.')
5485 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
5486 (options, args) = parser.parse_args(args)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005487
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005488 cl = Changelist()
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005489
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005490 if not cl.GetIssue():
5491 DieWithError('You must upload the change first to Gerrit.\n'
5492 ' If you would rather have `git cl land` upload '
5493 'automatically for you, see http://crbug.com/642759')
5494 return cl.CMDLand(options.force, options.bypass_hooks, options.verbose,
5495 options.parallel, options.resultdb, options.realm)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005496
5497
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005498@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005499@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005500def CMDpatch(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005501 """Applies (cherry-picks) a Gerrit changelist locally."""
5502 parser.add_option('-b',
Yiwei Zhangf2f50002023-10-13 20:40:37 +00005503 '--branch',
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005504 dest='newbranch',
5505 help='create a new branch off trunk for the patch')
5506 parser.add_option('-f',
5507 '--force',
5508 action='store_true',
5509 help='overwrite state on the current or chosen branch')
5510 parser.add_option('-n',
5511 '--no-commit',
5512 action='store_true',
5513 dest='nocommit',
5514 help='don\'t commit after patch applies.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005515
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005516 group = optparse.OptionGroup(
5517 parser,
5518 'Options for continuing work on the current issue uploaded from a '
5519 'different clone (e.g. different machine). Must be used independently '
5520 'from the other options. No issue number should be specified, and the '
5521 'branch must have an issue number associated with it')
5522 group.add_option('--reapply',
5523 action='store_true',
5524 dest='reapply',
5525 help='Reset the branch and reapply the issue.\n'
5526 'CAUTION: This will undo any local changes in this '
5527 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005528
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005529 group.add_option('--pull',
5530 action='store_true',
5531 dest='pull',
5532 help='Performs a pull before reapplying.')
5533 parser.add_option_group(group)
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005534
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005535 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005536
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005537 if options.reapply:
5538 if options.newbranch:
5539 parser.error('--reapply works on the current branch only.')
5540 if len(args) > 0:
5541 parser.error('--reapply implies no additional arguments.')
5542
5543 cl = Changelist()
5544 if not cl.GetIssue():
5545 parser.error('Current branch must have an associated issue.')
5546
5547 upstream = cl.GetUpstreamBranch()
5548 if upstream is None:
5549 parser.error('No upstream branch specified. Cannot reset branch.')
5550
5551 RunGit(['reset', '--hard', upstream])
5552 if options.pull:
5553 RunGit(['pull'])
5554
5555 target_issue_arg = ParseIssueNumberArgument(cl.GetIssue())
5556 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit,
5557 options.force, False)
5558
5559 if len(args) != 1 or not args[0]:
5560 parser.error('Must specify issue number or URL.')
5561
5562 target_issue_arg = ParseIssueNumberArgument(args[0])
5563 if not target_issue_arg.valid:
5564 parser.error('Invalid issue ID or URL.')
5565
5566 # We don't want uncommitted changes mixed up with the patch.
5567 if git_common.is_dirty_git_tree('patch'):
5568 return 1
5569
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005570 if options.newbranch:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005571 if options.force:
5572 RunGit(['branch', '-D', options.newbranch],
5573 stderr=subprocess2.PIPE,
5574 error_ok=True)
5575 git_new_branch.create_new_branch(options.newbranch)
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005576
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005577 cl = Changelist(codereview_host=target_issue_arg.hostname,
5578 issue=target_issue_arg.issue)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005579
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005580 if not args[0].isdigit():
5581 print('canonical issue/change URL: %s\n' % cl.GetIssueURL())
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005582
Joanna Wang44e9bee2023-01-25 21:51:42 +00005583 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit,
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005584 options.force, options.newbranch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005585
5586
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005587def GetTreeStatus(url=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005588 """Fetches the tree status and returns either 'open', 'closed',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005589 'unknown' or 'unset'."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005590 url = url or settings.GetTreeStatusUrl(error_ok=True)
5591 if url:
5592 status = str(urllib.request.urlopen(url).read().lower())
5593 if status.find('closed') != -1 or status == '0':
5594 return 'closed'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005595
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005596 if status.find('open') != -1 or status == '1':
5597 return 'open'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005598
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005599 return 'unknown'
5600 return 'unset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005601
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005602
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005603def GetTreeStatusReason():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005604 """Fetches the tree status from a json url and returns the message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005605 with the reason for the tree to be opened or closed."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005606 url = settings.GetTreeStatusUrl()
5607 json_url = urllib.parse.urljoin(url, '/current?format=json')
5608 connection = urllib.request.urlopen(json_url)
5609 status = json.loads(connection.read())
5610 connection.close()
5611 return status['message']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005612
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005613
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005614@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005615def CMDtree(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005616 """Shows the status of the tree."""
5617 _, args = parser.parse_args(args)
5618 status = GetTreeStatus()
5619 if 'unset' == status:
5620 print(
5621 'You must configure your tree status URL by running "git cl config".'
5622 )
5623 return 2
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005624
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005625 print('The tree is %s' % status)
5626 print()
5627 print(GetTreeStatusReason())
5628 if status != 'open':
5629 return 1
5630 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005631
5632
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005633@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005634def CMDtry(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005635 """Triggers tryjobs using either Buildbucket or CQ dry run."""
5636 group = optparse.OptionGroup(parser, 'Tryjob options')
5637 group.add_option(
5638 '-b',
5639 '--bot',
5640 action='append',
5641 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5642 'times to specify multiple builders. ex: '
5643 '"-b win_rel -b win_layout". See '
5644 'the try server waterfall for the builders name and the tests '
5645 'available.'))
5646 group.add_option(
5647 '-B',
5648 '--bucket',
5649 default='',
5650 help=('Buildbucket bucket to send the try requests. Format: '
5651 '"luci.$LUCI_PROJECT.$LUCI_BUCKET". eg: "luci.chromium.try"'))
5652 group.add_option(
5653 '-r',
5654 '--revision',
5655 help='Revision to use for the tryjob; default: the revision will '
5656 'be determined by the try recipe that builder runs, which usually '
5657 'defaults to HEAD of origin/master or origin/main')
5658 group.add_option(
5659 '-c',
5660 '--clobber',
5661 action='store_true',
5662 default=False,
5663 help='Force a clobber before building; that is don\'t do an '
5664 'incremental build')
5665 group.add_option('--category',
5666 default='git_cl_try',
5667 help='Specify custom build category.')
5668 group.add_option(
5669 '--project',
5670 help='Override which project to use. Projects are defined '
5671 'in recipe to determine to which repository or directory to '
5672 'apply the patch')
5673 group.add_option(
5674 '-p',
5675 '--property',
5676 dest='properties',
5677 action='append',
5678 default=[],
5679 help='Specify generic properties in the form -p key1=value1 -p '
5680 'key2=value2 etc. The value will be treated as '
5681 'json if decodable, or as string otherwise. '
5682 'NOTE: using this may make your tryjob not usable for CQ, '
5683 'which will then schedule another tryjob with default properties')
5684 group.add_option('--buildbucket-host',
5685 default='cr-buildbucket.appspot.com',
5686 help='Host of buildbucket. The default host is %default.')
5687 parser.add_option_group(group)
5688 parser.add_option('-R',
5689 '--retry-failed',
5690 action='store_true',
5691 default=False,
5692 help='Retry failed jobs from the latest set of tryjobs. '
5693 'Not allowed with --bucket and --bot options.')
5694 parser.add_option(
5695 '-i',
5696 '--issue',
5697 type=int,
5698 help='Operate on this issue instead of the current branch\'s implicit '
5699 'issue.')
5700 options, args = parser.parse_args(args)
maruel@chromium.org15192402012-09-06 12:38:29 +00005701
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005702 # Make sure that all properties are prop=value pairs.
5703 bad_params = [x for x in options.properties if '=' not in x]
5704 if bad_params:
5705 parser.error('Got properties with missing "=": %s' % bad_params)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005706
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005707 if args:
5708 parser.error('Unknown arguments: %s' % args)
maruel@chromium.org15192402012-09-06 12:38:29 +00005709
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005710 cl = Changelist(issue=options.issue)
5711 if not cl.GetIssue():
5712 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005713
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005714 # HACK: warm up Gerrit change detail cache to save on RPCs.
5715 cl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005716
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005717 error_message = cl.CannotTriggerTryJobReason()
5718 if error_message:
5719 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005720
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005721 if options.bot:
5722 if options.retry_failed:
5723 parser.error('--bot is not compatible with --retry-failed.')
5724 if not options.bucket:
5725 parser.error('A bucket (e.g. "chromium/try") is required.')
Edward Lemur45768512020-03-02 19:03:14 +00005726
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005727 triggered = [b for b in options.bot if 'triggered' in b]
5728 if triggered:
5729 parser.error(
5730 'Cannot schedule builds on triggered bots: %s.\n'
5731 'This type of bot requires an initial job from a parent (usually a '
5732 'builder). Schedule a job on the parent instead.\n' % triggered)
Edward Lemur45768512020-03-02 19:03:14 +00005733
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005734 if options.bucket.startswith('.master'):
5735 parser.error('Buildbot masters are not supported.')
Edward Lemur45768512020-03-02 19:03:14 +00005736
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005737 project, bucket = _parse_bucket(options.bucket)
5738 if project is None or bucket is None:
5739 parser.error('Invalid bucket: %s.' % options.bucket)
5740 jobs = sorted((project, bucket, bot) for bot in options.bot)
5741 elif options.retry_failed:
5742 print('Searching for failed tryjobs...')
5743 builds, patchset = _fetch_latest_builds(cl, DEFAULT_BUILDBUCKET_HOST)
5744 if options.verbose:
5745 print('Got %d builds in patchset #%d' % (len(builds), patchset))
5746 jobs = _filter_failed_for_retry(builds)
5747 if not jobs:
5748 print('There are no failed jobs in the latest set of jobs '
5749 '(patchset #%d), doing nothing.' % patchset)
5750 return 0
5751 num_builders = len(jobs)
5752 if num_builders > 10:
5753 confirm_or_exit('There are %d builders with failed builds.' %
5754 num_builders,
5755 action='continue')
5756 else:
5757 if options.verbose:
5758 print('git cl try with no bots now defaults to CQ dry run.')
5759 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5760 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005761
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005762 patchset = cl.GetMostRecentPatchset()
5763 try:
5764 _trigger_tryjobs(cl, jobs, options, patchset)
5765 except BuildbucketResponseException as ex:
5766 print('ERROR: %s' % ex)
5767 return 1
5768 return 0
maruel@chromium.org15192402012-09-06 12:38:29 +00005769
5770
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005771@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005772def CMDtry_results(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005773 """Prints info about results for tryjobs associated with the current CL."""
5774 group = optparse.OptionGroup(parser, 'Tryjob results options')
5775 group.add_option('-p',
5776 '--patchset',
5777 type=int,
5778 help='patchset number if not current.')
5779 group.add_option('--print-master',
5780 action='store_true',
5781 help='print master name as well.')
5782 group.add_option('--color',
5783 action='store_true',
5784 default=setup_color.IS_TTY,
5785 help='force color output, useful when piping output.')
5786 group.add_option('--buildbucket-host',
5787 default='cr-buildbucket.appspot.com',
5788 help='Host of buildbucket. The default host is %default.')
5789 group.add_option(
5790 '--json',
5791 help=('Path of JSON output file to write tryjob results to,'
5792 'or "-" for stdout.'))
5793 parser.add_option_group(group)
5794 parser.add_option(
5795 '-i',
5796 '--issue',
5797 type=int,
5798 help='Operate on this issue instead of the current branch\'s implicit '
5799 'issue.')
5800 options, args = parser.parse_args(args)
5801 if args:
5802 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005803
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005804 cl = Changelist(issue=options.issue)
5805 if not cl.GetIssue():
5806 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005807
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005808 patchset = options.patchset
tandrii221ab252016-10-06 08:12:04 -07005809 if not patchset:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005810 patchset = cl.GetMostRecentDryRunPatchset()
5811 if not patchset:
5812 parser.error('Code review host doesn\'t know about issue %s. '
5813 'No access to issue or wrong issue number?\n'
5814 'Either upload first, or pass --patchset explicitly.' %
5815 cl.GetIssue())
tandrii221ab252016-10-06 08:12:04 -07005816
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005817 try:
5818 jobs = _fetch_tryjobs(cl, DEFAULT_BUILDBUCKET_HOST, patchset)
5819 except BuildbucketResponseException as ex:
5820 print('Buildbucket error: %s' % ex)
5821 return 1
5822 if options.json:
5823 write_json(options.json, jobs)
5824 else:
5825 _print_tryjobs(options, jobs)
5826 return 0
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005827
5828
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005829@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005830@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005831def CMDupstream(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005832 """Prints or sets the name of the upstream branch, if any."""
5833 _, args = parser.parse_args(args)
5834 if len(args) > 1:
5835 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005836
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005837 cl = Changelist()
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005838 if args:
5839 # One arg means set upstream branch.
5840 branch = cl.GetBranch()
5841 RunGit(['branch', '--set-upstream-to', args[0], branch])
5842 cl = Changelist()
5843 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(), ))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005844
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005845 # Clear configured merge-base, if there is one.
5846 git_common.remove_merge_base(branch)
5847 else:
5848 print(cl.GetUpstreamBranch())
5849 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005850
5851
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005852@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005853def CMDweb(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005854 """Opens the current CL in the web browser."""
5855 parser.add_option('-p',
5856 '--print-only',
5857 action='store_true',
5858 dest='print_only',
5859 help='Only print the Gerrit URL, don\'t open it in the '
5860 'browser.')
5861 (options, args) = parser.parse_args(args)
5862 if args:
5863 parser.error('Unrecognized args: %s' % ' '.join(args))
thestig@chromium.org00858c82013-12-02 23:08:03 +00005864
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005865 issue_url = Changelist().GetIssueURL()
5866 if not issue_url:
5867 print('ERROR No issue to open', file=sys.stderr)
5868 return 1
thestig@chromium.org00858c82013-12-02 23:08:03 +00005869
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005870 if options.print_only:
5871 print(issue_url)
5872 return 0
5873
5874 # Redirect I/O before invoking browser to hide its output. For example, this
5875 # allows us to hide the "Created new window in existing browser session."
5876 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
5877 saved_stdout = os.dup(1)
5878 saved_stderr = os.dup(2)
5879 os.close(1)
5880 os.close(2)
5881 os.open(os.devnull, os.O_RDWR)
5882 try:
5883 webbrowser.open(issue_url)
5884 finally:
5885 os.dup2(saved_stdout, 1)
5886 os.dup2(saved_stderr, 2)
Orr Bernstein0b960582022-12-22 20:16:18 +00005887 return 0
5888
thestig@chromium.org00858c82013-12-02 23:08:03 +00005889
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005890@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005891def CMDset_commit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005892 """Sets the commit bit to trigger the CQ."""
5893 parser.add_option('-d',
5894 '--dry-run',
5895 action='store_true',
5896 help='trigger in dry run mode')
5897 parser.add_option('-c',
5898 '--clear',
5899 action='store_true',
5900 help='stop CQ run, if any')
5901 parser.add_option(
5902 '-i',
5903 '--issue',
5904 type=int,
5905 help='Operate on this issue instead of the current branch\'s implicit '
5906 'issue.')
5907 options, args = parser.parse_args(args)
5908 if args:
5909 parser.error('Unrecognized args: %s' % ' '.join(args))
5910 if [options.dry_run, options.clear].count(True) > 1:
5911 parser.error('Only one of --dry-run, and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005912
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005913 cl = Changelist(issue=options.issue)
5914 if not cl.GetIssue():
5915 parser.error('Must upload the issue first.')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005916
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005917 if options.clear:
5918 state = _CQState.NONE
5919 elif options.dry_run:
5920 state = _CQState.DRY_RUN
5921 else:
5922 state = _CQState.COMMIT
5923 cl.SetCQState(state)
5924 return 0
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005925
5926
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005927@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005928def CMDset_close(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005929 """Closes the issue."""
5930 parser.add_option(
5931 '-i',
5932 '--issue',
5933 type=int,
5934 help='Operate on this issue instead of the current branch\'s implicit '
5935 'issue.')
5936 options, args = parser.parse_args(args)
5937 if args:
5938 parser.error('Unrecognized args: %s' % ' '.join(args))
5939 cl = Changelist(issue=options.issue)
5940 # Ensure there actually is an issue to close.
5941 if not cl.GetIssue():
5942 DieWithError('ERROR: No issue to close.')
5943 cl.CloseIssue()
5944 return 0
groby@chromium.org411034a2013-02-26 15:12:01 +00005945
5946
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005947@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005948def CMDdiff(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005949 """Shows differences between local tree and last upload."""
5950 parser.add_option('--stat',
5951 action='store_true',
5952 dest='stat',
5953 help='Generate a diffstat')
5954 options, args = parser.parse_args(args)
5955 if args:
5956 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005957
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005958 cl = Changelist()
5959 issue = cl.GetIssue()
5960 branch = cl.GetBranch()
5961 if not issue:
5962 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005963
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005964 base = cl._GitGetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY)
5965 if not base:
5966 base = cl._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
5967 if not base:
5968 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5969 revision_info = detail['revisions'][detail['current_revision']]
5970 fetch_info = revision_info['fetch']['http']
5971 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5972 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005973
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005974 cmd = ['git', 'diff']
5975 if options.stat:
5976 cmd.append('--stat')
5977 cmd.append(base)
5978 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005979
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005980 return 0
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005981
5982
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005983@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005984def CMDowners(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005985 """Finds potential owners for reviewing."""
5986 parser.add_option(
5987 '--ignore-current',
5988 action='store_true',
5989 help='Ignore the CL\'s current reviewers and start from scratch.')
5990 parser.add_option('--ignore-self',
5991 action='store_true',
5992 help='Do not consider CL\'s author as an owners.')
5993 parser.add_option('--no-color',
5994 action='store_true',
5995 help='Use this option to disable color output')
5996 parser.add_option('--batch',
5997 action='store_true',
5998 help='Do not run interactively, just suggest some')
5999 # TODO: Consider moving this to another command, since other
6000 # git-cl owners commands deal with owners for a given CL.
6001 parser.add_option('--show-all',
6002 action='store_true',
6003 help='Show all owners for a particular file')
6004 options, args = parser.parse_args(args)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00006005
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006006 cl = Changelist()
6007 author = cl.GetAuthor()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00006008
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006009 if options.show_all:
6010 if len(args) == 0:
6011 print('No files specified for --show-all. Nothing to do.')
6012 return 0
6013 owners_by_path = cl.owners_client.BatchListOwners(args)
6014 for path in args:
6015 print('Owners for %s:' % path)
6016 print('\n'.join(
6017 ' - %s' % owner
6018 for owner in owners_by_path.get(path, ['No owners found'])))
6019 return 0
Yang Guo6e269a02019-06-26 11:17:02 +00006020
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006021 if args:
6022 if len(args) > 1:
6023 parser.error('Unknown args.')
6024 base_branch = args[0]
6025 else:
6026 # Default to diffing against the common ancestor of the upstream branch.
6027 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00006028
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006029 affected_files = cl.GetAffectedFiles(base_branch)
Dirk Prankebf980882017-09-02 15:08:00 -07006030
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006031 if options.batch:
6032 owners = cl.owners_client.SuggestOwners(affected_files,
6033 exclude=[author])
6034 print('\n'.join(owners))
6035 return 0
Dirk Prankebf980882017-09-02 15:08:00 -07006036
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006037 return owners_finder.OwnersFinder(
6038 affected_files,
6039 author, [] if options.ignore_current else cl.GetReviewers(),
6040 cl.owners_client,
6041 disable_color=options.no_color,
6042 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00006043
6044
Aiden Bennerc08566e2018-10-03 17:52:42 +00006045def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006046 """Generates a diff command."""
6047 # Generate diff for the current branch's changes.
6048 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
Aiden Bennerc08566e2018-10-03 17:52:42 +00006049
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006050 if allow_prefix:
6051 # explicitly setting --src-prefix and --dst-prefix is necessary in the
6052 # case that diff.noprefix is set in the user's git config.
6053 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
6054 else:
6055 diff_cmd += ['--no-prefix']
Aiden Bennerc08566e2018-10-03 17:52:42 +00006056
Arthur Eubanks92d8c4e2023-10-09 19:57:24 +00006057 diff_cmd += diff_type
6058 diff_cmd += [upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006059
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006060 if args:
6061 for arg in args:
6062 if os.path.isdir(arg) or os.path.isfile(arg):
6063 diff_cmd.append(arg)
6064 else:
6065 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006066
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006067 return diff_cmd
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006068
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006069
Jamie Madill5e96ad12020-01-13 16:08:35 +00006070def _RunClangFormatDiff(opts, clang_diff_files, top_dir, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006071 """Runs clang-format-diff and sets a return value if necessary."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006072 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
6073 # formatted. This is used to block during the presubmit.
6074 return_value = 0
Jamie Madill5e96ad12020-01-13 16:08:35 +00006075
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006076 # Locate the clang-format binary in the checkout
Jamie Madill5e96ad12020-01-13 16:08:35 +00006077 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006078 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
Jamie Madill5e96ad12020-01-13 16:08:35 +00006079 except clang_format.NotFoundError as e:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006080 DieWithError(e)
Jamie Madill5e96ad12020-01-13 16:08:35 +00006081
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006082 if opts.full or settings.GetFormatFullByDefault():
6083 cmd = [clang_format_tool]
6084 if not opts.dry_run and not opts.diff:
6085 cmd.append('-i')
6086 if opts.dry_run:
6087 for diff_file in clang_diff_files:
6088 with open(diff_file, 'r') as myfile:
6089 code = myfile.read().replace('\r\n', '\n')
6090 stdout = RunCommand(cmd + [diff_file], cwd=top_dir)
6091 stdout = stdout.replace('\r\n', '\n')
6092 if opts.diff:
6093 sys.stdout.write(stdout)
6094 if code != stdout:
6095 return_value = 2
6096 else:
6097 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
6098 if opts.diff:
6099 sys.stdout.write(stdout)
6100 else:
6101 try:
6102 script = clang_format.FindClangFormatScriptInChromiumTree(
6103 'clang-format-diff.py')
6104 except clang_format.NotFoundError as e:
6105 DieWithError(e)
Jamie Madill5e96ad12020-01-13 16:08:35 +00006106
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006107 cmd = ['vpython3', script, '-p0']
6108 if not opts.dry_run and not opts.diff:
6109 cmd.append('-i')
Jamie Madill5e96ad12020-01-13 16:08:35 +00006110
Arthur Eubanks92d8c4e2023-10-09 19:57:24 +00006111 diff_cmd = BuildGitDiffCmd(['-U0'], upstream_commit, clang_diff_files)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006112 diff_output = RunGit(diff_cmd).encode('utf-8')
Jamie Madill5e96ad12020-01-13 16:08:35 +00006113
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006114 env = os.environ.copy()
6115 env['PATH'] = (str(os.path.dirname(clang_format_tool)) + os.pathsep +
6116 env['PATH'])
6117 stdout = RunCommand(cmd,
6118 stdin=diff_output,
6119 cwd=top_dir,
6120 env=env,
6121 shell=sys.platform.startswith('win32'))
6122 if opts.diff:
6123 sys.stdout.write(stdout)
6124 if opts.dry_run and len(stdout) > 0:
6125 return_value = 2
6126
6127 return return_value
Jamie Madill5e96ad12020-01-13 16:08:35 +00006128
6129
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006130def _FindGoogleJavaFormat():
Andrew Grieve69e597f2023-10-12 02:16:43 +00006131 # Allow non-chromium projects to use a custom location.
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006132 primary_solution_path = gclient_paths.GetPrimarySolutionPath()
6133 if primary_solution_path:
Andrew Grieve69e597f2023-10-12 02:16:43 +00006134 override = os.environ.get('GOOGLE_JAVA_FORMAT_PATH')
6135 if override:
6136 # Make relative to solution root if not an absolute path.
6137 return os.path.join(primary_solution_path, override)
6138
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006139 path = os.path.join(primary_solution_path, 'third_party',
6140 'google-java-format', 'google-java-format')
6141 if os.path.exists(path):
6142 return path
Andrew Grieve69e597f2023-10-12 02:16:43 +00006143 return None
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006144
6145
6146def _RunGoogleJavaFormat(opts, paths, top_dir, upstream_commit):
6147 """Runs google-java-format and sets a return value if necessary."""
6148 google_java_format = _FindGoogleJavaFormat()
6149 if google_java_format is None:
Andrew Grieve69e597f2023-10-12 02:16:43 +00006150 # Fail silently. It could be we are on an old chromium revision, or that
6151 # it is a non-chromium project. https://crbug.com/1491627
6152 print('google-java-format not found, skipping java formatting.')
6153 return 0
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006154
6155 base_cmd = [google_java_format, '--aosp']
6156 if opts.dry_run or opts.diff:
6157 base_cmd += ['--dry-run']
6158 else:
6159 base_cmd += ['--replace']
6160
6161 changed_lines_only = not (opts.full or settings.GetFormatFullByDefault())
6162 if changed_lines_only:
6163 line_diffs = _ComputeFormatDiffLineRanges(paths, upstream_commit)
6164
6165 results = []
6166 kwds = {'error_ok': True, 'cwd': top_dir}
6167 with multiprocessing.pool.ThreadPool() as pool:
6168 for path in paths:
6169 cmd = base_cmd.copy()
6170 if changed_lines_only:
6171 ranges = line_diffs.get(path)
6172 if not ranges:
6173 # E.g. There were only deleted lines.
6174 continue
6175 cmd.extend('--lines={}:{}'.format(a, b) for a, b in ranges)
6176
6177 results.append(
6178 pool.apply_async(RunCommand, args=[cmd + [path]], kwds=kwds))
6179
6180 return_value = 0
6181 for result in results:
6182 stdout = result.get()
6183 if stdout:
6184 if opts.diff:
6185 sys.stdout.write('Requires formatting: ' + stdout)
6186 else:
6187 return_value = 2
6188
6189 return return_value
6190
6191
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006192def _RunRustFmt(opts, rust_diff_files, top_dir, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006193 """Runs rustfmt. Just like _RunClangFormatDiff returns 2 to indicate that
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006194 presubmit checks have failed (and returns 0 otherwise)."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006195 # Locate the rustfmt binary.
6196 try:
6197 rustfmt_tool = rustfmt.FindRustfmtToolInChromiumTree()
6198 except rustfmt.NotFoundError as e:
6199 DieWithError(e)
6200
6201 # TODO(crbug.com/1440869): Support formatting only the changed lines
6202 # if `opts.full or settings.GetFormatFullByDefault()` is False.
6203 cmd = [rustfmt_tool]
6204 if opts.dry_run:
6205 cmd.append('--check')
6206 cmd += rust_diff_files
6207 rustfmt_exitcode = subprocess2.call(cmd)
6208
6209 if opts.presubmit and rustfmt_exitcode != 0:
6210 return 2
6211
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006212 return 0
6213
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006214
Olivier Robin0a6b5442022-04-07 07:25:04 +00006215def _RunSwiftFormat(opts, swift_diff_files, top_dir, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006216 """Runs swift-format. Just like _RunClangFormatDiff returns 2 to indicate
Olivier Robin0a6b5442022-04-07 07:25:04 +00006217 that presubmit checks have failed (and returns 0 otherwise)."""
Andrew Grievecca48db2023-09-14 14:12:23 +00006218 if sys.platform != 'darwin':
6219 DieWithError('swift-format is only supported on macOS.')
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006220 # Locate the swift-format binary.
6221 try:
6222 swift_format_tool = swift_format.FindSwiftFormatToolInChromiumTree()
6223 except swift_format.NotFoundError as e:
6224 DieWithError(e)
6225
6226 cmd = [swift_format_tool]
6227 if opts.dry_run:
6228 cmd += ['lint', '-s']
6229 else:
6230 cmd += ['format', '-i']
6231 cmd += swift_diff_files
6232 swift_format_exitcode = subprocess2.call(cmd)
6233
6234 if opts.presubmit and swift_format_exitcode != 0:
6235 return 2
6236
Olivier Robin0a6b5442022-04-07 07:25:04 +00006237 return 0
6238
Olivier Robin0a6b5442022-04-07 07:25:04 +00006239
Andrew Grievecca48db2023-09-14 14:12:23 +00006240def _RunYapf(opts, paths, top_dir, upstream_commit):
6241 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
6242 yapf_tool = os.path.join(depot_tools_path, 'yapf')
6243
6244 # Used for caching.
6245 yapf_configs = {}
6246 for p in paths:
6247 # Find the yapf style config for the current file, defaults to depot
6248 # tools default.
6249 _FindYapfConfigFile(p, yapf_configs, top_dir)
6250
6251 # Turn on python formatting by default if a yapf config is specified.
6252 # This breaks in the case of this repo though since the specified
6253 # style file is also the global default.
6254 if opts.python is None:
6255 paths = [
6256 p for p in paths
6257 if _FindYapfConfigFile(p, yapf_configs, top_dir) is not None
6258 ]
6259
6260 # Note: yapf still seems to fix indentation of the entire file
6261 # even if line ranges are specified.
6262 # See https://github.com/google/yapf/issues/499
6263 if not opts.full and paths:
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006264 line_diffs = _ComputeFormatDiffLineRanges(paths, upstream_commit)
Andrew Grievecca48db2023-09-14 14:12:23 +00006265
6266 yapfignore_patterns = _GetYapfIgnorePatterns(top_dir)
6267 paths = _FilterYapfIgnoredFiles(paths, yapfignore_patterns)
6268
6269 return_value = 0
6270 for path in paths:
6271 yapf_style = _FindYapfConfigFile(path, yapf_configs, top_dir)
6272 # Default to pep8 if not .style.yapf is found.
6273 if not yapf_style:
6274 yapf_style = 'pep8'
6275
6276 with open(path, 'r') as py_f:
6277 if 'python2' in py_f.readline():
6278 vpython_script = 'vpython'
6279 else:
6280 vpython_script = 'vpython3'
6281
6282 cmd = [vpython_script, yapf_tool, '--style', yapf_style, path]
6283
Andrew Grievecca48db2023-09-14 14:12:23 +00006284 if not opts.full:
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006285 ranges = line_diffs.get(path)
6286 if not ranges:
Andrew Grievecca48db2023-09-14 14:12:23 +00006287 continue
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006288 # Only run yapf over changed line ranges.
6289 for diff_start, diff_end in ranges:
6290 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
Andrew Grievecca48db2023-09-14 14:12:23 +00006291
6292 if opts.diff or opts.dry_run:
6293 cmd += ['--diff']
6294 # Will return non-zero exit code if non-empty diff.
6295 stdout = RunCommand(cmd,
6296 error_ok=True,
6297 stderr=subprocess2.PIPE,
6298 cwd=top_dir,
6299 shell=sys.platform.startswith('win32'))
6300 if opts.diff:
6301 sys.stdout.write(stdout)
6302 elif len(stdout) > 0:
6303 return_value = 2
6304 else:
6305 cmd += ['-i']
6306 RunCommand(cmd, cwd=top_dir, shell=sys.platform.startswith('win32'))
6307 return return_value
6308
6309
6310def _RunGnFormat(opts, paths, top_dir, upstream_commit):
6311 cmd = ['gn', 'format']
6312 if opts.dry_run or opts.diff:
6313 cmd.append('--dry-run')
6314 return_value = 0
6315 for path in paths:
6316 gn_ret = subprocess2.call(cmd + [path],
6317 shell=sys.platform.startswith('win'),
6318 cwd=top_dir)
6319 if opts.dry_run and gn_ret == 2:
6320 return_value = 2 # Not formatted.
6321 elif opts.diff and gn_ret == 2:
6322 # TODO this should compute and print the actual diff.
6323 print('This change has GN build file diff for ' + path)
6324 elif gn_ret != 0:
6325 # For non-dry run cases (and non-2 return values for dry-run), a
6326 # nonzero error code indicates a failure, probably because the
6327 # file doesn't parse.
6328 DieWithError('gn format failed on ' + path +
6329 '\nTry running `gn format` on this file manually.')
6330 return return_value
6331
6332
6333def _FormatXml(opts, paths, top_dir, upstream_commit):
6334 # Skip the metrics formatting from the global presubmit hook. These files
6335 # have a separate presubmit hook that issues an error if the files need
6336 # formatting, whereas the top-level presubmit script merely issues a
6337 # warning. Formatting these files is somewhat slow, so it's important not to
6338 # duplicate the work.
6339 if opts.presubmit:
6340 return 0
6341
6342 return_value = 0
6343 for path in paths:
6344 xml_dir = GetMetricsDir(path)
6345 if not xml_dir:
6346 continue
6347
6348 tool_dir = os.path.join(top_dir, xml_dir)
6349 pretty_print_tool = os.path.join(tool_dir, 'pretty_print.py')
6350 cmd = [shutil.which('vpython3'), pretty_print_tool, '--non-interactive']
6351
6352 # If the XML file is histograms.xml or enums.xml, add the xml path
6353 # to the command as histograms/pretty_print.py now needs a relative
6354 # path argument after splitting the histograms into multiple
6355 # directories. For example, in tools/metrics/ukm, pretty-print could
6356 # be run using: $ python pretty_print.py But in
6357 # tools/metrics/histogrmas, pretty-print should be run with an
6358 # additional relative path argument, like: $ python pretty_print.py
6359 # metadata/UMA/histograms.xml $ python pretty_print.py enums.xml
6360 if xml_dir == os.path.join('tools', 'metrics', 'histograms'):
6361 if os.path.basename(path) not in ('histograms.xml', 'enums.xml',
6362 'histogram_suffixes_list.xml'):
6363 # Skip this XML file if it's not one of the known types.
6364 continue
6365 cmd.append(path)
6366
6367 if opts.dry_run or opts.diff:
6368 cmd.append('--diff')
6369
6370 stdout = RunCommand(cmd, cwd=top_dir)
6371 if opts.diff:
6372 sys.stdout.write(stdout)
6373 if opts.dry_run and stdout:
6374 return_value = 2 # Not formatted.
6375 return return_value
6376
6377
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006378def MatchingFileType(file_name, extensions):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006379 """Returns True if the file name ends with one of the given extensions."""
6380 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006381
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006382
enne@chromium.org555cfe42014-01-29 18:21:39 +00006383@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006384@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006385def CMDformat(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006386 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Andrew Grieve3008d7e2023-10-03 14:18:22 +00006387 clang_exts = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto']
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006388 GN_EXTS = ['.gn', '.gni', '.typemap']
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006389 parser.add_option('--full',
6390 action='store_true',
6391 help='Reformat the full content of all touched files')
6392 parser.add_option('--upstream', help='Branch to check against')
6393 parser.add_option('--dry-run',
6394 action='store_true',
6395 help='Don\'t modify any file on disk.')
6396 parser.add_option(
6397 '--no-clang-format',
6398 dest='clang_format',
6399 action='store_false',
6400 default=True,
6401 help='Disables formatting of various file types using clang-format.')
6402 parser.add_option('--python',
6403 action='store_true',
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006404 help='Enables python formatting on all python files.')
6405 parser.add_option(
6406 '--no-python',
Andrew Grievecca48db2023-09-14 14:12:23 +00006407 action='store_false',
6408 dest='python',
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006409 help='Disables python formatting on all python files. '
6410 'If neither --python or --no-python are set, python files that have a '
6411 '.style.yapf file in an ancestor directory will be formatted. '
6412 'It is an error to set both.')
6413 parser.add_option('--js',
6414 action='store_true',
6415 help='Format javascript code with clang-format. '
6416 'Has no effect if --no-clang-format is set.')
6417 parser.add_option('--diff',
6418 action='store_true',
6419 help='Print diff to stdout rather than modifying files.')
6420 parser.add_option('--presubmit',
6421 action='store_true',
6422 help='Used when running the script from a presubmit.')
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006423
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006424 parser.add_option(
6425 '--rust-fmt',
6426 dest='use_rust_fmt',
6427 action='store_true',
6428 default=rustfmt.IsRustfmtSupported(),
6429 help='Enables formatting of Rust file types using rustfmt.')
6430 parser.add_option(
6431 '--no-rust-fmt',
6432 dest='use_rust_fmt',
6433 action='store_false',
6434 help='Disables formatting of Rust file types using rustfmt.')
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006435
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006436 parser.add_option(
6437 '--swift-format',
6438 dest='use_swift_format',
6439 action='store_true',
6440 default=swift_format.IsSwiftFormatSupported(),
6441 help='Enables formatting of Swift file types using swift-format '
6442 '(macOS host only).')
6443 parser.add_option(
6444 '--no-swift-format',
6445 dest='use_swift_format',
6446 action='store_false',
6447 help='Disables formatting of Swift file types using swift-format.')
Olivier Robin0a6b5442022-04-07 07:25:04 +00006448
Andrew Grieve3008d7e2023-10-03 14:18:22 +00006449 parser.add_option('--no-java',
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006450 action='store_true',
Andrew Grieve3008d7e2023-10-03 14:18:22 +00006451 help='Disable auto-formatting of .java')
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006452
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006453 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006454
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006455 # Normalize any remaining args against the current path, so paths relative
6456 # to the current directory are still resolved as expected.
6457 args = [os.path.join(os.getcwd(), arg) for arg in args]
Daniel Chengc55eecf2016-12-30 03:11:02 -08006458
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006459 # git diff generates paths against the root of the repository. Change
6460 # to that directory so clang-format can find files even within subdirs.
6461 rel_base_path = settings.GetRelativeRoot()
6462 if rel_base_path:
6463 os.chdir(rel_base_path)
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00006464
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006465 # Grab the merge-base commit, i.e. the upstream commit of the current
6466 # branch when it was created or the last time it was rebased. This is
6467 # to cover the case where the user may have called "git fetch origin",
6468 # moving the origin branch to a newer commit, but hasn't rebased yet.
6469 upstream_commit = None
6470 upstream_branch = opts.upstream
6471 if not upstream_branch:
6472 cl = Changelist()
6473 upstream_branch = cl.GetUpstreamBranch()
6474 if upstream_branch:
6475 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
6476 upstream_commit = upstream_commit.strip()
digit@chromium.org29e47272013-05-17 17:01:46 +00006477
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006478 if not upstream_commit:
6479 DieWithError('Could not find base commit for this branch. '
6480 'Are you in detached state?')
digit@chromium.org29e47272013-05-17 17:01:46 +00006481
Arthur Eubanks92d8c4e2023-10-09 19:57:24 +00006482 # Filter out copied/renamed/deleted files
6483 changed_files_cmd = BuildGitDiffCmd(['--name-only', '--diff-filter=crd'],
6484 upstream_commit, args)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006485 diff_output = RunGit(changed_files_cmd)
6486 diff_files = diff_output.splitlines()
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006487
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006488 if opts.js:
Andrew Grievecca48db2023-09-14 14:12:23 +00006489 clang_exts.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11006490
Andrew Grievecca48db2023-09-14 14:12:23 +00006491 formatters = [
6492 (GN_EXTS, _RunGnFormat),
6493 (['.xml'], _FormatXml),
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00006494 ]
Andrew Grieve3008d7e2023-10-03 14:18:22 +00006495 if not opts.no_java:
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006496 formatters += [(['.java'], _RunGoogleJavaFormat)]
Andrew Grievecca48db2023-09-14 14:12:23 +00006497 if opts.clang_format:
6498 formatters += [(clang_exts, _RunClangFormatDiff)]
6499 if opts.use_rust_fmt:
6500 formatters += [(['.rs'], _RunRustFmt)]
6501 if opts.use_swift_format:
6502 formatters += [(['.swift'], _RunSwiftFormat)]
6503 if opts.python is not False:
6504 formatters += [(['.py'], _RunYapf)]
digit@chromium.org29e47272013-05-17 17:01:46 +00006505
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006506 top_dir = settings.GetRoot()
Andrew Grievecca48db2023-09-14 14:12:23 +00006507 return_value = 0
6508 for file_types, format_func in formatters:
6509 paths = [p for p in diff_files if MatchingFileType(p, file_types)]
6510 if not paths:
6511 continue
6512 ret = format_func(opts, paths, top_dir, upstream_commit)
6513 return_value = return_value or ret
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006514
6515 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006516
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006517
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006518def GetMetricsDir(diff_xml):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006519 metrics_xml_dirs = [
6520 os.path.join('tools', 'metrics', 'actions'),
6521 os.path.join('tools', 'metrics', 'histograms'),
6522 os.path.join('tools', 'metrics', 'structured'),
6523 os.path.join('tools', 'metrics', 'ukm'),
6524 ]
6525 for xml_dir in metrics_xml_dirs:
6526 if diff_xml.startswith(xml_dir):
6527 return xml_dir
6528 return None
Steven Holte2e664bf2017-04-21 13:10:47 -07006529
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006530
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006531@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006532@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006533def CMDcheckout(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006534 """Checks out a branch associated with a given Gerrit issue."""
6535 _, args = parser.parse_args(args)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006536
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006537 if len(args) != 1:
6538 parser.print_help()
6539 return 1
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006540
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006541 issue_arg = ParseIssueNumberArgument(args[0])
6542 if not issue_arg.valid:
6543 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006544
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006545 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006546
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006547 output = RunGit([
6548 'config', '--local', '--get-regexp', r'branch\..*\.' + ISSUE_CONFIG_KEY
6549 ],
6550 error_ok=True)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006551
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006552 branches = []
6553 for key, issue in [x.split() for x in output.splitlines()]:
6554 if issue == target_issue:
6555 branches.append(
6556 re.sub(r'branch\.(.*)\.' + ISSUE_CONFIG_KEY, r'\1', key))
Edward Lemur52969c92020-02-06 18:15:28 +00006557
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006558 if len(branches) == 0:
6559 print('No branch found for issue %s.' % target_issue)
6560 return 1
6561 if len(branches) == 1:
6562 RunGit(['checkout', branches[0]])
6563 else:
6564 print('Multiple branches match issue %s:' % target_issue)
6565 for i in range(len(branches)):
6566 print('%d: %s' % (i, branches[i]))
6567 which = gclient_utils.AskForData('Choose by index: ')
6568 try:
6569 RunGit(['checkout', branches[int(which)]])
6570 except (IndexError, ValueError):
6571 print('Invalid selection, not checking out any branch.')
6572 return 1
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006573
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006574 return 0
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006575
6576
maruel@chromium.org29404b52014-09-08 22:58:00 +00006577def CMDlol(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006578 # This command is intentionally undocumented.
6579 print(
6580 zlib.decompress(
6581 base64.b64decode(
6582 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6583 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6584 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
6585 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')).decode('utf-8'))
6586 return 0
maruel@chromium.org29404b52014-09-08 22:58:00 +00006587
6588
Josip Sokcevic0399e172022-03-21 23:11:51 +00006589def CMDversion(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006590 import utils
6591 print(utils.depot_tools_version())
Josip Sokcevic0399e172022-03-21 23:11:51 +00006592
6593
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006594class OptionParser(optparse.OptionParser):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006595 """Creates the option parse and add --verbose support."""
6596 def __init__(self, *args, **kwargs):
6597 optparse.OptionParser.__init__(self,
6598 *args,
6599 prog='git cl',
6600 version=__version__,
6601 **kwargs)
6602 self.add_option('-v',
6603 '--verbose',
6604 action='count',
6605 default=0,
6606 help='Use 2 times for more debugging info')
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00006607
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006608 def parse_args(self, args=None, _values=None):
Joanna Wangc5b38322023-03-15 20:38:46 +00006609 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006610 return self._parse_args(args)
6611 finally:
6612 # Regardless of success or failure of args parsing, we want to
6613 # report metrics, but only after logging has been initialized (if
6614 # parsing succeeded).
6615 global settings
6616 settings = Settings()
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00006617
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006618 if metrics.collector.config.should_collect_metrics:
6619 try:
6620 # GetViewVCUrl ultimately calls logging method.
6621 project_url = settings.GetViewVCUrl().strip('/+')
6622 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
6623 metrics.collector.add('project_urls', [project_url])
6624 except subprocess2.CalledProcessError:
6625 # Occurs when command is not executed in a git repository
6626 # We should not fail here. If the command needs to be
6627 # executed in a repo, it will be raised later.
6628 pass
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006629
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006630 def _parse_args(self, args=None):
6631 # Create an optparse.Values object that will store only the actual
6632 # passed options, without the defaults.
6633 actual_options = optparse.Values()
6634 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
6635 # Create an optparse.Values object with the default options.
6636 options = optparse.Values(self.get_default_values().__dict__)
6637 # Update it with the options passed by the user.
6638 options._update_careful(actual_options.__dict__)
6639 # Store the options passed by the user in an _actual_options attribute.
6640 # We store only the keys, and not the values, since the values can
6641 # contain arbitrary information, which might be PII.
6642 metrics.collector.add('arguments', list(actual_options.__dict__.keys()))
Edward Lemur83bd7f42018-10-10 00:14:21 +00006643
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006644 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
6645 logging.basicConfig(
6646 level=levels[min(options.verbose,
6647 len(levels) - 1)],
6648 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6649 '%(filename)s] %(message)s')
6650
6651 return options, args
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006652
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006653
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006654def main(argv):
Gavin Mak7f5b53f2023-09-07 18:13:01 +00006655 if sys.version_info[0] < 3:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006656 print('\nYour Python version %s is unsupported, please upgrade.\n' %
6657 (sys.version.split(' ', 1)[0], ),
6658 file=sys.stderr)
6659 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006660
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006661 colorize_CMDstatus_doc()
6662 dispatcher = subcommand.CommandDispatcher(__name__)
6663 try:
6664 return dispatcher.execute(OptionParser(), argv)
6665 except auth.LoginRequiredError as e:
6666 DieWithError(str(e))
6667 except urllib.error.HTTPError as e:
6668 if e.code != 500:
6669 raise
6670 DieWithError((
6671 'App Engine is misbehaving and returned HTTP %d, again. Keep faith '
6672 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
6673 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006674
6675
6676if __name__ == '__main__':
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006677 # These affect sys.stdout, so do it outside of main() to simplify mocks in
6678 # the unit tests.
6679 fix_encoding.fix_encoding()
6680 setup_color.init()
6681 with metrics.collector.print_notice_and_exit():
6682 sys.exit(main(sys.argv[1:]))