blob: d1936eceaaabe31c4a110f548d29afe3323351bf [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.
646 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
647 diff_output = RunGit(diff_cmd)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000648
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000649 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
650 # 2 capture groups
651 # 0 == fname of diff file
652 # 1 == 'diff_start,diff_count' or 'diff_start'
653 # will match each of
654 # diff --git a/foo.foo b/foo.py
655 # @@ -12,2 +14,3 @@
656 # @@ -12,2 +17 @@
657 # running re.findall on the above string with pattern will give
658 # [('foo.py', ''), ('', '14,3'), ('', '17')]
Aiden Bennerc08566e2018-10-03 17:52:42 +0000659
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000660 curr_file = None
661 line_diffs = {}
662 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
663 if match[0] != '':
664 # Will match the second filename in diff --git a/a.py b/b.py.
665 curr_file = match[0]
666 line_diffs[curr_file] = []
667 else:
668 # Matches +14,3
669 if ',' in match[1]:
670 diff_start, diff_count = match[1].split(',')
671 else:
672 # Single line changes are of the form +12 instead of +12,1.
673 diff_start = match[1]
674 diff_count = 1
Aiden Bennerc08566e2018-10-03 17:52:42 +0000675
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000676 diff_start = int(diff_start)
677 diff_count = int(diff_count)
Andrew Grieved7ba85d2023-09-15 18:28:33 +0000678 diff_end = diff_start + diff_count - 1
Aiden Bennerc08566e2018-10-03 17:52:42 +0000679
Andrew Grieved7ba85d2023-09-15 18:28:33 +0000680 # Only format added ranges (not removed ones).
681 if diff_end >= diff_start:
682 line_diffs[curr_file].append((diff_start, diff_end))
Aiden Bennerc08566e2018-10-03 17:52:42 +0000683
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000684 return line_diffs
Aiden Bennerc08566e2018-10-03 17:52:42 +0000685
686
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000687def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000688 """Checks if a yapf file is in any parent directory of fpath until top_dir.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000689
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000690 Recursively checks parent directories to find yapf file and if no yapf file
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000691 is found returns None. Uses yapf_config_cache as a cache for previously found
692 configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000693 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000694 fpath = os.path.abspath(fpath)
695 # Return result if we've already computed it.
696 if fpath in yapf_config_cache:
697 return yapf_config_cache[fpath]
Aiden Bennerc08566e2018-10-03 17:52:42 +0000698
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000699 parent_dir = os.path.dirname(fpath)
700 if os.path.isfile(fpath):
701 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000702 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000703 # Otherwise fpath is a directory
704 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
705 if os.path.isfile(yapf_file):
706 ret = yapf_file
707 elif fpath in (top_dir, parent_dir):
708 # If we're at the top level directory, or if we're at root
709 # there is no provided style.
710 ret = None
711 else:
712 # Otherwise recurse on the current directory.
713 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
714 yapf_config_cache[fpath] = ret
715 return ret
Aiden Bennerc08566e2018-10-03 17:52:42 +0000716
717
Brian Sheedyb4307d52019-12-02 19:18:17 +0000718def _GetYapfIgnorePatterns(top_dir):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000719 """Returns all patterns in the .yapfignore file.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000720
721 yapf is supposed to handle the ignoring of files listed in .yapfignore itself,
722 but this functionality appears to break when explicitly passing files to
723 yapf for formatting. According to
Josip Sokcevicc39ab992020-09-24 20:09:15 +0000724 https://github.com/google/yapf/blob/HEAD/README.rst#excluding-files-from-formatting-yapfignore,
Brian Sheedy59b06a82019-10-14 17:03:29 +0000725 the .yapfignore file should be in the directory that yapf is invoked from,
726 which we assume to be the top level directory in this case.
727
728 Args:
729 top_dir: The top level directory for the repository being formatted.
730
731 Returns:
Brian Sheedyb4307d52019-12-02 19:18:17 +0000732 A set of all fnmatch patterns to be ignored.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000733 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000734 yapfignore_file = os.path.join(top_dir, '.yapfignore')
735 ignore_patterns = set()
736 if not os.path.exists(yapfignore_file):
737 return ignore_patterns
Brian Sheedy59b06a82019-10-14 17:03:29 +0000738
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000739 for line in gclient_utils.FileRead(yapfignore_file).split('\n'):
740 stripped_line = line.strip()
741 # Comments and blank lines should be ignored.
742 if stripped_line.startswith('#') or stripped_line == '':
743 continue
744 ignore_patterns.add(stripped_line)
745 return ignore_patterns
Brian Sheedyb4307d52019-12-02 19:18:17 +0000746
747
748def _FilterYapfIgnoredFiles(filepaths, patterns):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000749 """Filters out any filepaths that match any of the given patterns.
Brian Sheedyb4307d52019-12-02 19:18:17 +0000750
751 Args:
752 filepaths: An iterable of strings containing filepaths to filter.
753 patterns: An iterable of strings containing fnmatch patterns to filter on.
754
755 Returns:
756 A list of strings containing all the elements of |filepaths| that did not
757 match any of the patterns in |patterns|.
758 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000759 # Not inlined so that tests can use the same implementation.
760 return [
761 f for f in filepaths
762 if not any(fnmatch.fnmatch(f, p) for p in patterns)
763 ]
Brian Sheedy59b06a82019-10-14 17:03:29 +0000764
765
Daniel Cheng66d0f152023-08-29 23:21:58 +0000766def _GetCommitCountSummary(begin_commit: str,
767 end_commit: str = "HEAD") -> Optional[str]:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000768 """Generate a summary of the number of commits in (begin_commit, end_commit).
Daniel Cheng66d0f152023-08-29 23:21:58 +0000769
770 Returns a string containing the summary, or None if the range is empty.
771 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000772 count = int(
773 RunGitSilent(['rev-list', '--count', f'{begin_commit}..{end_commit}']))
Daniel Cheng66d0f152023-08-29 23:21:58 +0000774
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000775 if not count:
776 return None
Daniel Cheng66d0f152023-08-29 23:21:58 +0000777
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000778 return f'{count} commit{"s"[:count!=1]}'
Daniel Cheng66d0f152023-08-29 23:21:58 +0000779
780
Aaron Gable13101a62018-02-09 13:20:41 -0800781def print_stats(args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000782 """Prints statistics about the change to the user."""
783 # --no-ext-diff is broken in some versions of Git, so try to work around
784 # this by overriding the environment (but there is still a problem if the
785 # git config key "diff.external" is used).
786 env = GetNoGitPagerEnv()
787 if 'GIT_EXTERNAL_DIFF' in env:
788 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000789
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000790 return subprocess2.call(
791 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
792 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000793
794
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000795class BuildbucketResponseException(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000796 pass
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000797
798
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000799class Settings(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000800 def __init__(self):
801 self.cc = None
802 self.root = None
803 self.tree_status_url = None
804 self.viewvc_url = None
805 self.updated = False
806 self.is_gerrit = None
807 self.squash_gerrit_uploads = None
808 self.gerrit_skip_ensure_authenticated = None
809 self.git_editor = None
810 self.format_full_by_default = None
811 self.is_status_commit_order_by_date = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000812
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000813 def _LazyUpdateIfNeeded(self):
814 """Updates the settings from a codereview.settings file, if available."""
815 if self.updated:
816 return
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000817
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000818 # The only value that actually changes the behavior is
819 # autoupdate = "false". Everything else means "true".
820 autoupdate = (scm.GIT.GetConfig(self.GetRoot(), 'rietveld.autoupdate',
821 '').lower())
Edward Lemur26964072020-02-19 19:18:51 +0000822
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000823 cr_settings_file = FindCodereviewSettingsFile()
824 if autoupdate != 'false' and cr_settings_file:
825 LoadCodereviewSettingsFromFile(cr_settings_file)
826 cr_settings_file.close()
Edward Lemur26964072020-02-19 19:18:51 +0000827
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000828 self.updated = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000829
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000830 @staticmethod
831 def GetRelativeRoot():
832 return scm.GIT.GetCheckoutRoot('.')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000833
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000834 def GetRoot(self):
835 if self.root is None:
836 self.root = os.path.abspath(self.GetRelativeRoot())
837 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000838
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000839 def GetTreeStatusUrl(self, error_ok=False):
840 if not self.tree_status_url:
841 self.tree_status_url = self._GetConfig('rietveld.tree-status-url')
842 if self.tree_status_url is None and not error_ok:
843 DieWithError(
844 'You must configure your tree status URL by running '
845 '"git cl config".')
846 return self.tree_status_url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000847
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000848 def GetViewVCUrl(self):
849 if not self.viewvc_url:
850 self.viewvc_url = self._GetConfig('rietveld.viewvc-url')
851 return self.viewvc_url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000852
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000853 def GetBugPrefix(self):
854 return self._GetConfig('rietveld.bug-prefix')
rmistry@google.com78948ed2015-07-08 23:09:57 +0000855
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000856 def GetRunPostUploadHook(self):
857 run_post_upload_hook = self._GetConfig('rietveld.run-post-upload-hook')
858 return run_post_upload_hook == "True"
rmistry@google.com5626a922015-02-26 14:03:30 +0000859
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000860 def GetDefaultCCList(self):
861 return self._GetConfig('rietveld.cc')
Joanna Wangc8f23e22023-01-19 21:18:10 +0000862
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000863 def GetSquashGerritUploads(self):
864 """Returns True if uploads to Gerrit should be squashed by default."""
865 if self.squash_gerrit_uploads is None:
866 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
867 if self.squash_gerrit_uploads is None:
868 # Default is squash now (http://crbug.com/611892#c23).
869 self.squash_gerrit_uploads = self._GetConfig(
870 'gerrit.squash-uploads').lower() != 'false'
871 return self.squash_gerrit_uploads
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000872
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000873 def GetSquashGerritUploadsOverride(self):
874 """Return True or False if codereview.settings should be overridden.
Edward Lesmes4de54132020-05-05 19:41:33 +0000875
876 Returns None if no override has been defined.
877 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000878 # See also http://crbug.com/611892#c23
879 result = self._GetConfig('gerrit.override-squash-uploads').lower()
880 if result == 'true':
881 return True
882 if result == 'false':
883 return False
884 return None
Edward Lesmes4de54132020-05-05 19:41:33 +0000885
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000886 def GetIsGerrit(self):
887 """Return True if gerrit.host is set."""
888 if self.is_gerrit is None:
889 self.is_gerrit = bool(self._GetConfig('gerrit.host', False))
890 return self.is_gerrit
Aleksey Khoroshilov35ef5ad2022-06-03 18:29:25 +0000891
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000892 def GetGerritSkipEnsureAuthenticated(self):
893 """Return True if EnsureAuthenticated should not be done for Gerrit
tandrii@chromium.org28253532016-04-14 13:46:56 +0000894 uploads."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000895 if self.gerrit_skip_ensure_authenticated is None:
896 self.gerrit_skip_ensure_authenticated = self._GetConfig(
897 'gerrit.skip-ensure-authenticated').lower() == 'true'
898 return self.gerrit_skip_ensure_authenticated
tandrii@chromium.org28253532016-04-14 13:46:56 +0000899
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000900 def GetGitEditor(self):
901 """Returns the editor specified in the git config, or None if none is."""
902 if self.git_editor is None:
903 # Git requires single quotes for paths with spaces. We need to
904 # replace them with double quotes for Windows to treat such paths as
905 # a single path.
906 self.git_editor = self._GetConfig('core.editor').replace('\'', '"')
907 return self.git_editor or None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000908
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000909 def GetLintRegex(self):
910 return self._GetConfig('rietveld.cpplint-regex', DEFAULT_LINT_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000911
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000912 def GetLintIgnoreRegex(self):
913 return self._GetConfig('rietveld.cpplint-ignore-regex',
914 DEFAULT_LINT_IGNORE_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000915
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000916 def GetFormatFullByDefault(self):
917 if self.format_full_by_default is None:
918 self._LazyUpdateIfNeeded()
919 result = (RunGit(
920 ['config', '--bool', 'rietveld.format-full-by-default'],
921 error_ok=True).strip())
922 self.format_full_by_default = (result == 'true')
923 return self.format_full_by_default
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000924
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000925 def IsStatusCommitOrderByDate(self):
926 if self.is_status_commit_order_by_date is None:
927 result = (RunGit(['config', '--bool', 'cl.date-order'],
928 error_ok=True).strip())
929 self.is_status_commit_order_by_date = (result == 'true')
930 return self.is_status_commit_order_by_date
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +0000931
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000932 def _GetConfig(self, key, default=''):
933 self._LazyUpdateIfNeeded()
934 return scm.GIT.GetConfig(self.GetRoot(), key, default)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000935
936
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000937class _CQState(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000938 """Enum for states of CL with respect to CQ."""
939 NONE = 'none'
940 DRY_RUN = 'dry_run'
941 COMMIT = 'commit'
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000942
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000943 ALL_STATES = [NONE, DRY_RUN, COMMIT]
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000944
945
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000946class _ParsedIssueNumberArgument(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000947 def __init__(self, issue=None, patchset=None, hostname=None):
948 self.issue = issue
949 self.patchset = patchset
950 self.hostname = hostname
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000951
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000952 @property
953 def valid(self):
954 return self.issue is not None
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000955
956
Edward Lemurf38bc172019-09-03 21:02:13 +0000957def ParseIssueNumberArgument(arg):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000958 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
959 fail_result = _ParsedIssueNumberArgument()
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000960
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000961 if isinstance(arg, int):
962 return _ParsedIssueNumberArgument(issue=arg)
963 if not isinstance(arg, str):
964 return fail_result
Edward Lemur678a6842019-10-03 22:25:05 +0000965
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000966 if arg.isdigit():
967 return _ParsedIssueNumberArgument(issue=int(arg))
Aaron Gableaee6c852017-06-26 12:49:01 -0700968
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000969 url = gclient_utils.UpgradeToHttps(arg)
970 if not url.startswith('http'):
971 return fail_result
972 for gerrit_url, short_url in _KNOWN_GERRIT_TO_SHORT_URLS.items():
973 if url.startswith(short_url):
974 url = gerrit_url + url[len(short_url):]
975 break
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000976
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000977 try:
978 parsed_url = urllib.parse.urlparse(url)
979 except ValueError:
980 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +0200981
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000982 # If "https://" was automatically added, fail if `arg` looks unlikely to be
983 # a URL.
984 if not arg.startswith('http') and '.' not in parsed_url.netloc:
985 return fail_result
Alex Turner30ae6372022-01-04 02:32:52 +0000986
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000987 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
988 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
989 # Short urls like https://domain/<issue_number> can be used, but don't allow
990 # specifying the patchset (you'd 404), but we allow that here.
991 if parsed_url.path == '/':
992 part = parsed_url.fragment
993 else:
994 part = parsed_url.path
Edward Lemur678a6842019-10-03 22:25:05 +0000995
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000996 match = re.match(r'(/c(/.*/\+)?)?/(?P<issue>\d+)(/(?P<patchset>\d+)?/?)?$',
997 part)
998 if not match:
999 return fail_result
Edward Lemur678a6842019-10-03 22:25:05 +00001000
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001001 issue = int(match.group('issue'))
1002 patchset = match.group('patchset')
1003 return _ParsedIssueNumberArgument(
1004 issue=issue,
1005 patchset=int(patchset) if patchset else None,
1006 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001007
1008
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001009def _create_description_from_log(args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001010 """Pulls out the commit log to use as a base for the CL description."""
1011 log_args = []
1012 if len(args) == 1 and args[0] == None:
1013 # Handle the case where None is passed as the branch.
1014 return ''
1015 if len(args) == 1 and not args[0].endswith('.'):
1016 log_args = [args[0] + '..']
1017 elif len(args) == 1 and args[0].endswith('...'):
1018 log_args = [args[0][:-1]]
1019 elif len(args) == 2:
1020 log_args = [args[0] + '..' + args[1]]
1021 else:
1022 log_args = args[:] # Hope for the best!
1023 return RunGit(['log', '--pretty=format:%B%n'] + log_args)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001024
1025
Aaron Gablea45ee112016-11-22 15:14:38 -08001026class GerritChangeNotExists(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001027 def __init__(self, issue, url):
1028 self.issue = issue
1029 self.url = url
1030 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001031
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001032 def __str__(self):
1033 return 'change %s at %s does not exist or you have no access to it' % (
1034 self.issue, self.url)
tandriic2405f52016-10-10 08:13:15 -07001035
1036
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001037_CommentSummary = collections.namedtuple(
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001038 '_CommentSummary',
1039 [
1040 'date',
1041 'message',
1042 'sender',
1043 'autogenerated',
1044 # TODO(tandrii): these two aren't known in Gerrit.
1045 'approval',
1046 'disapproval'
1047 ])
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001048
Joanna Wang6215dd02023-02-07 15:58:03 +00001049# TODO(b/265929888): Change `parent` to `pushed_commit_base`.
Joanna Wange8523912023-01-21 02:05:40 +00001050_NewUpload = collections.namedtuple('NewUpload', [
Joanna Wang40497912023-01-24 21:18:16 +00001051 'reviewers', 'ccs', 'commit_to_push', 'new_last_uploaded_commit', 'parent',
Joanna Wang7603f042023-03-01 22:17:36 +00001052 'change_desc', 'prev_patchset'
Joanna Wange8523912023-01-21 02:05:40 +00001053])
1054
1055
Daniel Chengabf48472023-08-30 15:45:13 +00001056class ChangeDescription(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001057 """Contains a parsed form of the change description."""
1058 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
1059 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
1060 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
1061 FIXED_LINE = r'^[ \t]*Fixed[ \t]*:[ \t]*(.*?)[ \t]*$'
1062 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
1063 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
1064 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
1065 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):($|[^:])'
1066 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
Daniel Chengabf48472023-08-30 15:45:13 +00001067
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001068 def __init__(self, description, bug=None, fixed=None):
1069 self._description_lines = (description or '').strip().splitlines()
1070 if bug:
1071 regexp = re.compile(self.BUG_LINE)
1072 prefix = settings.GetBugPrefix()
1073 if not any(
1074 (regexp.match(line) for line in self._description_lines)):
1075 values = list(_get_bug_line_values(prefix, bug))
1076 self.append_footer('Bug: %s' % ', '.join(values))
1077 if fixed:
1078 regexp = re.compile(self.FIXED_LINE)
1079 prefix = settings.GetBugPrefix()
1080 if not any(
1081 (regexp.match(line) for line in self._description_lines)):
1082 values = list(_get_bug_line_values(prefix, fixed))
1083 self.append_footer('Fixed: %s' % ', '.join(values))
Daniel Chengabf48472023-08-30 15:45:13 +00001084
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001085 @property # www.logilab.org/ticket/89786
1086 def description(self): # pylint: disable=method-hidden
1087 return '\n'.join(self._description_lines)
Daniel Chengabf48472023-08-30 15:45:13 +00001088
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001089 def set_description(self, desc):
1090 if isinstance(desc, str):
1091 lines = desc.splitlines()
1092 else:
1093 lines = [line.rstrip() for line in desc]
1094 while lines and not lines[0]:
1095 lines.pop(0)
1096 while lines and not lines[-1]:
1097 lines.pop(-1)
1098 self._description_lines = lines
Daniel Chengabf48472023-08-30 15:45:13 +00001099
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001100 def ensure_change_id(self, change_id):
1101 description = self.description
1102 footer_change_ids = git_footers.get_footer_change_id(description)
1103 # Make sure that the Change-Id in the description matches the given one.
1104 if footer_change_ids != [change_id]:
1105 if footer_change_ids:
1106 # Remove any existing Change-Id footers since they don't match
1107 # the expected change_id footer.
1108 description = git_footers.remove_footer(description,
1109 'Change-Id')
1110 print(
1111 'WARNING: Change-Id has been set to %s. Use `git cl issue 0` '
1112 'if you want to set a new one.')
1113 # Add the expected Change-Id footer.
1114 description = git_footers.add_footer_change_id(
1115 description, change_id)
1116 self.set_description(description)
Daniel Chengabf48472023-08-30 15:45:13 +00001117
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001118 def update_reviewers(self, reviewers):
1119 """Rewrites the R= line(s) as a single line each.
Daniel Chengabf48472023-08-30 15:45:13 +00001120
1121 Args:
1122 reviewers (list(str)) - list of additional emails to use for reviewers.
1123 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001124 if not reviewers:
1125 return
Daniel Chengabf48472023-08-30 15:45:13 +00001126
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001127 reviewers = set(reviewers)
Daniel Chengabf48472023-08-30 15:45:13 +00001128
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001129 # Get the set of R= lines and remove them from the description.
1130 regexp = re.compile(self.R_LINE)
1131 matches = [regexp.match(line) for line in self._description_lines]
1132 new_desc = [
1133 l for i, l in enumerate(self._description_lines) if not matches[i]
1134 ]
1135 self.set_description(new_desc)
Daniel Chengabf48472023-08-30 15:45:13 +00001136
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001137 # Construct new unified R= lines.
Daniel Chengabf48472023-08-30 15:45:13 +00001138
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001139 # First, update reviewers with names from the R= lines (if any).
1140 for match in matches:
1141 if not match:
1142 continue
1143 reviewers.update(cleanup_list([match.group(2).strip()]))
Daniel Chengabf48472023-08-30 15:45:13 +00001144
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001145 new_r_line = 'R=' + ', '.join(sorted(reviewers))
Daniel Chengabf48472023-08-30 15:45:13 +00001146
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001147 # Put the new lines in the description where the old first R= line was.
1148 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1149 if 0 <= line_loc < len(self._description_lines):
1150 self._description_lines.insert(line_loc, new_r_line)
1151 else:
1152 self.append_footer(new_r_line)
Daniel Chengabf48472023-08-30 15:45:13 +00001153
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001154 def set_preserve_tryjobs(self):
1155 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
1156 footers = git_footers.parse_footers(self.description)
1157 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
1158 if v.lower() == 'true':
1159 return
1160 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
Daniel Chengabf48472023-08-30 15:45:13 +00001161
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001162 def prompt(self):
1163 """Asks the user to update the description."""
1164 self.set_description([
1165 '# Enter a description of the change.',
1166 '# This will be displayed on the codereview site.',
1167 '# The first line will also be used as the subject of the review.',
1168 '#--------------------This line is 72 characters long'
1169 '--------------------',
1170 ] + self._description_lines)
1171 bug_regexp = re.compile(self.BUG_LINE)
1172 fixed_regexp = re.compile(self.FIXED_LINE)
1173 prefix = settings.GetBugPrefix()
1174 has_issue = lambda l: bug_regexp.match(l) or fixed_regexp.match(l)
Daniel Chengabf48472023-08-30 15:45:13 +00001175
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001176 if not any((has_issue(line) for line in self._description_lines)):
1177 self.append_footer('Bug: %s' % prefix)
Daniel Chengabf48472023-08-30 15:45:13 +00001178
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001179 print('Waiting for editor...')
1180 content = gclient_utils.RunEditor(self.description,
1181 True,
1182 git_editor=settings.GetGitEditor())
1183 if not content:
1184 DieWithError('Running editor failed')
1185 lines = content.splitlines()
Daniel Chengabf48472023-08-30 15:45:13 +00001186
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001187 # Strip off comments and default inserted "Bug:" line.
1188 clean_lines = [
1189 line.rstrip() for line in lines
1190 if not (line.startswith('#') or line.rstrip() == "Bug:"
1191 or line.rstrip() == "Bug: " + prefix)
1192 ]
1193 if not clean_lines:
1194 DieWithError('No CL description, aborting')
1195 self.set_description(clean_lines)
Daniel Chengabf48472023-08-30 15:45:13 +00001196
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001197 def append_footer(self, line):
1198 """Adds a footer line to the description.
Daniel Chengabf48472023-08-30 15:45:13 +00001199
1200 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
1201 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
1202 that Gerrit footers are always at the end.
1203 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001204 parsed_footer_line = git_footers.parse_footer(line)
1205 if parsed_footer_line:
1206 # Line is a gerrit footer in the form: Footer-Key: any value.
1207 # Thus, must be appended observing Gerrit footer rules.
1208 self.set_description(
1209 git_footers.add_footer(self.description,
1210 key=parsed_footer_line[0],
1211 value=parsed_footer_line[1]))
1212 return
Daniel Chengabf48472023-08-30 15:45:13 +00001213
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001214 if not self._description_lines:
1215 self._description_lines.append(line)
1216 return
Daniel Chengabf48472023-08-30 15:45:13 +00001217
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001218 top_lines, gerrit_footers, _ = git_footers.split_footers(
1219 self.description)
1220 if gerrit_footers:
1221 # git_footers.split_footers ensures that there is an empty line
1222 # before actual (gerrit) footers, if any. We have to keep it that
1223 # way.
1224 assert top_lines and top_lines[-1] == ''
1225 top_lines, separator = top_lines[:-1], top_lines[-1:]
1226 else:
1227 separator = [
1228 ] # No need for separator if there are no gerrit_footers.
Daniel Chengabf48472023-08-30 15:45:13 +00001229
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001230 prev_line = top_lines[-1] if top_lines else ''
1231 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line)
1232 or not presubmit_support.Change.TAG_LINE_RE.match(line)):
1233 top_lines.append('')
1234 top_lines.append(line)
1235 self._description_lines = top_lines + separator + gerrit_footers
Daniel Chengabf48472023-08-30 15:45:13 +00001236
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001237 def get_reviewers(self, tbr_only=False):
1238 """Retrieves the list of reviewers."""
1239 matches = [
1240 re.match(self.R_LINE, line) for line in self._description_lines
1241 ]
1242 reviewers = [
1243 match.group(2).strip() for match in matches
1244 if match and (not tbr_only or match.group(1).upper() == 'TBR')
1245 ]
1246 return cleanup_list(reviewers)
Daniel Chengabf48472023-08-30 15:45:13 +00001247
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001248 def get_cced(self):
1249 """Retrieves the list of reviewers."""
1250 matches = [
1251 re.match(self.CC_LINE, line) for line in self._description_lines
1252 ]
1253 cced = [match.group(2).strip() for match in matches if match]
1254 return cleanup_list(cced)
Daniel Chengabf48472023-08-30 15:45:13 +00001255
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001256 def get_hash_tags(self):
1257 """Extracts and sanitizes a list of Gerrit hashtags."""
1258 subject = (self._description_lines or ('', ))[0]
1259 subject = re.sub(self.STRIP_HASH_TAG_PREFIX,
1260 '',
1261 subject,
1262 flags=re.IGNORECASE)
Daniel Chengabf48472023-08-30 15:45:13 +00001263
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001264 tags = []
1265 start = 0
1266 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
1267 while True:
1268 m = bracket_exp.match(subject, start)
1269 if not m:
1270 break
1271 tags.append(self.sanitize_hash_tag(m.group(1)))
1272 start = m.end()
Daniel Chengabf48472023-08-30 15:45:13 +00001273
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001274 if not tags:
1275 # Try "Tag: " prefix.
1276 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
1277 if m:
1278 tags.append(self.sanitize_hash_tag(m.group(1)))
1279 return tags
Daniel Chengabf48472023-08-30 15:45:13 +00001280
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001281 @classmethod
1282 def sanitize_hash_tag(cls, tag):
1283 """Returns a sanitized Gerrit hash tag.
Daniel Chengabf48472023-08-30 15:45:13 +00001284
1285 A sanitized hashtag can be used as a git push refspec parameter value.
1286 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001287 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
Daniel Chengabf48472023-08-30 15:45:13 +00001288
1289
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001290class Changelist(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001291 """Changelist works with one changelist in local branch.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001292
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001293 Notes:
1294 * Not safe for concurrent multi-{thread,process} use.
1295 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001296 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001297 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001298 def __init__(self,
1299 branchref=None,
1300 issue=None,
1301 codereview_host=None,
1302 commit_date=None):
1303 """Create a new ChangeList instance.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001304
Edward Lemurf38bc172019-09-03 21:02:13 +00001305 **kwargs will be passed directly to Gerrit implementation.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001306 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001307 # Poke settings so we get the "configure your server" message if
1308 # necessary.
1309 global settings
1310 if not settings:
1311 # Happens when git_cl.py is used as a utility library.
1312 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001313
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001314 self.branchref = branchref
1315 if self.branchref:
1316 assert branchref.startswith('refs/heads/')
1317 self.branch = scm.GIT.ShortBranchName(self.branchref)
1318 else:
1319 self.branch = None
1320 self.commit_date = commit_date
1321 self.upstream_branch = None
1322 self.lookedup_issue = False
1323 self.issue = issue or None
1324 self.description = None
1325 self.lookedup_patchset = False
1326 self.patchset = None
1327 self.cc = None
1328 self.more_cc = []
1329 self._remote = None
1330 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001331
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001332 # Lazily cached values.
1333 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1334 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1335 self._owners_client = None
1336 # Map from change number (issue) to its detail cache.
1337 self._detail_cache = {}
Edward Lemur125d60a2019-09-13 18:25:41 +00001338
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001339 if codereview_host is not None:
1340 assert not codereview_host.startswith('https://'), codereview_host
1341 self._gerrit_host = codereview_host
1342 self._gerrit_server = 'https://%s' % codereview_host
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001343
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001344 @property
1345 def owners_client(self):
1346 if self._owners_client is None:
1347 remote, remote_branch = self.GetRemoteBranch()
1348 branch = GetTargetRef(remote, remote_branch, None)
1349 self._owners_client = owners_client.GetCodeOwnersClient(
1350 host=self.GetGerritHost(),
1351 project=self.GetGerritProject(),
1352 branch=branch)
1353 return self._owners_client
Edward Lesmese1576912021-02-16 21:53:34 +00001354
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001355 def GetCCList(self):
1356 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001357
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001358 The return value is a string suitable for passing to git cl with the --cc
1359 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001360 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001361 if self.cc is None:
1362 base_cc = settings.GetDefaultCCList()
1363 more_cc = ','.join(self.more_cc)
1364 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1365 return self.cc
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001366
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001367 def ExtendCC(self, more_cc):
1368 """Extends the list of users to cc on this CL based on the changed files."""
1369 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001370
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001371 def GetCommitDate(self):
1372 """Returns the commit date as provided in the constructor"""
1373 return self.commit_date
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00001374
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001375 def GetBranch(self):
1376 """Returns the short branch name, e.g. 'main'."""
1377 if not self.branch:
1378 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
1379 if not branchref:
1380 return None
1381 self.branchref = branchref
1382 self.branch = scm.GIT.ShortBranchName(self.branchref)
1383 return self.branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001384
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001385 def GetBranchRef(self):
1386 """Returns the full branch name, e.g. 'refs/heads/main'."""
1387 self.GetBranch() # Poke the lazy loader.
1388 return self.branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001389
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001390 def _GitGetBranchConfigValue(self, key, default=None):
1391 return scm.GIT.GetBranchConfig(settings.GetRoot(), self.GetBranch(),
1392 key, default)
tandrii5d48c322016-08-18 16:19:37 -07001393
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001394 def _GitSetBranchConfigValue(self, key, value):
1395 action = 'set %s to %r' % (key, value)
1396 if not value:
1397 action = 'unset %s' % key
1398 assert self.GetBranch(), 'a branch is needed to ' + action
1399 return scm.GIT.SetBranchConfig(settings.GetRoot(), self.GetBranch(),
1400 key, value)
tandrii5d48c322016-08-18 16:19:37 -07001401
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001402 @staticmethod
1403 def FetchUpstreamTuple(branch):
1404 """Returns a tuple containing remote and remote ref,
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001405 e.g. 'origin', 'refs/heads/main'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001406 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001407 remote, upstream_branch = scm.GIT.FetchUpstreamTuple(
1408 settings.GetRoot(), branch)
1409 if not remote or not upstream_branch:
1410 DieWithError(
1411 'Unable to determine default branch to diff against.\n'
1412 'Verify this branch is set up to track another \n'
1413 '(via the --track argument to "git checkout -b ..."). \n'
1414 'or pass complete "git diff"-style arguments if supported, like\n'
1415 ' git cl upload origin/main\n')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001417 return remote, upstream_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001418
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001419 def GetCommonAncestorWithUpstream(self):
1420 upstream_branch = self.GetUpstreamBranch()
1421 if not scm.GIT.IsValidRevision(settings.GetRoot(), upstream_branch):
1422 DieWithError(
Joanna Wangd4dfff02023-09-13 17:44:31 +00001423 'The current branch (%s) has an upstream (%s) that does not exist '
1424 'anymore.\nPlease fix it and try again.' %
1425 (self.GetBranch(), upstream_branch))
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001426 return git_common.get_or_create_merge_base(self.GetBranch(),
1427 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001428
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001429 def GetUpstreamBranch(self):
1430 if self.upstream_branch is None:
1431 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1432 if remote != '.':
1433 upstream_branch = upstream_branch.replace(
1434 'refs/heads/', 'refs/remotes/%s/' % remote)
1435 upstream_branch = upstream_branch.replace(
1436 'refs/branch-heads/', 'refs/remotes/branch-heads/')
1437 self.upstream_branch = upstream_branch
1438 return self.upstream_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001439
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001440 def GetRemoteBranch(self):
1441 if not self._remote:
1442 remote, branch = None, self.GetBranch()
1443 seen_branches = set()
1444 while branch not in seen_branches:
1445 seen_branches.add(branch)
1446 remote, branch = self.FetchUpstreamTuple(branch)
1447 branch = scm.GIT.ShortBranchName(branch)
1448 if remote != '.' or branch.startswith('refs/remotes'):
1449 break
1450 else:
1451 remotes = RunGit(['remote'], error_ok=True).split()
1452 if len(remotes) == 1:
1453 remote, = remotes
1454 elif 'origin' in remotes:
1455 remote = 'origin'
1456 logging.warning(
1457 'Could not determine which remote this change is '
1458 'associated with, so defaulting to "%s".' %
1459 self._remote)
1460 else:
1461 logging.warning(
1462 'Could not determine which remote this change is '
1463 'associated with.')
1464 branch = 'HEAD'
1465 if branch.startswith('refs/remotes'):
1466 self._remote = (remote, branch)
1467 elif branch.startswith('refs/branch-heads/'):
1468 self._remote = (remote, branch.replace('refs/',
1469 'refs/remotes/'))
1470 else:
1471 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
1472 return self._remote
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001473
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001474 def GetRemoteUrl(self) -> Optional[str]:
1475 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001476
1477 Returns None if there is no remote.
1478 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001479 is_cached, value = self._cached_remote_url
1480 if is_cached:
1481 return value
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001482
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001483 remote, _ = self.GetRemoteBranch()
1484 url = scm.GIT.GetConfig(settings.GetRoot(), 'remote.%s.url' % remote,
1485 '')
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001486
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001487 # Check if the remote url can be parsed as an URL.
1488 host = urllib.parse.urlparse(url).netloc
1489 if host:
1490 self._cached_remote_url = (True, url)
1491 return url
Edward Lemur298f2cf2019-02-22 21:40:39 +00001492
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001493 # If it cannot be parsed as an url, assume it is a local directory,
1494 # probably a git cache.
1495 logging.warning(
1496 '"%s" doesn\'t appear to point to a git host. '
1497 'Interpreting it as a local directory.', url)
1498 if not os.path.isdir(url):
1499 logging.error(
1500 'Remote "%(remote)s" for branch "%(branch)s" points to "%(url)s", '
1501 'but it doesn\'t exist.', {
1502 'remote': remote,
1503 'branch': self.GetBranch(),
1504 'url': url
1505 })
1506 return None
Edward Lemur298f2cf2019-02-22 21:40:39 +00001507
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001508 cache_path = url
1509 url = scm.GIT.GetConfig(url, 'remote.%s.url' % remote, '')
Edward Lemur298f2cf2019-02-22 21:40:39 +00001510
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001511 host = urllib.parse.urlparse(url).netloc
1512 if not host:
1513 logging.error(
1514 'Remote "%(remote)s" for branch "%(branch)s" points to '
1515 '"%(cache_path)s", but it is misconfigured.\n'
1516 '"%(cache_path)s" must be a git repo and must have a remote named '
1517 '"%(remote)s" pointing to the git host.', {
1518 'remote': remote,
1519 'cache_path': cache_path,
1520 'branch': self.GetBranch()
1521 })
1522 return None
Edward Lemur298f2cf2019-02-22 21:40:39 +00001523
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001524 self._cached_remote_url = (True, url)
1525 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001526
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001527 def GetIssue(self):
1528 """Returns the issue number as a int or None if not set."""
1529 if self.issue is None and not self.lookedup_issue:
1530 if self.GetBranch():
1531 self.issue = self._GitGetBranchConfigValue(ISSUE_CONFIG_KEY)
1532 if self.issue is not None:
1533 self.issue = int(self.issue)
1534 self.lookedup_issue = True
1535 return self.issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001536
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001537 def GetIssueURL(self, short=False):
1538 """Get the URL for a particular issue."""
1539 issue = self.GetIssue()
1540 if not issue:
1541 return None
1542 server = self.GetCodereviewServer()
1543 if short:
1544 server = _KNOWN_GERRIT_TO_SHORT_URLS.get(server, server)
1545 return '%s/%s' % (server, issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001546
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001547 def FetchDescription(self, pretty=False):
1548 assert self.GetIssue(), 'issue is required to query Gerrit'
Edward Lemur6c6827c2020-02-06 21:15:18 +00001549
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001550 if self.description is None:
1551 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
1552 current_rev = data['current_revision']
1553 self.description = data['revisions'][current_rev]['commit'][
1554 'message']
Edward Lemur6c6827c2020-02-06 21:15:18 +00001555
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001556 if not pretty:
1557 return self.description
Edward Lemur6c6827c2020-02-06 21:15:18 +00001558
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001559 # Set width to 72 columns + 2 space indent.
1560 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
1561 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1562 lines = self.description.splitlines()
1563 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001564
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001565 def GetPatchset(self):
1566 """Returns the patchset number as a int or None if not set."""
1567 if self.patchset is None and not self.lookedup_patchset:
1568 if self.GetBranch():
1569 self.patchset = self._GitGetBranchConfigValue(
1570 PATCHSET_CONFIG_KEY)
1571 if self.patchset is not None:
1572 self.patchset = int(self.patchset)
1573 self.lookedup_patchset = True
1574 return self.patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001575
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001576 def GetAuthor(self):
1577 return scm.GIT.GetConfig(settings.GetRoot(), 'user.email')
Edward Lemur9aa1a962020-02-25 00:58:38 +00001578
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001579 def SetPatchset(self, patchset):
1580 """Set this branch's patchset. If patchset=0, clears the patchset."""
1581 assert self.GetBranch()
1582 if not patchset:
1583 self.patchset = None
1584 else:
1585 self.patchset = int(patchset)
1586 self._GitSetBranchConfigValue(PATCHSET_CONFIG_KEY, str(self.patchset))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001587
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001588 def SetIssue(self, issue=None):
1589 """Set this branch's issue. If issue isn't given, clears the issue."""
1590 assert self.GetBranch()
1591 if issue:
1592 issue = int(issue)
1593 self._GitSetBranchConfigValue(ISSUE_CONFIG_KEY, str(issue))
1594 self.issue = issue
1595 codereview_server = self.GetCodereviewServer()
1596 if codereview_server:
1597 self._GitSetBranchConfigValue(CODEREVIEW_SERVER_CONFIG_KEY,
1598 codereview_server)
1599 else:
1600 # Reset all of these just to be clean.
1601 reset_suffixes = [
1602 LAST_UPLOAD_HASH_CONFIG_KEY,
1603 ISSUE_CONFIG_KEY,
1604 PATCHSET_CONFIG_KEY,
1605 CODEREVIEW_SERVER_CONFIG_KEY,
1606 GERRIT_SQUASH_HASH_CONFIG_KEY,
1607 ]
1608 for prop in reset_suffixes:
1609 try:
1610 self._GitSetBranchConfigValue(prop, None)
1611 except subprocess2.CalledProcessError:
1612 pass
1613 msg = RunGit(['log', '-1', '--format=%B']).strip()
1614 if msg and git_footers.get_footer_change_id(msg):
1615 print(
1616 'WARNING: The change patched into this branch has a Change-Id. '
1617 'Removing it.')
1618 RunGit([
1619 'commit', '--amend', '-m',
1620 git_footers.remove_footer(msg, 'Change-Id')
1621 ])
1622 self.lookedup_issue = True
1623 self.issue = None
1624 self.patchset = None
1625
1626 def GetAffectedFiles(self,
1627 upstream: str,
1628 end_commit: Optional[str] = None) -> Sequence[str]:
1629 """Returns the list of affected files for the given commit range."""
Edward Lemur85153282020-02-14 22:06:29 +00001630 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001631 return [
1632 f for _, f in scm.GIT.CaptureStatus(
1633 settings.GetRoot(), upstream, end_commit=end_commit)
1634 ]
Edward Lemur85153282020-02-14 22:06:29 +00001635 except subprocess2.CalledProcessError:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001636 DieWithError(
1637 ('\nFailed to diff against upstream branch %s\n\n'
1638 'This branch probably doesn\'t exist anymore. To reset the\n'
1639 'tracking branch, please run\n'
1640 ' git branch --set-upstream-to origin/main %s\n'
1641 'or replace origin/main with the relevant branch') %
1642 (upstream, self.GetBranch()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001643
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001644 def UpdateDescription(self, description, force=False):
1645 assert self.GetIssue(), 'issue is required to update description'
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001646
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001647 if gerrit_util.HasPendingChangeEdit(self.GetGerritHost(),
1648 self._GerritChangeIdentifier()):
1649 if not force:
1650 confirm_or_exit(
1651 'The description cannot be modified while the issue has a pending '
1652 'unpublished edit. Either publish the edit in the Gerrit web UI '
1653 'or delete it.\n\n',
1654 action='delete the unpublished edit')
Edward Lemur6c6827c2020-02-06 21:15:18 +00001655
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001656 gerrit_util.DeletePendingChangeEdit(self.GetGerritHost(),
1657 self._GerritChangeIdentifier())
1658 gerrit_util.SetCommitMessage(self.GetGerritHost(),
1659 self._GerritChangeIdentifier(),
1660 description,
1661 notify='NONE')
Edward Lemur6c6827c2020-02-06 21:15:18 +00001662
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001663 self.description = description
Edward Lemur6c6827c2020-02-06 21:15:18 +00001664
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001665 def _GetCommonPresubmitArgs(self, verbose, upstream):
1666 args = [
1667 '--root',
1668 settings.GetRoot(),
1669 '--upstream',
1670 upstream,
1671 ]
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001672
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001673 args.extend(['--verbose'] * verbose)
Edward Lemur227d5102020-02-25 23:45:35 +00001674
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001675 remote, remote_branch = self.GetRemoteBranch()
1676 target_ref = GetTargetRef(remote, remote_branch, None)
1677 if settings.GetIsGerrit():
1678 args.extend(['--gerrit_url', self.GetCodereviewServer()])
1679 args.extend(['--gerrit_project', self.GetGerritProject()])
1680 args.extend(['--gerrit_branch', target_ref])
Edward Lemur227d5102020-02-25 23:45:35 +00001681
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001682 author = self.GetAuthor()
1683 issue = self.GetIssue()
1684 patchset = self.GetPatchset()
1685 if author:
1686 args.extend(['--author', author])
1687 if issue:
1688 args.extend(['--issue', str(issue)])
1689 if patchset:
1690 args.extend(['--patchset', str(patchset)])
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001691
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001692 return args
Edward Lemur227d5102020-02-25 23:45:35 +00001693
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001694 def RunHook(self,
1695 committing,
1696 may_prompt,
1697 verbose,
1698 parallel,
1699 upstream,
1700 description,
1701 all_files,
1702 files=None,
1703 resultdb=False,
1704 realm=None):
1705 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1706 args = self._GetCommonPresubmitArgs(verbose, upstream)
1707 args.append('--commit' if committing else '--upload')
1708 if may_prompt:
1709 args.append('--may_prompt')
1710 if parallel:
1711 args.append('--parallel')
1712 if all_files:
1713 args.append('--all_files')
1714 if files:
1715 args.extend(files.split(';'))
1716 args.append('--source_controlled_only')
1717 if files or all_files:
1718 args.append('--no_diffs')
Edward Lemur75526302020-02-27 22:31:05 +00001719
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001720 if resultdb and not realm:
1721 # TODO (crbug.com/1113463): store realm somewhere and look it up so
1722 # it is not required to pass the realm flag
1723 print(
1724 'Note: ResultDB reporting will NOT be performed because --realm'
1725 ' was not specified. To enable ResultDB, please run the command'
1726 ' again with the --realm argument to specify the LUCI realm.')
Edward Lemur227d5102020-02-25 23:45:35 +00001727
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001728 return self._RunPresubmit(args,
1729 description,
1730 resultdb=resultdb,
1731 realm=realm)
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001732
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001733 def _RunPresubmit(self,
1734 args: Sequence[str],
1735 description: str,
1736 resultdb: bool = False,
1737 realm: Optional[str] = None) -> Mapping[str, Any]:
1738 args = list(args)
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001739
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001740 with gclient_utils.temporary_file() as description_file:
1741 with gclient_utils.temporary_file() as json_output:
1742 gclient_utils.FileWrite(description_file, description)
1743 args.extend(['--json_output', json_output])
1744 args.extend(['--description_file', description_file])
1745 start = time_time()
1746 cmd = ['vpython3', PRESUBMIT_SUPPORT] + args
1747 if resultdb and realm:
1748 cmd = ['rdb', 'stream', '-new', '-realm', realm, '--'] + cmd
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001749
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001750 p = subprocess2.Popen(cmd)
1751 exit_code = p.wait()
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001752
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001753 metrics.collector.add_repeated(
1754 'sub_commands', {
1755 'command': 'presubmit',
1756 'execution_time': time_time() - start,
1757 'exit_code': exit_code,
1758 })
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001759
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001760 if exit_code:
1761 sys.exit(exit_code)
Edward Lemur227d5102020-02-25 23:45:35 +00001762
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001763 json_results = gclient_utils.FileRead(json_output)
1764 return json.loads(json_results)
Edward Lemur227d5102020-02-25 23:45:35 +00001765
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001766 def RunPostUploadHook(self, verbose, upstream, description):
1767 args = self._GetCommonPresubmitArgs(verbose, upstream)
1768 args.append('--post_upload')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001769
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001770 with gclient_utils.temporary_file() as description_file:
1771 gclient_utils.FileWrite(description_file, description)
1772 args.extend(['--description_file', description_file])
1773 subprocess2.Popen(['vpython3', PRESUBMIT_SUPPORT] + args).wait()
Edward Lemur75526302020-02-27 22:31:05 +00001774
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001775 def _GetDescriptionForUpload(self, options: optparse.Values,
1776 git_diff_args: Sequence[str],
1777 files: Sequence[str]) -> ChangeDescription:
1778 """Get description message for upload."""
1779 if self.GetIssue():
1780 description = self.FetchDescription()
1781 elif options.message:
1782 description = options.message
1783 else:
1784 description = _create_description_from_log(git_diff_args)
1785 if options.title and options.squash:
1786 description = options.title + '\n\n' + description
Edward Lemur75526302020-02-27 22:31:05 +00001787
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001788 bug = options.bug
1789 fixed = options.fixed
1790 if not self.GetIssue():
1791 # Extract bug number from branch name, but only if issue is being
1792 # created. It must start with bug or fix, followed by _ or - and
1793 # number. Optionally, it may contain _ or - after number with
1794 # arbitrary text. Examples: bug-123 bug_123 fix-123
1795 # fix-123-some-description
1796 branch = self.GetBranch()
1797 if branch is not None:
1798 match = re.match(
1799 r'^(?P<type>bug|fix(?:e[sd])?)[_-]?(?P<bugnum>\d+)([-_]|$)',
1800 branch)
1801 if not bug and not fixed and match:
1802 if match.group('type') == 'bug':
1803 bug = match.group('bugnum')
1804 else:
1805 fixed = match.group('bugnum')
Edward Lemur5a644f82020-03-18 16:44:57 +00001806
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001807 change_description = ChangeDescription(description, bug, fixed)
Edward Lemur5a644f82020-03-18 16:44:57 +00001808
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001809 # Fill gaps in OWNERS coverage to reviewers if requested.
1810 if options.add_owners_to:
1811 assert options.add_owners_to in ('R'), options.add_owners_to
1812 status = self.owners_client.GetFilesApprovalStatus(
1813 files, [], options.reviewers)
1814 missing_files = [
1815 f for f in files
1816 if status[f] == self._owners_client.INSUFFICIENT_REVIEWERS
1817 ]
1818 owners = self.owners_client.SuggestOwners(
1819 missing_files, exclude=[self.GetAuthor()])
1820 assert isinstance(options.reviewers, list), options.reviewers
1821 options.reviewers.extend(owners)
Edward Lemur5a644f82020-03-18 16:44:57 +00001822
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001823 # Set the reviewer list now so that presubmit checks can access it.
1824 if options.reviewers:
1825 change_description.update_reviewers(options.reviewers)
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001826
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001827 return change_description
Edward Lemur5a644f82020-03-18 16:44:57 +00001828
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001829 def _GetTitleForUpload(self, options, multi_change_upload=False):
1830 # type: (optparse.Values, Optional[bool]) -> str
Edward Lemur5a644f82020-03-18 16:44:57 +00001831
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001832 # Getting titles for multipl commits is not supported so we return the
1833 # default.
1834 if not options.squash or multi_change_upload or options.title:
1835 return options.title
Joanna Wanga1abbed2023-01-24 01:41:05 +00001836
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001837 # On first upload, patchset title is always this string, while
1838 # options.title gets converted to first line of message.
1839 if not self.GetIssue():
1840 return 'Initial upload'
Edward Lemur5a644f82020-03-18 16:44:57 +00001841
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001842 # When uploading subsequent patchsets, options.message is taken as the
1843 # title if options.title is not provided.
1844 if options.message:
1845 return options.message.strip()
Edward Lemur5a644f82020-03-18 16:44:57 +00001846
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001847 # Use the subject of the last commit as title by default.
1848 title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
1849 if options.force or options.skip_title:
1850 return title
1851 user_title = gclient_utils.AskForData('Title for patchset [%s]: ' %
1852 title)
Edward Lemur5a644f82020-03-18 16:44:57 +00001853
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001854 # Use the default title if the user confirms the default with a 'y'.
1855 if user_title.lower() == 'y':
1856 return title
1857 return user_title or title
mlcui3da91712021-05-05 10:00:30 +00001858
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001859 def _GetRefSpecOptions(self,
1860 options: optparse.Values,
1861 change_desc: ChangeDescription,
1862 multi_change_upload: bool = False,
1863 dogfood_path: bool = False) -> List[str]:
1864 # Extra options that can be specified at push time. Doc:
1865 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
1866 refspec_opts = []
Edward Lemur5a644f82020-03-18 16:44:57 +00001867
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001868 # By default, new changes are started in WIP mode, and subsequent
1869 # patchsets don't send email. At any time, passing --send-mail or
1870 # --send-email will mark the change ready and send email for that
1871 # particular patch.
1872 if options.send_mail:
1873 refspec_opts.append('ready')
1874 refspec_opts.append('notify=ALL')
1875 elif (not self.GetIssue() and options.squash and not dogfood_path):
1876 refspec_opts.append('wip')
Joanna Wanga1abbed2023-01-24 01:41:05 +00001877
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001878 # TODO(tandrii): options.message should be posted as a comment if
1879 # --send-mail or --send-email is set on non-initial upload as Rietveld
1880 # used to do it.
Joanna Wanga1abbed2023-01-24 01:41:05 +00001881
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001882 # Set options.title in case user was prompted in _GetTitleForUpload and
1883 # _CMDUploadChange needs to be called again.
1884 options.title = self._GetTitleForUpload(
1885 options, multi_change_upload=multi_change_upload)
Joanna Wanga1abbed2023-01-24 01:41:05 +00001886
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001887 if options.title:
1888 # Punctuation and whitespace in |title| must be percent-encoded.
1889 refspec_opts.append(
1890 'm=' + gerrit_util.PercentEncodeForGitRef(options.title))
Joanna Wanga1abbed2023-01-24 01:41:05 +00001891
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001892 if options.private:
1893 refspec_opts.append('private')
Joanna Wanga1abbed2023-01-24 01:41:05 +00001894
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001895 if options.topic:
1896 # Documentation on Gerrit topics is here:
1897 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
1898 refspec_opts.append('topic=%s' % options.topic)
Joanna Wanga1abbed2023-01-24 01:41:05 +00001899
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001900 if options.enable_auto_submit:
1901 refspec_opts.append('l=Auto-Submit+1')
1902 if options.set_bot_commit:
1903 refspec_opts.append('l=Bot-Commit+1')
1904 if options.use_commit_queue:
1905 refspec_opts.append('l=Commit-Queue+2')
1906 elif options.cq_dry_run:
1907 refspec_opts.append('l=Commit-Queue+1')
Joanna Wanga1abbed2023-01-24 01:41:05 +00001908
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001909 if change_desc.get_reviewers(tbr_only=True):
1910 score = gerrit_util.GetCodeReviewTbrScore(self.GetGerritHost(),
1911 self.GetGerritProject())
1912 refspec_opts.append('l=Code-Review+%s' % score)
Joanna Wanga1abbed2023-01-24 01:41:05 +00001913
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001914 # Gerrit sorts hashtags, so order is not important.
1915 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
1916 # We check GetIssue because we only add hashtags from the
1917 # description on the first upload.
1918 # TODO(b/265929888): When we fully launch the new path:
1919 # 1) remove fetching hashtags from description alltogether
1920 # 2) Or use descrtiption hashtags for:
1921 # `not (self.GetIssue() and multi_change_upload)`
1922 # 3) Or enabled change description tags for multi and single changes
1923 # by adding them post `git push`.
1924 if not (self.GetIssue() and dogfood_path):
1925 hashtags.update(change_desc.get_hash_tags())
1926 refspec_opts.extend(['hashtag=%s' % t for t in hashtags])
Joanna Wanga1abbed2023-01-24 01:41:05 +00001927
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001928 # Note: Reviewers, and ccs are handled individually for each
1929 # branch/change.
1930 return refspec_opts
Joanna Wang40497912023-01-24 21:18:16 +00001931
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001932 def PrepareSquashedCommit(self,
1933 options: optparse.Values,
1934 parent: str,
1935 orig_parent: str,
1936 end_commit: Optional[str] = None) -> _NewUpload:
1937 """Create a squashed commit to upload.
Joanna Wang05b60342023-03-29 20:25:57 +00001938
1939
1940 Args:
1941 parent: The commit to use as the parent for the new squashed.
1942 orig_parent: The commit that is an actual ancestor of `end_commit`. It
1943 is part of the same original tree as end_commit, which does not
1944 contain squashed commits. This is used to create the change
1945 description for the new squashed commit with:
1946 `git log orig_parent..end_commit`.
1947 end_commit: The commit to use as the end of the new squashed commit.
1948 """
Joanna Wangb88a4342023-01-24 01:28:22 +00001949
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001950 if end_commit is None:
1951 end_commit = RunGit(['rev-parse', self.branchref]).strip()
Joanna Wangb88a4342023-01-24 01:28:22 +00001952
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001953 reviewers, ccs, change_desc = self._PrepareChange(
1954 options, orig_parent, end_commit)
1955 latest_tree = RunGit(['rev-parse', end_commit + ':']).strip()
1956 with gclient_utils.temporary_file() as desc_tempfile:
1957 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
1958 commit_to_push = RunGit(
1959 ['commit-tree', latest_tree, '-p', parent, '-F',
1960 desc_tempfile]).strip()
Joanna Wangb88a4342023-01-24 01:28:22 +00001961
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001962 # Gerrit may or may not update fast enough to return the correct
1963 # patchset number after we push. Get the pre-upload patchset and
1964 # increment later.
1965 prev_patchset = self.GetMostRecentPatchset(update=False) or 0
1966 return _NewUpload(reviewers, ccs, commit_to_push, end_commit, parent,
1967 change_desc, prev_patchset)
Joanna Wangb88a4342023-01-24 01:28:22 +00001968
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001969 def PrepareCherryPickSquashedCommit(self, options: optparse.Values,
1970 parent: str) -> _NewUpload:
1971 """Create a commit cherry-picked on parent to push."""
Joanna Wange8523912023-01-21 02:05:40 +00001972
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001973 # The `parent` is what we will cherry-pick on top of.
1974 # The `cherry_pick_base` is the beginning range of what
1975 # we are cherry-picking.
1976 cherry_pick_base = self.GetCommonAncestorWithUpstream()
1977 reviewers, ccs, change_desc = self._PrepareChange(
1978 options, cherry_pick_base, self.branchref)
Joanna Wange8523912023-01-21 02:05:40 +00001979
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001980 new_upload_hash = RunGit(['rev-parse', self.branchref]).strip()
1981 latest_tree = RunGit(['rev-parse', self.branchref + ':']).strip()
1982 with gclient_utils.temporary_file() as desc_tempfile:
1983 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
1984 commit_to_cp = RunGit([
1985 'commit-tree', latest_tree, '-p', cherry_pick_base, '-F',
1986 desc_tempfile
1987 ]).strip()
Joanna Wange8523912023-01-21 02:05:40 +00001988
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001989 RunGit(['checkout', '-q', parent])
1990 ret, _out = RunGitWithCode(['cherry-pick', commit_to_cp])
1991 if ret:
1992 RunGit(['cherry-pick', '--abort'])
1993 RunGit(['checkout', '-q', self.branch])
1994 DieWithError('Could not cleanly cherry-pick')
Joanna Wange8523912023-01-21 02:05:40 +00001995
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001996 commit_to_push = RunGit(['rev-parse', 'HEAD']).strip()
1997 RunGit(['checkout', '-q', self.branch])
Joanna Wange8523912023-01-21 02:05:40 +00001998
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001999 # Gerrit may or may not update fast enough to return the correct
2000 # patchset number after we push. Get the pre-upload patchset and
2001 # increment later.
2002 prev_patchset = self.GetMostRecentPatchset(update=False) or 0
2003 return _NewUpload(reviewers, ccs, commit_to_push, new_upload_hash,
2004 cherry_pick_base, change_desc, prev_patchset)
Joanna Wange8523912023-01-21 02:05:40 +00002005
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002006 def _PrepareChange(
2007 self, options: optparse.Values, parent: str, end_commit: str
2008 ) -> Tuple[Sequence[str], Sequence[str], ChangeDescription]:
2009 """Prepares the change to be uploaded."""
2010 self.EnsureCanUploadPatchset(options.force)
Joanna Wangb46232e2023-01-21 01:58:46 +00002011
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002012 files = self.GetAffectedFiles(parent, end_commit=end_commit)
2013 change_desc = self._GetDescriptionForUpload(options,
2014 [parent, end_commit], files)
Joanna Wangb46232e2023-01-21 01:58:46 +00002015
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002016 watchlist = watchlists.Watchlists(settings.GetRoot())
2017 self.ExtendCC(watchlist.GetWatchersForPaths(files))
2018 if not options.bypass_hooks:
2019 hook_results = self.RunHook(committing=False,
2020 may_prompt=not options.force,
2021 verbose=options.verbose,
2022 parallel=options.parallel,
2023 upstream=parent,
2024 description=change_desc.description,
2025 all_files=False)
2026 self.ExtendCC(hook_results['more_cc'])
Joanna Wangb46232e2023-01-21 01:58:46 +00002027
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002028 # Update the change description and ensure we have a Change Id.
2029 if self.GetIssue():
2030 if options.edit_description:
2031 change_desc.prompt()
2032 change_detail = self._GetChangeDetail(['CURRENT_REVISION'])
2033 change_id = change_detail['change_id']
2034 change_desc.ensure_change_id(change_id)
Joanna Wangb46232e2023-01-21 01:58:46 +00002035
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002036 else: # No change issue. First time uploading
2037 if not options.force and not options.message_file:
2038 change_desc.prompt()
Joanna Wangb46232e2023-01-21 01:58:46 +00002039
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002040 # Check if user added a change_id in the descripiton.
2041 change_ids = git_footers.get_footer_change_id(
2042 change_desc.description)
2043 if len(change_ids) == 1:
2044 change_id = change_ids[0]
2045 else:
2046 change_id = GenerateGerritChangeId(change_desc.description)
2047 change_desc.ensure_change_id(change_id)
Joanna Wangb46232e2023-01-21 01:58:46 +00002048
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002049 if options.preserve_tryjobs:
2050 change_desc.set_preserve_tryjobs()
Joanna Wangb46232e2023-01-21 01:58:46 +00002051
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002052 SaveDescriptionBackup(change_desc)
Joanna Wangb46232e2023-01-21 01:58:46 +00002053
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002054 # Add ccs
2055 ccs = []
2056 # Add default, watchlist, presubmit ccs if this is the initial upload
2057 # and CL is not private and auto-ccing has not been disabled.
2058 if not options.private and not options.no_autocc and not self.GetIssue(
2059 ):
2060 ccs = self.GetCCList().split(',')
2061 if len(ccs) > 100:
2062 lsc = (
2063 'https://chromium.googlesource.com/chromium/src/+/HEAD/docs/'
2064 'process/lsc/lsc_workflow.md')
2065 print('WARNING: This will auto-CC %s users.' % len(ccs))
2066 print('LSC may be more appropriate: %s' % lsc)
2067 print(
2068 'You can also use the --no-autocc flag to disable auto-CC.')
2069 confirm_or_exit(action='continue')
Joanna Wangb46232e2023-01-21 01:58:46 +00002070
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002071 # Add ccs from the --cc flag.
2072 if options.cc:
2073 ccs.extend(options.cc)
Joanna Wangb46232e2023-01-21 01:58:46 +00002074
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002075 ccs = [email.strip() for email in ccs if email.strip()]
2076 if change_desc.get_cced():
2077 ccs.extend(change_desc.get_cced())
Joanna Wangb46232e2023-01-21 01:58:46 +00002078
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002079 return change_desc.get_reviewers(), ccs, change_desc
Joanna Wangb46232e2023-01-21 01:58:46 +00002080
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002081 def PostUploadUpdates(self, options: optparse.Values,
2082 new_upload: _NewUpload, change_number: str) -> None:
2083 """Makes necessary post upload changes to the local and remote cl."""
2084 if not self.GetIssue():
2085 self.SetIssue(change_number)
Joanna Wang40497912023-01-24 21:18:16 +00002086
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002087 self.SetPatchset(new_upload.prev_patchset + 1)
Joanna Wang7603f042023-03-01 22:17:36 +00002088
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002089 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY,
2090 new_upload.commit_to_push)
2091 self._GitSetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY,
2092 new_upload.new_last_uploaded_commit)
Joanna Wang40497912023-01-24 21:18:16 +00002093
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002094 if settings.GetRunPostUploadHook():
2095 self.RunPostUploadHook(options.verbose, new_upload.parent,
2096 new_upload.change_desc.description)
Joanna Wang40497912023-01-24 21:18:16 +00002097
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002098 if new_upload.reviewers or new_upload.ccs:
2099 gerrit_util.AddReviewers(self.GetGerritHost(),
2100 self._GerritChangeIdentifier(),
2101 reviewers=new_upload.reviewers,
2102 ccs=new_upload.ccs,
2103 notify=bool(options.send_mail))
Joanna Wang40497912023-01-24 21:18:16 +00002104
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002105 def CMDUpload(self, options, git_diff_args, orig_args):
2106 """Uploads a change to codereview."""
2107 custom_cl_base = None
2108 if git_diff_args:
2109 custom_cl_base = base_branch = git_diff_args[0]
2110 else:
2111 if self.GetBranch() is None:
2112 DieWithError(
2113 'Can\'t upload from detached HEAD state. Get on a branch!')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002114
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002115 # Default to diffing against common ancestor of upstream branch
2116 base_branch = self.GetCommonAncestorWithUpstream()
2117 git_diff_args = [base_branch, 'HEAD']
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002118
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002119 # Fast best-effort checks to abort before running potentially expensive
2120 # hooks if uploading is likely to fail anyway. Passing these checks does
2121 # not guarantee that uploading will not fail.
2122 self.EnsureAuthenticated(force=options.force)
2123 self.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002124
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002125 print(f'Processing {_GetCommitCountSummary(*git_diff_args)}...')
Daniel Cheng66d0f152023-08-29 23:21:58 +00002126
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002127 # Apply watchlists on upload.
2128 watchlist = watchlists.Watchlists(settings.GetRoot())
2129 files = self.GetAffectedFiles(base_branch)
2130 if not options.bypass_watchlists:
2131 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002132
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002133 change_desc = self._GetDescriptionForUpload(options, git_diff_args,
2134 files)
2135 if not options.bypass_hooks:
2136 hook_results = self.RunHook(committing=False,
2137 may_prompt=not options.force,
2138 verbose=options.verbose,
2139 parallel=options.parallel,
2140 upstream=base_branch,
2141 description=change_desc.description,
2142 all_files=False)
2143 self.ExtendCC(hook_results['more_cc'])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002144
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002145 print_stats(git_diff_args)
2146 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base,
2147 change_desc)
2148 if not ret:
2149 if self.GetBranch() is not None:
2150 self._GitSetBranchConfigValue(
2151 LAST_UPLOAD_HASH_CONFIG_KEY,
2152 scm.GIT.ResolveCommit(settings.GetRoot(), 'HEAD'))
2153 # Run post upload hooks, if specified.
2154 if settings.GetRunPostUploadHook():
2155 self.RunPostUploadHook(options.verbose, base_branch,
2156 change_desc.description)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002157
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002158 # Upload all dependencies if specified.
2159 if options.dependencies:
2160 print()
2161 print('--dependencies has been specified.')
2162 print('All dependent local branches will be re-uploaded.')
2163 print()
2164 # Remove the dependencies flag from args so that we do not end
2165 # up in a loop.
2166 orig_args.remove('--dependencies')
2167 ret = upload_branch_deps(self, orig_args, options.force)
2168 return ret
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002169
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002170 def SetCQState(self, new_state):
2171 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002172
Struan Shrimpton8b2072b2023-07-31 21:01:26 +00002173 Issue must have been already uploaded and known.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002174 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002175 assert new_state in _CQState.ALL_STATES
2176 assert self.GetIssue()
2177 try:
2178 vote_map = {
2179 _CQState.NONE: 0,
2180 _CQState.DRY_RUN: 1,
2181 _CQState.COMMIT: 2,
2182 }
2183 labels = {'Commit-Queue': vote_map[new_state]}
2184 notify = False if new_state == _CQState.DRY_RUN else None
2185 gerrit_util.SetReview(self.GetGerritHost(),
2186 self._GerritChangeIdentifier(),
2187 labels=labels,
2188 notify=notify)
2189 return 0
2190 except KeyboardInterrupt:
2191 raise
2192 except:
2193 print(
2194 'WARNING: Failed to %s.\n'
2195 'Either:\n'
2196 ' * Your project has no CQ,\n'
2197 ' * You don\'t have permission to change the CQ state,\n'
2198 ' * There\'s a bug in this code (see stack trace below).\n'
2199 'Consider specifying which bots to trigger manually or asking your '
2200 'project owners for permissions or contacting Chrome Infra at:\n'
2201 'https://www.chromium.org/infra\n\n' %
2202 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
2203 # Still raise exception so that stack trace is printed.
2204 raise
qyearsley1fdfcb62016-10-24 13:22:03 -07002205
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002206 def GetGerritHost(self):
2207 # Lazy load of configs.
2208 self.GetCodereviewServer()
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002209
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002210 if self._gerrit_host and '.' not in self._gerrit_host:
2211 # Abbreviated domain like "chromium" instead of
2212 # chromium.googlesource.com.
2213 parsed = urllib.parse.urlparse(self.GetRemoteUrl())
2214 if parsed.scheme == 'sso':
2215 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2216 self._gerrit_server = 'https://%s' % self._gerrit_host
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002217
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002218 return self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002219
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002220 def _GetGitHost(self):
2221 """Returns git host to be used when uploading change to Gerrit."""
2222 remote_url = self.GetRemoteUrl()
2223 if not remote_url:
2224 return None
2225 return urllib.parse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002226
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002227 def GetCodereviewServer(self):
2228 if not self._gerrit_server:
2229 # If we're on a branch then get the server potentially associated
2230 # with that branch.
2231 if self.GetIssue() and self.GetBranch():
2232 self._gerrit_server = self._GitGetBranchConfigValue(
2233 CODEREVIEW_SERVER_CONFIG_KEY)
2234 if self._gerrit_server:
2235 self._gerrit_host = urllib.parse.urlparse(
2236 self._gerrit_server).netloc
2237 if not self._gerrit_server:
2238 url = urllib.parse.urlparse(self.GetRemoteUrl())
2239 parts = url.netloc.split('.')
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002240
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002241 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2242 # has "-review" suffix for lowest level subdomain.
2243 parts[0] = parts[0] + '-review'
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002244
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002245 if url.scheme == 'sso' and len(parts) == 1:
2246 # sso:// uses abbreivated hosts, eg. sso://chromium instead
2247 # of chromium.googlesource.com. Hence, for code review
2248 # server, they need to be expanded.
2249 parts[0] += '.googlesource.com'
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002250
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002251 self._gerrit_host = '.'.join(parts)
2252 self._gerrit_server = 'https://%s' % self._gerrit_host
2253 return self._gerrit_server
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002254
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002255 def GetGerritProject(self):
2256 """Returns Gerrit project name based on remote git URL."""
2257 remote_url = self.GetRemoteUrl()
2258 if remote_url is None:
2259 logging.warning('can\'t detect Gerrit project.')
2260 return None
2261 project = urllib.parse.urlparse(remote_url).path.strip('/')
2262 if project.endswith('.git'):
2263 project = project[:-len('.git')]
2264 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start
2265 # with 'a/' prefix, because 'a/' prefix is used to force authentication
2266 # in gitiles/git-over-https protocol. E.g.,
2267 # https://chromium.googlesource.com/a/v8/v8 refers to the same
2268 # repo/project as https://chromium.googlesource.com/v8/v8
2269 if project.startswith('a/'):
2270 project = project[len('a/'):]
2271 return project
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002272
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002273 def _GerritChangeIdentifier(self):
2274 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002275
2276 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002277 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002278 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002279 project = self.GetGerritProject()
2280 if project:
2281 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
2282 # Fall back on still unique, but less efficient change number.
2283 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002284
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002285 def EnsureAuthenticated(self, force, refresh=None):
2286 """Best effort check that user is authenticated with Gerrit server."""
2287 if settings.GetGerritSkipEnsureAuthenticated():
2288 # For projects with unusual authentication schemes.
2289 # See http://crbug.com/603378.
2290 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002291
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002292 # Check presence of cookies only if using cookies-based auth method.
2293 cookie_auth = gerrit_util.Authenticator.get()
2294 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2295 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002296
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002297 remote_url = self.GetRemoteUrl()
2298 if remote_url is None:
2299 logging.warning('invalid remote')
2300 return
2301 if urllib.parse.urlparse(remote_url).scheme not in ['https', 'sso']:
2302 logging.warning(
2303 'Ignoring branch %(branch)s with non-https/sso remote '
2304 '%(remote)s', {
2305 'branch': self.branch,
2306 'remote': self.GetRemoteUrl()
2307 })
2308 return
Daniel Chengcf6269b2019-05-18 01:02:12 +00002309
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002310 # Lazy-loader to identify Gerrit and Git hosts.
2311 self.GetCodereviewServer()
2312 git_host = self._GetGitHost()
2313 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002314
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002315 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2316 git_auth = cookie_auth.get_auth_header(git_host)
2317 if gerrit_auth and git_auth:
2318 if gerrit_auth == git_auth:
2319 return
2320 all_gsrc = cookie_auth.get_auth_header(
2321 'd0esN0tEx1st.googlesource.com')
2322 print(
2323 'WARNING: You have different credentials for Gerrit and git hosts:\n'
2324 ' %s\n'
2325 ' %s\n'
2326 ' Consider running the following command:\n'
2327 ' git cl creds-check\n'
2328 ' %s\n'
2329 ' %s' %
2330 (git_host, self._gerrit_host,
2331 ('Hint: delete creds for .googlesource.com' if all_gsrc else
2332 ''), cookie_auth.get_new_password_message(git_host)))
2333 if not force:
2334 confirm_or_exit('If you know what you are doing',
2335 action='continue')
2336 return
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002337
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002338 missing = (([] if gerrit_auth else [self._gerrit_host]) +
2339 ([] if git_auth else [git_host]))
2340 DieWithError('Credentials for the following hosts are required:\n'
2341 ' %s\n'
2342 'These are read from %s (or legacy %s)\n'
2343 '%s' %
2344 ('\n '.join(missing), cookie_auth.get_gitcookies_path(),
2345 cookie_auth.get_netrc_path(),
2346 cookie_auth.get_new_password_message(git_host)))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002347
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002348 def EnsureCanUploadPatchset(self, force):
2349 if not self.GetIssue():
2350 return
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002351
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002352 status = self._GetChangeDetail()['status']
2353 if status == 'ABANDONED':
2354 DieWithError(
2355 'Change %s has been abandoned, new uploads are not allowed' %
2356 (self.GetIssueURL()))
2357 if status == 'MERGED':
2358 answer = gclient_utils.AskForData(
2359 'Change %s has been submitted, new uploads are not allowed. '
2360 'Would you like to start a new change (Y/n)?' %
2361 self.GetIssueURL()).lower()
2362 if answer not in ('y', ''):
2363 DieWithError('New uploads are not allowed.')
2364 self.SetIssue()
2365 return
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002366
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002367 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2368 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2369 # Apparently this check is not very important? Otherwise get_auth_email
2370 # could have been added to other implementations of Authenticator.
2371 cookies_auth = gerrit_util.Authenticator.get()
2372 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
2373 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002374
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002375 cookies_user = cookies_auth.get_auth_email(self.GetGerritHost())
2376 if self.GetIssueOwner() == cookies_user:
2377 return
2378 logging.debug('change %s owner is %s, cookies user is %s',
2379 self.GetIssue(), self.GetIssueOwner(), cookies_user)
2380 # Maybe user has linked accounts or something like that,
2381 # so ask what Gerrit thinks of this user.
2382 details = gerrit_util.GetAccountDetails(self.GetGerritHost(), 'self')
2383 if details['email'] == self.GetIssueOwner():
2384 return
2385 if not force:
2386 print(
2387 'WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
2388 'as %s.\n'
2389 'Uploading may fail due to lack of permissions.' %
2390 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2391 confirm_or_exit(action='upload')
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002392
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002393 def GetStatus(self):
2394 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002395 or CQ status, assuming adherence to a common workflow.
2396
2397 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002398 * 'error' - error from review tool (including deleted issues)
2399 * 'unsent' - no reviewers added
2400 * 'waiting' - waiting for review
2401 * 'reply' - waiting for uploader to reply to review
2402 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002403 * 'dry-run' - dry-running in the CQ
2404 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07002405 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002406 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002407 if not self.GetIssue():
2408 return None
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002409
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002410 try:
2411 data = self._GetChangeDetail(
2412 ['DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
2413 except GerritChangeNotExists:
2414 return 'error'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002415
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002416 if data['status'] in ('ABANDONED', 'MERGED'):
2417 return 'closed'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002418
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002419 cq_label = data['labels'].get('Commit-Queue', {})
2420 max_cq_vote = 0
2421 for vote in cq_label.get('all', []):
2422 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
2423 if max_cq_vote == 2:
2424 return 'commit'
2425 if max_cq_vote == 1:
2426 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002427
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002428 if data['labels'].get('Code-Review', {}).get('approved'):
2429 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002430
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002431 if not data.get('reviewers', {}).get('REVIEWER', []):
2432 return 'unsent'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002433
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002434 owner = data['owner'].get('_account_id')
2435 messages = sorted(data.get('messages', []), key=lambda m: m.get('date'))
2436 while messages:
2437 m = messages.pop()
2438 if (m.get('tag', '').startswith('autogenerated:cq')
2439 or m.get('tag', '').startswith('autogenerated:cv')):
2440 # Ignore replies from LUCI CV/CQ.
2441 continue
2442 if m.get('author', {}).get('_account_id') == owner:
2443 # Most recent message was by owner.
2444 return 'waiting'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002445
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002446 # Some reply from non-owner.
2447 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002448
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002449 # Somehow there are no messages even though there are reviewers.
2450 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002451
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002452 def GetMostRecentPatchset(self, update=True):
2453 if not self.GetIssue():
2454 return None
Edward Lemur6c6827c2020-02-06 21:15:18 +00002455
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002456 data = self._GetChangeDetail(['CURRENT_REVISION'])
2457 patchset = data['revisions'][data['current_revision']]['_number']
2458 if update:
2459 self.SetPatchset(patchset)
2460 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002461
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002462 def _IsPatchsetRangeSignificant(self, lower, upper):
2463 """Returns True if the inclusive range of patchsets contains any reworks or
Gavin Makf35a9eb2022-11-17 18:34:36 +00002464 rebases."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002465 if not self.GetIssue():
2466 return False
Gavin Makf35a9eb2022-11-17 18:34:36 +00002467
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002468 data = self._GetChangeDetail(['ALL_REVISIONS'])
2469 ps_kind = {}
2470 for rev_info in data.get('revisions', {}).values():
2471 ps_kind[rev_info['_number']] = rev_info.get('kind', '')
Gavin Makf35a9eb2022-11-17 18:34:36 +00002472
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002473 for ps in range(lower, upper + 1):
2474 assert ps in ps_kind, 'expected patchset %d in change detail' % ps
2475 if ps_kind[ps] not in ('NO_CHANGE', 'NO_CODE_CHANGE'):
2476 return True
2477 return False
Gavin Makf35a9eb2022-11-17 18:34:36 +00002478
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002479 def GetMostRecentDryRunPatchset(self):
2480 """Get patchsets equivalent to the most recent patchset and return
Gavin Make61ccc52020-11-13 00:12:57 +00002481 the patchset with the latest dry run. If none have been dry run, return
2482 the latest patchset."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002483 if not self.GetIssue():
2484 return None
Gavin Make61ccc52020-11-13 00:12:57 +00002485
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002486 data = self._GetChangeDetail(['ALL_REVISIONS'])
2487 patchset = data['revisions'][data['current_revision']]['_number']
2488 dry_run = {
2489 int(m['_revision_number'])
2490 for m in data.get('messages', [])
2491 if m.get('tag', '').endswith('dry-run')
2492 }
Gavin Make61ccc52020-11-13 00:12:57 +00002493
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002494 for revision_info in sorted(data.get('revisions', {}).values(),
2495 key=lambda c: c['_number'],
2496 reverse=True):
2497 if revision_info['_number'] in dry_run:
2498 patchset = revision_info['_number']
2499 break
2500 if revision_info.get('kind', '') not in \
2501 ('NO_CHANGE', 'NO_CODE_CHANGE', 'TRIVIAL_REBASE'):
2502 break
2503 self.SetPatchset(patchset)
2504 return patchset
Gavin Make61ccc52020-11-13 00:12:57 +00002505
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002506 def AddComment(self, message, publish=None):
2507 gerrit_util.SetReview(self.GetGerritHost(),
2508 self._GerritChangeIdentifier(),
2509 msg=message,
2510 ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002511
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002512 def GetCommentsSummary(self, readable=True):
2513 # DETAILED_ACCOUNTS is to get emails in accounts.
2514 # CURRENT_REVISION is included to get the latest patchset so that
2515 # only the robot comments from the latest patchset can be shown.
2516 messages = self._GetChangeDetail(
2517 options=['MESSAGES', 'DETAILED_ACCOUNTS', 'CURRENT_REVISION']).get(
2518 'messages', [])
2519 file_comments = gerrit_util.GetChangeComments(
2520 self.GetGerritHost(), self._GerritChangeIdentifier())
2521 robot_file_comments = gerrit_util.GetChangeRobotComments(
2522 self.GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002523
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002524 # Add the robot comments onto the list of comments, but only
2525 # keep those that are from the latest patchset.
2526 latest_patch_set = self.GetMostRecentPatchset()
2527 for path, robot_comments in robot_file_comments.items():
2528 line_comments = file_comments.setdefault(path, [])
2529 line_comments.extend([
2530 c for c in robot_comments if c['patch_set'] == latest_patch_set
2531 ])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002532
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002533 # Build dictionary of file comments for easy access and sorting later.
2534 # {author+date: {path: {patchset: {line: url+message}}}}
2535 comments = collections.defaultdict(lambda: collections.defaultdict(
2536 lambda: collections.defaultdict(dict)))
Andrii Shyshkalova3762a92020-11-25 10:20:42 +00002537
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002538 server = self.GetCodereviewServer()
2539 if server in _KNOWN_GERRIT_TO_SHORT_URLS:
2540 # /c/ is automatically added by short URL server.
2541 url_prefix = '%s/%s' % (_KNOWN_GERRIT_TO_SHORT_URLS[server],
2542 self.GetIssue())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002543 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002544 url_prefix = '%s/c/%s' % (server, self.GetIssue())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002545
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002546 for path, line_comments in file_comments.items():
2547 for comment in line_comments:
2548 tag = comment.get('tag', '')
2549 if tag.startswith(
2550 'autogenerated') and 'robot_id' not in comment:
2551 continue
2552 key = (comment['author']['email'], comment['updated'])
2553 if comment.get('side', 'REVISION') == 'PARENT':
2554 patchset = 'Base'
2555 else:
2556 patchset = 'PS%d' % comment['patch_set']
2557 line = comment.get('line', 0)
2558 url = ('%s/%s/%s#%s%s' %
2559 (url_prefix, comment['patch_set'],
2560 path, 'b' if comment.get('side') == 'PARENT' else '',
2561 str(line) if line else ''))
2562 comments[key][path][patchset][line] = (url, comment['message'])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002563
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002564 summaries = []
2565 for msg in messages:
2566 summary = self._BuildCommentSummary(msg, comments, readable)
2567 if summary:
2568 summaries.append(summary)
2569 return summaries
Josip Sokcevic266129c2021-11-09 00:22:00 +00002570
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002571 @staticmethod
2572 def _BuildCommentSummary(msg, comments, readable):
2573 if 'email' not in msg['author']:
2574 # Some bot accounts may not have an email associated.
2575 return None
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002576
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002577 key = (msg['author']['email'], msg['date'])
2578 # Don't bother showing autogenerated messages that don't have associated
2579 # file or line comments. this will filter out most autogenerated
2580 # messages, but will keep robot comments like those from Tricium.
2581 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
2582 if is_autogenerated and not comments.get(key):
2583 return None
2584 message = msg['message']
2585 # Gerrit spits out nanoseconds.
2586 assert len(msg['date'].split('.')[-1]) == 9
2587 date = datetime.datetime.strptime(msg['date'][:-3],
2588 '%Y-%m-%d %H:%M:%S.%f')
2589 if key in comments:
2590 message += '\n'
2591 for path, patchsets in sorted(comments.get(key, {}).items()):
2592 if readable:
2593 message += '\n%s' % path
2594 for patchset, lines in sorted(patchsets.items()):
2595 for line, (url, content) in sorted(lines.items()):
2596 if line:
2597 line_str = 'Line %d' % line
2598 path_str = '%s:%d:' % (path, line)
2599 else:
2600 line_str = 'File comment'
2601 path_str = '%s:0:' % path
2602 if readable:
2603 message += '\n %s, %s: %s' % (patchset, line_str, url)
2604 message += '\n %s\n' % content
2605 else:
2606 message += '\n%s ' % path_str
2607 message += '\n%s\n' % content
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002608
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002609 return _CommentSummary(
2610 date=date,
2611 message=message,
2612 sender=msg['author']['email'],
2613 autogenerated=is_autogenerated,
2614 # These could be inferred from the text messages and correlated with
2615 # Code-Review label maximum, however this is not reliable.
2616 # Leaving as is until the need arises.
2617 approval=False,
2618 disapproval=False,
2619 )
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002620
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002621 def CloseIssue(self):
2622 gerrit_util.AbandonChange(self.GetGerritHost(),
2623 self._GerritChangeIdentifier(),
2624 msg='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002625
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002626 def SubmitIssue(self):
2627 gerrit_util.SubmitChange(self.GetGerritHost(),
2628 self._GerritChangeIdentifier())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002629
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002630 def _GetChangeDetail(self, options=None):
2631 """Returns details of associated Gerrit change and caching results."""
2632 options = options or []
2633 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002634
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002635 # Optimization to avoid multiple RPCs:
2636 if 'CURRENT_REVISION' in options or 'ALL_REVISIONS' in options:
2637 options.append('CURRENT_COMMIT')
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002638
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002639 # Normalize issue and options for consistent keys in cache.
2640 cache_key = str(self.GetIssue())
2641 options_set = frozenset(o.upper() for o in options)
2642
2643 for cached_options_set, data in self._detail_cache.get(cache_key, []):
2644 # Assumption: data fetched before with extra options is suitable
2645 # for return for a smaller set of options.
2646 # For example, if we cached data for
2647 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2648 # and request is for options=[CURRENT_REVISION],
2649 # THEN we can return prior cached data.
2650 if options_set.issubset(cached_options_set):
2651 return data
2652
2653 try:
2654 data = gerrit_util.GetChangeDetail(self.GetGerritHost(),
2655 self._GerritChangeIdentifier(),
2656 options_set)
2657 except gerrit_util.GerritError as e:
2658 if e.http_status == 404:
2659 raise GerritChangeNotExists(self.GetIssue(),
2660 self.GetCodereviewServer())
2661 raise
2662
2663 self._detail_cache.setdefault(cache_key, []).append((options_set, data))
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002664 return data
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002665
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002666 def _GetChangeCommit(self, revision='current'):
2667 assert self.GetIssue(), 'issue must be set to query Gerrit'
2668 try:
2669 data = gerrit_util.GetChangeCommit(self.GetGerritHost(),
2670 self._GerritChangeIdentifier(),
2671 revision)
2672 except gerrit_util.GerritError as e:
2673 if e.http_status == 404:
2674 raise GerritChangeNotExists(self.GetIssue(),
2675 self.GetCodereviewServer())
2676 raise
2677 return data
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002678
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002679 def _IsCqConfigured(self):
2680 detail = self._GetChangeDetail(['LABELS'])
2681 return u'Commit-Queue' in detail.get('labels', {})
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002682
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002683 def CMDLand(self, force, bypass_hooks, verbose, parallel, resultdb, realm):
2684 if git_common.is_dirty_git_tree('land'):
2685 return 1
agable32978d92016-11-01 12:55:02 -07002686
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002687 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2688 if not force and self._IsCqConfigured():
2689 confirm_or_exit(
2690 '\nIt seems this repository has a CQ, '
2691 'which can test and land changes for you. '
2692 'Are you sure you wish to bypass it?\n',
2693 action='bypass CQ')
2694 differs = True
2695 last_upload = self._GitGetBranchConfigValue(
Gavin Mak4e5e3992022-11-14 22:40:12 +00002696 GERRIT_SQUASH_HASH_CONFIG_KEY)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002697 # Note: git diff outputs nothing if there is no diff.
2698 if not last_upload or RunGit(['diff', last_upload]).strip():
2699 print(
2700 'WARNING: Some changes from local branch haven\'t been uploaded.'
2701 )
Edward Lemur5a644f82020-03-18 16:44:57 +00002702 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002703 if detail['current_revision'] == last_upload:
2704 differs = False
2705 else:
2706 print(
2707 'WARNING: Local branch contents differ from latest uploaded '
2708 'patchset.')
2709 if differs:
2710 if not force:
2711 confirm_or_exit(
2712 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2713 action='submit')
2714 print(
2715 'WARNING: Bypassing hooks and submitting latest uploaded patchset.'
2716 )
2717 elif not bypass_hooks:
2718 upstream = self.GetCommonAncestorWithUpstream()
2719 if self.GetIssue():
2720 description = self.FetchDescription()
2721 else:
2722 description = _create_description_from_log([upstream])
2723 self.RunHook(committing=True,
2724 may_prompt=not force,
2725 verbose=verbose,
2726 parallel=parallel,
2727 upstream=upstream,
2728 description=description,
2729 all_files=False,
2730 resultdb=resultdb,
2731 realm=realm)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002732
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002733 self.SubmitIssue()
2734 print('Issue %s has been submitted.' % self.GetIssueURL())
2735 links = self._GetChangeCommit().get('web_links', [])
2736 for link in links:
2737 if link.get('name') in ['gitiles', 'browse'] and link.get('url'):
2738 print('Landed as: %s' % link.get('url'))
2739 break
2740 return 0
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002741
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002742 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force,
2743 newbranch):
2744 assert parsed_issue_arg.valid
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002745
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002746 self.issue = parsed_issue_arg.issue
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002747
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002748 if parsed_issue_arg.hostname:
2749 self._gerrit_host = parsed_issue_arg.hostname
2750 self._gerrit_server = 'https://%s' % self._gerrit_host
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002751
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002752 try:
2753 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2754 except GerritChangeNotExists as e:
2755 DieWithError(str(e))
agablec6787972016-09-09 16:13:34 -07002756
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002757 if not parsed_issue_arg.patchset:
2758 # Use current revision by default.
2759 revision_info = detail['revisions'][detail['current_revision']]
2760 patchset = int(revision_info['_number'])
2761 else:
2762 patchset = parsed_issue_arg.patchset
2763 for revision_info in detail['revisions'].values():
2764 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2765 break
2766 else:
2767 DieWithError('Couldn\'t find patchset %i in change %i' %
2768 (parsed_issue_arg.patchset, self.GetIssue()))
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002769
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002770 remote_url = self.GetRemoteUrl()
2771 if remote_url.endswith('.git'):
2772 remote_url = remote_url[:-len('.git')]
2773 remote_url = remote_url.rstrip('/')
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002774
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002775 fetch_info = revision_info['fetch']['http']
2776 fetch_info['url'] = fetch_info['url'].rstrip('/')
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002777
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002778 if remote_url != fetch_info['url']:
2779 DieWithError(
2780 'Trying to patch a change from %s but this repo appears '
2781 'to be %s.' % (fetch_info['url'], remote_url))
Gavin Mak4e5e3992022-11-14 22:40:12 +00002782
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002783 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002784
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002785 # Set issue immediately in case the cherry-pick fails, which happens
2786 # when resolving conflicts.
2787 if self.GetBranch():
2788 self.SetIssue(parsed_issue_arg.issue)
tandrii88189772016-09-29 04:29:57 -07002789
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002790 if force:
2791 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2792 print('Checked out commit for change %i patchset %i locally' %
2793 (parsed_issue_arg.issue, patchset))
2794 elif nocommit:
2795 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2796 print('Patch applied to index.')
2797 else:
2798 RunGit(['cherry-pick', 'FETCH_HEAD'])
2799 print('Committed patch for change %i patchset %i locally.' %
2800 (parsed_issue_arg.issue, patchset))
2801 print(
2802 'Note: this created a local commit which does not have '
2803 'the same hash as the one uploaded for review. This will make '
2804 'uploading changes based on top of this branch difficult.\n'
2805 'If you want to do that, use "git cl patch --force" instead.')
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002806
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002807 if self.GetBranch():
2808 self.SetPatchset(patchset)
2809 fetched_hash = scm.GIT.ResolveCommit(settings.GetRoot(),
2810 'FETCH_HEAD')
2811 self._GitSetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY,
2812 fetched_hash)
2813 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY,
2814 fetched_hash)
2815 else:
2816 print(
2817 'WARNING: You are in detached HEAD state.\n'
2818 'The patch has been applied to your checkout, but you will not be '
2819 'able to upload a new patch set to the gerrit issue.\n'
2820 'Try using the \'-b\' option if you would like to work on a '
2821 'branch and/or upload a new patch set.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002822
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002823 return 0
2824
2825 @staticmethod
2826 def _GerritCommitMsgHookCheck(offer_removal):
2827 # type: (bool) -> None
2828 """Checks for the gerrit's commit-msg hook and removes it if necessary."""
2829 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2830 if not os.path.exists(hook):
2831 return
2832 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
2833 # custom developer-made one.
2834 data = gclient_utils.FileRead(hook)
2835 if not ('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2836 return
2837 print('WARNING: You have Gerrit commit-msg hook installed.\n'
2838 'It is not necessary for uploading with git cl in squash mode, '
2839 'and may interfere with it in subtle ways.\n'
2840 'We recommend you remove the commit-msg hook.')
2841 if offer_removal:
2842 if ask_for_explicit_yes('Do you want to remove it now?'):
2843 gclient_utils.rm_file_or_tree(hook)
2844 print('Gerrit commit-msg hook removed.')
2845 else:
2846 print('OK, will keep Gerrit commit-msg hook in place.')
2847
2848 def _CleanUpOldTraces(self):
2849 """Keep only the last |MAX_TRACES| traces."""
2850 try:
2851 traces = sorted([
2852 os.path.join(TRACES_DIR, f) for f in os.listdir(TRACES_DIR)
2853 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2854 and not f.startswith('tmp'))
2855 ])
2856 traces_to_delete = traces[:-MAX_TRACES]
2857 for trace in traces_to_delete:
2858 os.remove(trace)
2859 except OSError:
2860 print('WARNING: Failed to remove old git traces from\n'
2861 ' %s'
2862 'Consider removing them manually.' % TRACES_DIR)
2863
2864 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
2865 """Zip and write the git push traces stored in traces_dir."""
2866 gclient_utils.safe_makedirs(TRACES_DIR)
2867 traces_zip = trace_name + '-traces'
2868 traces_readme = trace_name + '-README'
2869 # Create a temporary dir to store git config and gitcookies in. It will
2870 # be compressed and stored next to the traces.
2871 git_info_dir = tempfile.mkdtemp()
2872 git_info_zip = trace_name + '-git-info'
2873
2874 git_push_metadata['now'] = datetime_now().strftime(
2875 '%Y-%m-%dT%H:%M:%S.%f')
2876
2877 git_push_metadata['trace_name'] = trace_name
2878 gclient_utils.FileWrite(traces_readme,
2879 TRACES_README_FORMAT % git_push_metadata)
2880
2881 # Keep only the first 6 characters of the git hashes on the packet
2882 # trace. This greatly decreases size after compression.
2883 packet_traces = os.path.join(traces_dir, 'trace-packet')
2884 if os.path.isfile(packet_traces):
2885 contents = gclient_utils.FileRead(packet_traces)
2886 gclient_utils.FileWrite(packet_traces,
2887 GIT_HASH_RE.sub(r'\1', contents))
2888 shutil.make_archive(traces_zip, 'zip', traces_dir)
2889
2890 # Collect and compress the git config and gitcookies.
2891 git_config = RunGit(['config', '-l'])
2892 gclient_utils.FileWrite(os.path.join(git_info_dir, 'git-config'),
2893 git_config)
2894
2895 cookie_auth = gerrit_util.Authenticator.get()
2896 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2897 gitcookies_path = cookie_auth.get_gitcookies_path()
2898 if os.path.isfile(gitcookies_path):
2899 gitcookies = gclient_utils.FileRead(gitcookies_path)
2900 gclient_utils.FileWrite(
2901 os.path.join(git_info_dir, 'gitcookies'),
2902 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2903 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2904
2905 gclient_utils.rmtree(git_info_dir)
2906
2907 def _RunGitPushWithTraces(self,
2908 refspec,
2909 refspec_opts,
2910 git_push_metadata,
2911 git_push_options=None):
2912 """Run git push and collect the traces resulting from the execution."""
2913 # Create a temporary directory to store traces in. Traces will be
2914 # compressed and stored in a 'traces' dir inside depot_tools.
2915 traces_dir = tempfile.mkdtemp()
2916 trace_name = os.path.join(TRACES_DIR,
2917 datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
2918
2919 env = os.environ.copy()
2920 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2921 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
2922 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
2923 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2924 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2925 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2926
2927 push_returncode = 0
2928 before_push = time_time()
2929 try:
2930 remote_url = self.GetRemoteUrl()
2931 push_cmd = ['git', 'push', remote_url, refspec]
2932 if git_push_options:
2933 for opt in git_push_options:
2934 push_cmd.extend(['-o', opt])
2935
2936 push_stdout = gclient_utils.CheckCallAndFilter(
2937 push_cmd,
2938 env=env,
2939 print_stdout=True,
2940 # Flush after every line: useful for seeing progress when
2941 # running as recipe.
2942 filter_fn=lambda _: sys.stdout.flush())
2943 push_stdout = push_stdout.decode('utf-8', 'replace')
2944 except subprocess2.CalledProcessError as e:
2945 push_returncode = e.returncode
2946 if 'blocked keyword' in str(e.stdout) or 'banned word' in str(
2947 e.stdout):
2948 raise GitPushError(
2949 'Failed to create a change, very likely due to blocked keyword. '
2950 'Please examine output above for the reason of the failure.\n'
2951 'If this is a false positive, you can try to bypass blocked '
2952 'keyword by using push option '
2953 '-o banned-words~skip, e.g.:\n'
2954 'git cl upload -o banned-words~skip\n\n'
2955 'If git-cl is not working correctly, file a bug under the '
2956 'Infra>SDK component.')
2957 if 'git push -o nokeycheck' in str(e.stdout):
2958 raise GitPushError(
2959 'Failed to create a change, very likely due to a private key being '
2960 'detected. Please examine output above for the reason of the '
2961 'failure.\n'
2962 'If this is a false positive, you can try to bypass private key '
2963 'detection by using push option '
2964 '-o nokeycheck, e.g.:\n'
2965 'git cl upload -o nokeycheck\n\n'
2966 'If git-cl is not working correctly, file a bug under the '
2967 'Infra>SDK component.')
2968
2969 raise GitPushError(
2970 'Failed to create a change. Please examine output above for the '
2971 'reason of the failure.\n'
2972 'For emergencies, Googlers can escalate to '
2973 'go/gob-support or go/notify#gob\n'
2974 'Hint: run command below to diagnose common Git/Gerrit '
2975 'credential problems:\n'
2976 ' git cl creds-check\n'
2977 '\n'
2978 'If git-cl is not working correctly, file a bug under the Infra>SDK '
2979 'component including the files below.\n'
2980 'Review the files before upload, since they might contain sensitive '
2981 'information.\n'
2982 'Set the Restrict-View-Google label so that they are not publicly '
2983 'accessible.\n' + TRACES_MESSAGE % {'trace_name': trace_name})
2984 finally:
2985 execution_time = time_time() - before_push
2986 metrics.collector.add_repeated(
2987 'sub_commands', {
2988 'command':
2989 'git push',
2990 'execution_time':
2991 execution_time,
2992 'exit_code':
2993 push_returncode,
2994 'arguments':
2995 metrics_utils.extract_known_subcommand_args(refspec_opts),
2996 })
2997
2998 git_push_metadata['execution_time'] = execution_time
2999 git_push_metadata['exit_code'] = push_returncode
3000 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
3001
3002 self._CleanUpOldTraces()
3003 gclient_utils.rmtree(traces_dir)
3004
3005 return push_stdout
3006
3007 def CMDUploadChange(self, options, git_diff_args, custom_cl_base,
3008 change_desc):
3009 """Upload the current branch to Gerrit, retry if new remote HEAD is
3010 found. options and change_desc may be mutated."""
3011 remote, remote_branch = self.GetRemoteBranch()
3012 branch = GetTargetRef(remote, remote_branch, options.target_branch)
3013
3014 try:
3015 return self._CMDUploadChange(options, git_diff_args, custom_cl_base,
3016 change_desc, branch)
3017 except GitPushError as e:
3018 # Repository might be in the middle of transition to main branch as
3019 # default, and uploads to old default might be blocked.
3020 if remote_branch not in [DEFAULT_OLD_BRANCH, DEFAULT_NEW_BRANCH]:
3021 DieWithError(str(e), change_desc)
3022
3023 project_head = gerrit_util.GetProjectHead(self._gerrit_host,
3024 self.GetGerritProject())
3025 if project_head == branch:
3026 DieWithError(str(e), change_desc)
3027 branch = project_head
3028
3029 print("WARNING: Fetching remote state and retrying upload to default "
3030 "branch...")
3031 RunGit(['fetch', '--prune', remote])
3032 options.edit_description = False
3033 options.force = True
3034 try:
3035 self._CMDUploadChange(options, git_diff_args, custom_cl_base,
3036 change_desc, branch)
3037 except GitPushError as e:
3038 DieWithError(str(e), change_desc)
3039
3040 def _CMDUploadChange(self, options, git_diff_args, custom_cl_base,
3041 change_desc, branch):
3042 """Upload the current branch to Gerrit."""
3043 if options.squash:
3044 Changelist._GerritCommitMsgHookCheck(
3045 offer_removal=not options.force)
3046 external_parent = None
3047 if self.GetIssue():
3048 # User requested to change description
3049 if options.edit_description:
3050 change_desc.prompt()
3051 change_detail = self._GetChangeDetail(['CURRENT_REVISION'])
3052 change_id = change_detail['change_id']
3053 change_desc.ensure_change_id(change_id)
3054
3055 # Check if changes outside of this workspace have been uploaded.
3056 current_rev = change_detail['current_revision']
3057 last_uploaded_rev = self._GitGetBranchConfigValue(
3058 GERRIT_SQUASH_HASH_CONFIG_KEY)
3059 if last_uploaded_rev and current_rev != last_uploaded_rev:
3060 external_parent = self._UpdateWithExternalChanges()
3061 else: # if not self.GetIssue()
3062 if not options.force and not options.message_file:
3063 change_desc.prompt()
3064 change_ids = git_footers.get_footer_change_id(
3065 change_desc.description)
3066 if len(change_ids) == 1:
3067 change_id = change_ids[0]
3068 else:
3069 change_id = GenerateGerritChangeId(change_desc.description)
3070 change_desc.ensure_change_id(change_id)
3071
3072 if options.preserve_tryjobs:
3073 change_desc.set_preserve_tryjobs()
3074
3075 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
3076 parent = external_parent or self._ComputeParent(
3077 remote, upstream_branch, custom_cl_base, options.force,
3078 change_desc)
3079 tree = RunGit(['rev-parse', 'HEAD:']).strip()
3080 with gclient_utils.temporary_file() as desc_tempfile:
3081 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
3082 ref_to_push = RunGit(
3083 ['commit-tree', tree, '-p', parent, '-F',
3084 desc_tempfile]).strip()
3085 else: # if not options.squash
3086 if options.no_add_changeid:
3087 pass
3088 else: # adding Change-Ids is okay.
3089 if not git_footers.get_footer_change_id(
3090 change_desc.description):
3091 DownloadGerritHook(False)
3092 change_desc.set_description(
3093 self._AddChangeIdToCommitMessage(
3094 change_desc.description, git_diff_args))
3095 ref_to_push = 'HEAD'
3096 # For no-squash mode, we assume the remote called "origin" is the
3097 # one we want. It is not worthwhile to support different workflows
3098 # for no-squash mode.
3099 parent = 'origin/%s' % branch
3100 # attempt to extract the changeid from the current description
3101 # fail informatively if not possible.
3102 change_id_candidates = git_footers.get_footer_change_id(
3103 change_desc.description)
3104 if not change_id_candidates:
3105 DieWithError("Unable to extract change-id from message.")
3106 change_id = change_id_candidates[0]
3107
3108 SaveDescriptionBackup(change_desc)
3109 commits = RunGitSilent(['rev-list',
3110 '%s..%s' % (parent, ref_to_push)]).splitlines()
3111 if len(commits) > 1:
3112 print(
3113 'WARNING: This will upload %d commits. Run the following command '
3114 'to see which commits will be uploaded: ' % len(commits))
3115 print('git log %s..%s' % (parent, ref_to_push))
3116 print('You can also use `git squash-branch` to squash these into a '
3117 'single commit.')
3118 confirm_or_exit(action='upload')
3119
3120 reviewers = sorted(change_desc.get_reviewers())
3121 cc = []
3122 # Add default, watchlist, presubmit ccs if this is the initial upload
3123 # and CL is not private and auto-ccing has not been disabled.
3124 if not options.private and not options.no_autocc and not self.GetIssue(
3125 ):
3126 cc = self.GetCCList().split(',')
3127 if len(cc) > 100:
3128 lsc = ('https://chromium.googlesource.com/chromium/src/+/HEAD/docs/'
3129 'process/lsc/lsc_workflow.md')
3130 print('WARNING: This will auto-CC %s users.' % len(cc))
3131 print('LSC may be more appropriate: %s' % lsc)
3132 print('You can also use the --no-autocc flag to disable auto-CC.')
3133 confirm_or_exit(action='continue')
3134 # Add cc's from the --cc flag.
3135 if options.cc:
3136 cc.extend(options.cc)
3137 cc = [email.strip() for email in cc if email.strip()]
3138 if change_desc.get_cced():
3139 cc.extend(change_desc.get_cced())
3140 if self.GetGerritHost() == 'chromium-review.googlesource.com':
3141 valid_accounts = set(reviewers + cc)
3142 # TODO(crbug/877717): relax this for all hosts.
3143 else:
3144 valid_accounts = gerrit_util.ValidAccounts(self.GetGerritHost(),
3145 reviewers + cc)
3146 logging.info('accounts %s are recognized, %s invalid',
3147 sorted(valid_accounts),
3148 set(reviewers + cc).difference(set(valid_accounts)))
3149
3150 # Extra options that can be specified at push time. Doc:
3151 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
3152 refspec_opts = self._GetRefSpecOptions(options, change_desc)
3153
3154 for r in sorted(reviewers):
3155 if r in valid_accounts:
3156 refspec_opts.append('r=%s' % r)
3157 reviewers.remove(r)
3158 else:
3159 # TODO(tandrii): this should probably be a hard failure.
3160 print(
3161 'WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
3162 % r)
3163 for c in sorted(cc):
3164 # refspec option will be rejected if cc doesn't correspond to an
3165 # account, even though REST call to add such arbitrary cc may
3166 # succeed.
3167 if c in valid_accounts:
3168 refspec_opts.append('cc=%s' % c)
3169 cc.remove(c)
3170
3171 refspec_suffix = ''
3172 if refspec_opts:
3173 refspec_suffix = '%' + ','.join(refspec_opts)
3174 assert ' ' not in refspec_suffix, (
3175 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3176 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3177
3178 git_push_metadata = {
3179 'gerrit_host': self.GetGerritHost(),
3180 'title': options.title or '<untitled>',
3181 'change_id': change_id,
3182 'description': change_desc.description,
3183 }
3184
3185 # Gerrit may or may not update fast enough to return the correct
3186 # patchset number after we push. Get the pre-upload patchset and
3187 # increment later.
3188 latest_ps = self.GetMostRecentPatchset(update=False) or 0
3189
3190 push_stdout = self._RunGitPushWithTraces(refspec, refspec_opts,
3191 git_push_metadata,
3192 options.push_options)
3193
3194 if options.squash:
3195 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
3196 change_numbers = [
3197 m.group(1) for m in map(regex.match, push_stdout.splitlines())
3198 if m
3199 ]
3200 if len(change_numbers) != 1:
3201 DieWithError((
3202 'Created|Updated %d issues on Gerrit, but only 1 expected.\n'
3203 'Change-Id: %s') % (len(change_numbers), change_id),
3204 change_desc)
3205 self.SetIssue(change_numbers[0])
3206 self.SetPatchset(latest_ps + 1)
3207 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY,
3208 ref_to_push)
3209
3210 if self.GetIssue() and (reviewers or cc):
3211 # GetIssue() is not set in case of non-squash uploads according to
3212 # tests. TODO(crbug.com/751901): non-squash uploads in git cl should
3213 # be removed.
3214 gerrit_util.AddReviewers(self.GetGerritHost(),
3215 self._GerritChangeIdentifier(),
3216 reviewers,
3217 cc,
3218 notify=bool(options.send_mail))
3219
3220 return 0
3221
3222 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3223 change_desc):
3224 """Computes parent of the generated commit to be uploaded to Gerrit.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003225
3226 Returns revision or a ref name.
3227 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003228 if custom_cl_base:
3229 # Try to avoid creating additional unintended CLs when uploading,
3230 # unless user wants to take this risk.
3231 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3232 code, _ = RunGitWithCode([
3233 'merge-base', '--is-ancestor', custom_cl_base,
3234 local_ref_of_target_remote
3235 ])
3236 if code == 1:
3237 print(
3238 '\nWARNING: Manually specified base of this CL `%s` '
3239 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3240 'If you proceed with upload, more than 1 CL may be created by '
3241 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3242 'If you are certain that specified base `%s` has already been '
3243 'uploaded to Gerrit as another CL, you may proceed.\n' %
3244 (custom_cl_base, local_ref_of_target_remote,
3245 custom_cl_base))
3246 if not force:
3247 confirm_or_exit(
3248 'Do you take responsibility for cleaning up potential mess '
3249 'resulting from proceeding with upload?',
3250 action='upload')
3251 return custom_cl_base
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003252
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003253 if remote != '.':
3254 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003255
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003256 # If our upstream branch is local, we base our squashed commit on its
3257 # squashed version.
3258 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
Aaron Gablef97e33d2017-03-30 15:44:27 -07003259
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003260 if upstream_branch_name == 'master':
3261 return self.GetCommonAncestorWithUpstream()
3262 if upstream_branch_name == 'main':
3263 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003264
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003265 # Check the squashed hash of the parent.
3266 # TODO(tandrii): consider checking parent change in Gerrit and using its
3267 # hash if tree hash of latest parent revision (patchset) in Gerrit
3268 # matches the tree hash of the parent branch. The upside is less likely
3269 # bogus requests to reupload parent change just because it's uploadhash
3270 # is missing, yet the downside likely exists, too (albeit unknown to me
3271 # yet).
3272 parent = scm.GIT.GetBranchConfig(settings.GetRoot(),
3273 upstream_branch_name,
3274 GERRIT_SQUASH_HASH_CONFIG_KEY)
3275 # Verify that the upstream branch has been uploaded too, otherwise
3276 # Gerrit will create additional CLs when uploading.
3277 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3278 RunGitSilent(['rev-parse', parent + ':'])):
3279 DieWithError(
3280 '\nUpload upstream branch %s first.\n'
3281 'It is likely that this branch has been rebased since its last '
3282 'upload, so you just need to upload it again.\n'
3283 '(If you uploaded it with --no-squash, then branch dependencies '
3284 'are not supported, and you should reupload with --squash.)' %
3285 upstream_branch_name, change_desc)
3286 return parent
Aaron Gablef97e33d2017-03-30 15:44:27 -07003287
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003288 def _UpdateWithExternalChanges(self):
3289 """Updates workspace with external changes.
Gavin Mak4e5e3992022-11-14 22:40:12 +00003290
3291 Returns the commit hash that should be used as the merge base on upload.
3292 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003293 local_ps = self.GetPatchset()
3294 if local_ps is None:
3295 return
Gavin Mak4e5e3992022-11-14 22:40:12 +00003296
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003297 external_ps = self.GetMostRecentPatchset(update=False)
3298 if external_ps is None or local_ps == external_ps or \
3299 not self._IsPatchsetRangeSignificant(local_ps + 1, external_ps):
3300 return
Gavin Mak4e5e3992022-11-14 22:40:12 +00003301
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003302 num_changes = external_ps - local_ps
3303 if num_changes > 1:
3304 change_words = 'changes were'
3305 else:
3306 change_words = 'change was'
3307 print('\n%d external %s published to %s:\n' %
3308 (num_changes, change_words, self.GetIssueURL(short=True)))
Gavin Mak6f905472023-01-06 21:01:36 +00003309
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003310 # Print an overview of external changes.
3311 ps_to_commit = {}
3312 ps_to_info = {}
3313 revisions = self._GetChangeDetail(['ALL_REVISIONS'])
3314 for commit_id, revision_info in revisions.get('revisions', {}).items():
3315 ps_num = revision_info['_number']
3316 ps_to_commit[ps_num] = commit_id
3317 ps_to_info[ps_num] = revision_info
Gavin Mak6f905472023-01-06 21:01:36 +00003318
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003319 for ps in range(external_ps, local_ps, -1):
3320 commit = ps_to_commit[ps][:8]
3321 desc = ps_to_info[ps].get('description', '')
3322 print('Patchset %d [%s] %s' % (ps, commit, desc))
Gavin Mak6f905472023-01-06 21:01:36 +00003323
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003324 print('\nSee diff at: %s/%d..%d' %
3325 (self.GetIssueURL(short=True), local_ps, external_ps))
3326 print('\nUploading without applying patches will override them.')
Josip Sokcevic43ceaf02023-05-25 15:56:00 +00003327
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003328 if not ask_for_explicit_yes('Get the latest changes and apply on top?'):
3329 return
Gavin Mak4e5e3992022-11-14 22:40:12 +00003330
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003331 # Get latest Gerrit merge base. Use the first parent even if multiple
3332 # exist.
3333 external_parent = self._GetChangeCommit(
3334 revision=external_ps)['parents'][0]
3335 external_base = external_parent['commit']
Gavin Mak4e5e3992022-11-14 22:40:12 +00003336
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003337 branch = git_common.current_branch()
3338 local_base = self.GetCommonAncestorWithUpstream()
3339 if local_base != external_base:
3340 print('\nLocal merge base %s is different from Gerrit %s.\n' %
3341 (local_base, external_base))
3342 if git_common.upstream(branch):
3343 confirm_or_exit(
3344 'Can\'t apply the latest changes from Gerrit.\n'
3345 'Continue with upload and override the latest changes?')
3346 return
3347 print(
3348 'No upstream branch set. Continuing upload with Gerrit merge base.'
3349 )
Gavin Mak4e5e3992022-11-14 22:40:12 +00003350
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003351 external_parent_last_uploaded = self._GetChangeCommit(
3352 revision=local_ps)['parents'][0]
3353 external_base_last_uploaded = external_parent_last_uploaded['commit']
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003354
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003355 if external_base != external_base_last_uploaded:
3356 print('\nPatch set merge bases are different (%s, %s).\n' %
3357 (external_base_last_uploaded, external_base))
3358 confirm_or_exit(
3359 'Can\'t apply the latest changes from Gerrit.\n'
3360 'Continue with upload and override the latest changes?')
3361 return
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003362
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003363 # Fetch Gerrit's CL base if it doesn't exist locally.
3364 remote, _ = self.GetRemoteBranch()
3365 if not scm.GIT.IsValidRevision(settings.GetRoot(), external_base):
3366 RunGitSilent(['fetch', remote, external_base])
Gavin Mak4e5e3992022-11-14 22:40:12 +00003367
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003368 # Get the diff between local_ps and external_ps.
3369 print('Fetching changes...')
3370 issue = self.GetIssue()
3371 changes_ref = 'refs/changes/%02d/%d/' % (issue % 100, issue)
3372 RunGitSilent(['fetch', remote, changes_ref + str(local_ps)])
3373 last_uploaded = RunGitSilent(['rev-parse', 'FETCH_HEAD']).strip()
3374 RunGitSilent(['fetch', remote, changes_ref + str(external_ps)])
3375 latest_external = RunGitSilent(['rev-parse', 'FETCH_HEAD']).strip()
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003376
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003377 # If the commit parents are different, don't apply the diff as it very
3378 # likely contains many more changes not relevant to this CL.
3379 parents = RunGitSilent(
3380 ['rev-parse',
3381 '%s~1' % (last_uploaded),
3382 '%s~1' % (latest_external)]).strip().split()
3383 assert len(parents) == 2, 'Expected two parents.'
3384 if parents[0] != parents[1]:
3385 confirm_or_exit(
3386 'Can\'t apply the latest changes from Gerrit (parent mismatch '
3387 'between PS).\n'
3388 'Continue with upload and override the latest changes?')
3389 return
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003390
Joanna Wangbcba1782023-09-12 22:48:05 +00003391 diff = RunGitSilent([
3392 'diff', '--no-ext-diff',
3393 '%s..%s' % (last_uploaded, latest_external)
3394 ])
Gavin Mak4e5e3992022-11-14 22:40:12 +00003395
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003396 # Diff can be empty in the case of trivial rebases.
3397 if not diff:
3398 return external_base
Gavin Mak4e5e3992022-11-14 22:40:12 +00003399
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003400 # Apply the diff.
3401 with gclient_utils.temporary_file() as diff_tempfile:
3402 gclient_utils.FileWrite(diff_tempfile, diff)
3403 clean_patch = RunGitWithCode(['apply', '--check',
3404 diff_tempfile])[0] == 0
3405 RunGitSilent(['apply', '-3', '--intent-to-add', diff_tempfile])
3406 if not clean_patch:
3407 # Normally patchset is set after upload. But because we exit,
3408 # that never happens. Updating here makes sure that subsequent
3409 # uploads don't need to fetch/apply the same diff again.
3410 self.SetPatchset(external_ps)
3411 DieWithError(
3412 '\nPatch did not apply cleanly. Please resolve any '
3413 'conflicts and reupload.')
Gavin Mak4e5e3992022-11-14 22:40:12 +00003414
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003415 message = 'Incorporate external changes from '
3416 if num_changes == 1:
3417 message += 'patchset %d' % external_ps
3418 else:
3419 message += 'patchsets %d to %d' % (local_ps + 1, external_ps)
3420 RunGitSilent(['commit', '-am', message])
3421 # TODO(crbug.com/1382528): Use the previous commit's message as a
3422 # default patchset title instead of this 'Incorporate' message.
3423 return external_base
Gavin Mak4e5e3992022-11-14 22:40:12 +00003424
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003425 def _AddChangeIdToCommitMessage(self, log_desc, args):
3426 """Re-commits using the current message, assumes the commit hook is in
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003427 place.
3428 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003429 RunGit(['commit', '--amend', '-m', log_desc])
3430 new_log_desc = _create_description_from_log(args)
3431 if git_footers.get_footer_change_id(new_log_desc):
3432 print('git-cl: Added Change-Id to commit message.')
3433 return new_log_desc
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003434
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003435 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003436
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003437 def CannotTriggerTryJobReason(self):
3438 try:
3439 data = self._GetChangeDetail()
3440 except GerritChangeNotExists:
3441 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003442
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003443 if data['status'] in ('ABANDONED', 'MERGED'):
3444 return 'CL %s is closed' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003445
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003446 def GetGerritChange(self, patchset=None):
3447 """Returns a buildbucket.v2.GerritChange message for the current issue."""
3448 host = urllib.parse.urlparse(self.GetCodereviewServer()).hostname
3449 issue = self.GetIssue()
3450 patchset = int(patchset or self.GetPatchset())
3451 data = self._GetChangeDetail(['ALL_REVISIONS'])
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003452
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003453 assert host and issue and patchset, 'CL must be uploaded first'
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003454
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003455 has_patchset = any(
3456 int(revision_data['_number']) == patchset
3457 for revision_data in data['revisions'].values())
3458 if not has_patchset:
3459 raise Exception('Patchset %d is not known in Gerrit change %d' %
3460 (patchset, self.GetIssue()))
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003461
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003462 return {
3463 'host': host,
3464 'change': issue,
3465 'project': data['project'],
3466 'patchset': patchset,
3467 }
tandriie113dfd2016-10-11 10:20:12 -07003468
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003469 def GetIssueOwner(self):
3470 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003471
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003472 def GetReviewers(self):
3473 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3474 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01003475
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003476
Lei Zhang8a0efc12020-08-05 19:58:45 +00003477def _get_bug_line_values(default_project_prefix, bugs):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003478 """Given default_project_prefix and comma separated list of bugs, yields bug
Lei Zhang8a0efc12020-08-05 19:58:45 +00003479 line values.
tandriif9aefb72016-07-01 09:06:51 -07003480
3481 Each bug can be either:
Lei Zhang8a0efc12020-08-05 19:58:45 +00003482 * a number, which is combined with default_project_prefix
tandriif9aefb72016-07-01 09:06:51 -07003483 * string, which is left as is.
3484
3485 This function may produce more than one line, because bugdroid expects one
3486 project per line.
3487
Lei Zhang8a0efc12020-08-05 19:58:45 +00003488 >>> list(_get_bug_line_values('v8:', '123,chromium:789'))
tandriif9aefb72016-07-01 09:06:51 -07003489 ['v8:123', 'chromium:789']
3490 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003491 default_bugs = []
3492 others = []
3493 for bug in bugs.split(','):
3494 bug = bug.strip()
3495 if bug:
3496 try:
3497 default_bugs.append(int(bug))
3498 except ValueError:
3499 others.append(bug)
tandriif9aefb72016-07-01 09:06:51 -07003500
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003501 if default_bugs:
3502 default_bugs = ','.join(map(str, default_bugs))
3503 if default_project_prefix:
3504 if not default_project_prefix.endswith(':'):
3505 default_project_prefix += ':'
3506 yield '%s%s' % (default_project_prefix, default_bugs)
3507 else:
3508 yield default_bugs
3509 for other in sorted(others):
3510 # Don't bother finding common prefixes, CLs with >2 bugs are very very
3511 # rare.
3512 yield other
tandriif9aefb72016-07-01 09:06:51 -07003513
3514
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003515def FindCodereviewSettingsFile(filename='codereview.settings'):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003516 """Finds the given file starting in the cwd and going up.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003517
3518 Only looks up to the top of the repository unless an
3519 'inherit-review-settings-ok' file exists in the root of the repository.
3520 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003521 inherit_ok_file = 'inherit-review-settings-ok'
3522 cwd = os.getcwd()
3523 root = settings.GetRoot()
3524 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3525 root = None
3526 while True:
3527 if os.path.isfile(os.path.join(cwd, filename)):
3528 return open(os.path.join(cwd, filename))
3529 if cwd == root:
3530 break
3531 parent_dir = os.path.dirname(cwd)
3532 if parent_dir == cwd:
3533 # We hit the system root directory.
3534 break
3535 cwd = parent_dir
3536 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003537
3538
3539def LoadCodereviewSettingsFromFile(fileobj):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003540 """Parses a codereview.settings file and updates hooks."""
3541 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003542
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003543 def SetProperty(name, setting, unset_error_ok=False):
3544 fullname = 'rietveld.' + name
3545 if setting in keyvals:
3546 RunGit(['config', fullname, keyvals[setting]])
3547 else:
3548 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003549
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003550 if not keyvals.get('GERRIT_HOST', False):
3551 SetProperty('server', 'CODE_REVIEW_SERVER')
3552 # Only server setting is required. Other settings can be absent.
3553 # In that case, we ignore errors raised during option deletion attempt.
3554 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3555 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3556 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
3557 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
3558 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3559 SetProperty('cpplint-ignore-regex',
3560 'LINT_IGNORE_REGEX',
3561 unset_error_ok=True)
3562 SetProperty('run-post-upload-hook',
3563 'RUN_POST_UPLOAD_HOOK',
3564 unset_error_ok=True)
3565 SetProperty('format-full-by-default',
3566 'FORMAT_FULL_BY_DEFAULT',
3567 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003568
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003569 if 'GERRIT_HOST' in keyvals:
3570 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003571
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003572 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
3573 RunGit([
3574 'config', 'gerrit.squash-uploads', keyvals['GERRIT_SQUASH_UPLOADS']
3575 ])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003576
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003577 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
3578 RunGit([
3579 'config', 'gerrit.skip-ensure-authenticated',
3580 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']
3581 ])
tandrii@chromium.org28253532016-04-14 13:46:56 +00003582
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003583 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3584 # should be of the form
3585 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3586 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
3587 RunGit([
3588 'config', keyvals['PUSH_URL_CONFIG'], keyvals['ORIGIN_URL_CONFIG']
3589 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003590
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003591
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003592def urlretrieve(source, destination):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003593 """Downloads a network object to a local file, like urllib.urlretrieve.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003594
3595 This is necessary because urllib is broken for SSL connections via a proxy.
3596 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003597 with open(destination, 'wb') as f:
3598 f.write(urllib.request.urlopen(source).read())
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003599
3600
ukai@chromium.org712d6102013-11-27 00:52:58 +00003601def hasSheBang(fname):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003602 """Checks fname is a #! script."""
3603 with open(fname) as f:
3604 return f.read(2).startswith('#!')
ukai@chromium.org712d6102013-11-27 00:52:58 +00003605
3606
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003607def DownloadGerritHook(force):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003608 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003609
3610 Args:
3611 force: True to update hooks. False to install hooks if not present.
3612 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003613 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
3614 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3615 if not os.access(dst, os.X_OK):
3616 if os.path.exists(dst):
3617 if not force:
3618 return
3619 try:
3620 urlretrieve(src, dst)
3621 if not hasSheBang(dst):
3622 DieWithError('Not a script: %s\n'
3623 'You need to download from\n%s\n'
3624 'into .git/hooks/commit-msg and '
3625 'chmod +x .git/hooks/commit-msg' % (dst, src))
3626 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3627 except Exception:
3628 if os.path.exists(dst):
3629 os.remove(dst)
3630 DieWithError('\nFailed to download hooks.\n'
3631 'You need to download from\n%s\n'
3632 'into .git/hooks/commit-msg and '
3633 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003634
3635
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003636class _GitCookiesChecker(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003637 """Provides facilities for validating and suggesting fixes to .gitcookies."""
3638 def __init__(self):
3639 # Cached list of [host, identity, source], where source is either
3640 # .gitcookies or .netrc.
3641 self._all_hosts = None
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003642
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003643 def ensure_configured_gitcookies(self):
3644 """Runs checks and suggests fixes to make git use .gitcookies from default
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003645 path."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003646 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3647 configured_path = RunGitSilent(
3648 ['config', '--global', 'http.cookiefile']).strip()
3649 configured_path = os.path.expanduser(configured_path)
3650 if configured_path:
3651 self._ensure_default_gitcookies_path(configured_path, default)
3652 else:
3653 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003654
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003655 @staticmethod
3656 def _ensure_default_gitcookies_path(configured_path, default_path):
3657 assert configured_path
3658 if configured_path == default_path:
3659 print('git is already configured to use your .gitcookies from %s' %
3660 configured_path)
3661 return
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003662
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003663 print('WARNING: You have configured custom path to .gitcookies: %s\n'
3664 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3665 (configured_path, default_path))
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003666
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003667 if not os.path.exists(configured_path):
3668 print('However, your configured .gitcookies file is missing.')
3669 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3670 action='reconfigure')
3671 RunGit(['config', '--global', 'http.cookiefile', default_path])
3672 return
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003673
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003674 if os.path.exists(default_path):
3675 print('WARNING: default .gitcookies file already exists %s' %
3676 default_path)
3677 DieWithError(
3678 'Please delete %s manually and re-run git cl creds-check' %
3679 default_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003680
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003681 confirm_or_exit('Move existing .gitcookies to default location?',
3682 action='move')
3683 shutil.move(configured_path, default_path)
3684 RunGit(['config', '--global', 'http.cookiefile', default_path])
3685 print('Moved and reconfigured git to use .gitcookies from %s' %
3686 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003687
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003688 @staticmethod
3689 def _configure_gitcookies_path(default_path):
3690 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3691 if os.path.exists(netrc_path):
3692 print(
3693 'You seem to be using outdated .netrc for git credentials: %s' %
3694 netrc_path)
3695 print(
3696 'This tool will guide you through setting up recommended '
3697 '.gitcookies store for git credentials.\n'
3698 '\n'
3699 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3700 ' git config --global --unset http.cookiefile\n'
3701 ' mv %s %s.backup\n\n' % (default_path, default_path))
3702 confirm_or_exit(action='setup .gitcookies')
3703 RunGit(['config', '--global', 'http.cookiefile', default_path])
3704 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003705
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003706 def get_hosts_with_creds(self, include_netrc=False):
3707 if self._all_hosts is None:
3708 a = gerrit_util.CookiesAuthenticator()
3709 self._all_hosts = [(h, u, s) for h, u, s in itertools.chain((
3710 (h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.items()), (
3711 (h, u, '.gitcookies')
3712 for h, (u, _) in a.gitcookies.items()))
3713 if h.endswith(_GOOGLESOURCE)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003714
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003715 if include_netrc:
3716 return self._all_hosts
3717 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003718
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003719 def print_current_creds(self, include_netrc=False):
3720 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3721 if not hosts:
3722 print('No Git/Gerrit credentials found')
3723 return
3724 lengths = [max(map(len, (row[i] for row in hosts))) for i in range(3)]
3725 header = [('Host', 'User', 'Which file'), ['=' * l for l in lengths]]
3726 for row in (header + hosts):
3727 print('\t'.join((('%%+%ds' % l) % s) for l, s in zip(lengths, row)))
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003728
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003729 @staticmethod
3730 def _parse_identity(identity):
3731 """Parses identity "git-<username>.domain" into <username> and domain."""
3732 # Special case: usernames that contain ".", which are generally not
3733 # distinguishable from sub-domains. But we do know typical domains:
3734 if identity.endswith('.chromium.org'):
3735 domain = 'chromium.org'
3736 username = identity[:-len('.chromium.org')]
3737 else:
3738 username, domain = identity.split('.', 1)
3739 if username.startswith('git-'):
3740 username = username[len('git-'):]
3741 return username, domain
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003742
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003743 def has_generic_host(self):
3744 """Returns whether generic .googlesource.com has been configured.
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003745
3746 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3747 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003748 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3749 if host == '.' + _GOOGLESOURCE:
3750 return True
3751 return False
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003752
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003753 def _get_git_gerrit_identity_pairs(self):
3754 """Returns map from canonic host to pair of identities (Git, Gerrit).
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003755
3756 One of identities might be None, meaning not configured.
3757 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003758 host_to_identity_pairs = {}
3759 for host, identity, _ in self.get_hosts_with_creds():
3760 canonical = _canonical_git_googlesource_host(host)
3761 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3762 idx = 0 if canonical == host else 1
3763 pair[idx] = identity
3764 return host_to_identity_pairs
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003765
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003766 def get_partially_configured_hosts(self):
3767 return set(
3768 (host if i1 else _canonical_gerrit_googlesource_host(host))
3769 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
3770 if None in (i1, i2) and host != '.' + _GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003771
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003772 def get_conflicting_hosts(self):
3773 return set(
3774 host
3775 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
3776 if None not in (i1, i2) and i1 != i2)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003777
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003778 def get_duplicated_hosts(self):
3779 counters = collections.Counter(
3780 h for h, _, _ in self.get_hosts_with_creds())
3781 return set(host for host, count in counters.items() if count > 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003782
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003783 @staticmethod
3784 def _format_hosts(hosts, extra_column_func=None):
3785 hosts = sorted(hosts)
3786 assert hosts
3787 if extra_column_func is None:
3788 extras = [''] * len(hosts)
3789 else:
3790 extras = [extra_column_func(host) for host in hosts]
3791 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len,
3792 extras)))
3793 lines = []
3794 for he in zip(hosts, extras):
3795 lines.append(tmpl % he)
3796 return lines
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003797
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003798 def _find_problems(self):
3799 if self.has_generic_host():
3800 yield ('.googlesource.com wildcard record detected', [
3801 'Chrome Infrastructure team recommends to list full host names '
3802 'explicitly.'
3803 ], None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003804
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003805 dups = self.get_duplicated_hosts()
3806 if dups:
3807 yield ('The following hosts were defined twice',
3808 self._format_hosts(dups), None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003809
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003810 partial = self.get_partially_configured_hosts()
3811 if partial:
3812 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3813 'These hosts are missing',
3814 self._format_hosts(
3815 partial, lambda host: 'but %s defined' %
3816 _get_counterpart_host(host)), partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003817
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003818 conflicting = self.get_conflicting_hosts()
3819 if conflicting:
3820 yield (
3821 'The following Git hosts have differing credentials from their '
3822 'Gerrit counterparts',
3823 self._format_hosts(
3824 conflicting, lambda host: '%s vs %s' % tuple(
3825 self._get_git_gerrit_identity_pairs()[host])),
3826 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003827
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003828 def find_and_report_problems(self):
3829 """Returns True if there was at least one problem, else False."""
3830 found = False
3831 bad_hosts = set()
3832 for title, sublines, hosts in self._find_problems():
3833 if not found:
3834 found = True
3835 print('\n\n.gitcookies problem report:\n')
3836 bad_hosts.update(hosts or [])
3837 print(' %s%s' % (title, (':' if sublines else '')))
3838 if sublines:
3839 print()
3840 print(' %s' % '\n '.join(sublines))
3841 print()
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003842
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003843 if bad_hosts:
3844 assert found
3845 print(
3846 ' You can manually remove corresponding lines in your %s file and '
3847 'visit the following URLs with correct account to generate '
3848 'correct credential lines:\n' %
3849 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3850 print(' %s' % '\n '.join(
3851 sorted(
3852 set(gerrit_util.CookiesAuthenticator().get_new_password_url(
3853 _canonical_git_googlesource_host(host))
3854 for host in bad_hosts))))
3855 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003856
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003857
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003858@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003859def CMDcreds_check(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003860 """Checks credentials and suggests changes."""
3861 _, _ = parser.parse_args(args)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003862
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003863 # Code below checks .gitcookies. Abort if using something else.
3864 authn = gerrit_util.Authenticator.get()
3865 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3866 message = (
3867 'This command is not designed for bot environment. It checks '
3868 '~/.gitcookies file not generally used on bots.')
3869 # TODO(crbug.com/1059384): Automatically detect when running on
3870 # cloudtop.
3871 if isinstance(authn, gerrit_util.GceAuthenticator):
3872 message += (
3873 '\n'
3874 'If you need to run this on GCE or a cloudtop instance, '
3875 'export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
3876 DieWithError(message)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003877
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003878 checker = _GitCookiesChecker()
3879 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003880
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003881 print('Your .netrc and .gitcookies have credentials for these hosts:')
3882 checker.print_current_creds(include_netrc=True)
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003883
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003884 if not checker.find_and_report_problems():
3885 print('\nNo problems detected in your .gitcookies file.')
3886 return 0
3887 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003888
3889
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003890@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003891def CMDbaseurl(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003892 """Gets or sets base-url for this branch."""
3893 _, args = parser.parse_args(args)
3894 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
3895 branch = scm.GIT.ShortBranchName(branchref)
3896 if not args:
3897 print('Current base-url:')
3898 return RunGit(['config', 'branch.%s.base-url' % branch],
3899 error_ok=False).strip()
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003900
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003901 print('Setting base-url to %s' % args[0])
3902 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3903 error_ok=False).strip()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003904
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003905
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003906def color_for_status(status):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003907 """Maps a Changelist status to color, for CMDstatus and other tools."""
3908 BOLD = '\033[1m'
3909 return {
3910 'unsent': BOLD + Fore.YELLOW,
3911 'waiting': BOLD + Fore.RED,
3912 'reply': BOLD + Fore.YELLOW,
3913 'not lgtm': BOLD + Fore.RED,
3914 'lgtm': BOLD + Fore.GREEN,
3915 'commit': BOLD + Fore.MAGENTA,
3916 'closed': BOLD + Fore.CYAN,
3917 'error': BOLD + Fore.WHITE,
3918 }.get(status, Fore.WHITE)
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003919
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003920
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003921def get_cl_statuses(changes, fine_grained, max_processes=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003922 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003923
3924 If fine_grained is true, this will fetch CL statuses from the server.
3925 Otherwise, simply indicate if there's a matching url for the given branches.
3926
3927 If max_processes is specified, it is used as the maximum number of processes
3928 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3929 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003930
3931 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003932 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003933 if not changes:
3934 return
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003935
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003936 if not fine_grained:
3937 # Fast path which doesn't involve querying codereview servers.
3938 # Do not use get_approving_reviewers(), since it requires an HTTP
3939 # request.
3940 for cl in changes:
3941 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
3942 return
3943
3944 # First, sort out authentication issues.
3945 logging.debug('ensuring credentials exist')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003946 for cl in changes:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003947 cl.EnsureAuthenticated(force=False, refresh=True)
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003948
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003949 def fetch(cl):
3950 try:
3951 return (cl, cl.GetStatus())
3952 except:
3953 # See http://crbug.com/629863.
3954 logging.exception('failed to fetch status for cl %s:',
3955 cl.GetIssue())
3956 raise
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003957
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003958 threads_count = len(changes)
3959 if max_processes:
3960 threads_count = max(1, min(threads_count, max_processes))
3961 logging.debug('querying %d CLs using %d threads', len(changes),
3962 threads_count)
3963
3964 pool = multiprocessing.pool.ThreadPool(threads_count)
3965 fetched_cls = set()
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003966 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003967 it = pool.imap_unordered(fetch, changes).__iter__()
3968 while True:
3969 try:
3970 cl, status = it.next(timeout=5)
3971 except (multiprocessing.TimeoutError, StopIteration):
3972 break
3973 fetched_cls.add(cl)
3974 yield cl, status
3975 finally:
3976 pool.close()
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003977
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003978 # Add any branches that failed to fetch.
3979 for cl in set(changes) - fetched_cls:
3980 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003981
rmistry@google.com2dd99862015-06-22 12:22:18 +00003982
Jose Lopes3863fc52020-04-07 17:00:25 +00003983def upload_branch_deps(cl, args, force=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003984 """Uploads CLs of local branches that are dependents of the current branch.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003985
3986 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003987
3988 test1 -> test2.1 -> test3.1
3989 -> test3.2
3990 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003991
3992 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3993 run on the dependent branches in this order:
3994 test2.1, test3.1, test3.2, test2.2, test3.3
3995
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003996 Note: This function does not rebase your local dependent branches. Use it
3997 when you make a change to the parent branch that will not conflict
3998 with its dependent branches, and you would like their dependencies
3999 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00004000 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004001 if git_common.is_dirty_git_tree('upload-branch-deps'):
4002 return 1
rmistry@google.com2dd99862015-06-22 12:22:18 +00004003
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004004 root_branch = cl.GetBranch()
4005 if root_branch is None:
4006 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4007 'Get on a branch!')
4008 if not cl.GetIssue():
4009 DieWithError(
4010 'Current branch does not have an uploaded CL. We cannot set '
4011 'patchset dependencies without an uploaded CL.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004012
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004013 branches = RunGit([
4014 'for-each-ref', '--format=%(refname:short) %(upstream:short)',
4015 'refs/heads'
4016 ])
4017 if not branches:
4018 print('No local branches found.')
4019 return 0
rmistry@google.com2dd99862015-06-22 12:22:18 +00004020
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004021 # Create a dictionary of all local branches to the branches that are
4022 # dependent on it.
4023 tracked_to_dependents = collections.defaultdict(list)
4024 for b in branches.splitlines():
4025 tokens = b.split()
4026 if len(tokens) == 2:
4027 branch_name, tracked = tokens
4028 tracked_to_dependents[tracked].append(branch_name)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004029
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004030 print()
4031 print('The dependent local branches of %s are:' % root_branch)
4032 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004033
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004034 def traverse_dependents_preorder(branch, padding=''):
4035 dependents_to_process = tracked_to_dependents.get(branch, [])
4036 padding += ' '
4037 for dependent in dependents_to_process:
4038 print('%s%s' % (padding, dependent))
4039 dependents.append(dependent)
4040 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004041
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004042 traverse_dependents_preorder(root_branch)
4043 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004044
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004045 if not dependents:
4046 print('There are no dependent local branches for %s' % root_branch)
4047 return 0
rmistry@google.com2dd99862015-06-22 12:22:18 +00004048
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004049 # Record all dependents that failed to upload.
4050 failures = {}
4051 # Go through all dependents, checkout the branch and upload.
4052 try:
4053 for dependent_branch in dependents:
4054 print()
4055 print('--------------------------------------')
4056 print('Running "git cl upload" from %s:' % dependent_branch)
4057 RunGit(['checkout', '-q', dependent_branch])
4058 print()
4059 try:
4060 if CMDupload(OptionParser(), args) != 0:
4061 print('Upload failed for %s!' % dependent_branch)
4062 failures[dependent_branch] = 1
4063 except: # pylint: disable=bare-except
4064 failures[dependent_branch] = 1
4065 print()
4066 finally:
4067 # Swap back to the original root branch.
4068 RunGit(['checkout', '-q', root_branch])
4069
4070 print()
4071 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004072 for dependent_branch in dependents:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004073 upload_status = 'failed' if failures.get(
4074 dependent_branch) else 'succeeded'
4075 print(' %s : %s' % (dependent_branch, upload_status))
4076 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004077
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004078 return 0
rmistry@google.com2dd99862015-06-22 12:22:18 +00004079
4080
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00004081def GetArchiveTagForBranch(issue_num, branch_name, existing_tags, pattern):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004082 """Given a proposed tag name, returns a tag name that is guaranteed to be
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00004083 unique. If 'foo' is proposed but already exists, then 'foo-2' is used,
4084 or 'foo-3', and so on."""
4085
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004086 proposed_tag = pattern.format(**{'issue': issue_num, 'branch': branch_name})
4087 for suffix_num in itertools.count(1):
4088 if suffix_num == 1:
4089 to_check = proposed_tag
4090 else:
4091 to_check = '%s-%d' % (proposed_tag, suffix_num)
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00004092
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004093 if to_check not in existing_tags:
4094 return to_check
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00004095
4096
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004097@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004098def CMDarchive(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004099 """Archives and deletes branches associated with closed changelists."""
4100 parser.add_option(
4101 '-j',
4102 '--maxjobs',
4103 action='store',
4104 type=int,
4105 help='The maximum number of jobs to use when retrieving review status.')
4106 parser.add_option('-f',
4107 '--force',
4108 action='store_true',
4109 help='Bypasses the confirmation prompt.')
4110 parser.add_option('-d',
4111 '--dry-run',
4112 action='store_true',
4113 help='Skip the branch tagging and removal steps.')
4114 parser.add_option('-t',
4115 '--notags',
4116 action='store_true',
4117 help='Do not tag archived branches. '
4118 'Note: local commit history may be lost.')
4119 parser.add_option('-p',
4120 '--pattern',
4121 default='git-cl-archived-{issue}-{branch}',
4122 help='Format string for archive tags. '
4123 'E.g. \'archived-{issue}-{branch}\'.')
kmarshall3bff56b2016-06-06 18:31:47 -07004124
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004125 options, args = parser.parse_args(args)
4126 if args:
4127 parser.error('Unsupported args: %s' % ' '.join(args))
kmarshall3bff56b2016-06-06 18:31:47 -07004128
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004129 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4130 if not branches:
4131 return 0
4132
4133 tags = RunGit(['for-each-ref', '--format=%(refname)', 'refs/tags'
4134 ]).splitlines() or []
4135 tags = [t.split('/')[-1] for t in tags]
4136
4137 print('Finding all branches associated with closed issues...')
4138 changes = [Changelist(branchref=b) for b in branches.splitlines()]
4139 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4140 statuses = get_cl_statuses(changes,
4141 fine_grained=True,
4142 max_processes=options.maxjobs)
4143 proposal = [(cl.GetBranch(),
4144 GetArchiveTagForBranch(cl.GetIssue(), cl.GetBranch(), tags,
4145 options.pattern))
4146 for cl, status in statuses
4147 if status in ('closed', 'rietveld-not-supported')]
4148 proposal.sort()
4149
4150 if not proposal:
4151 print('No branches with closed codereview issues found.')
4152 return 0
4153
4154 current_branch = scm.GIT.GetBranch(settings.GetRoot())
4155
4156 print('\nBranches with closed issues that will be archived:\n')
4157 if options.notags:
4158 for next_item in proposal:
4159 print(' ' + next_item[0])
4160 else:
4161 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4162 for next_item in proposal:
4163 print('%*s %s' % (alignment, next_item[0], next_item[1]))
4164
4165 # Quit now on precondition failure or if instructed by the user, either
4166 # via an interactive prompt or by command line flags.
4167 if options.dry_run:
4168 print('\nNo changes were made (dry run).\n')
4169 return 0
4170
4171 if any(branch == current_branch for branch, _ in proposal):
4172 print('You are currently on a branch \'%s\' which is associated with a '
4173 'closed codereview issue, so archive cannot proceed. Please '
4174 'checkout another branch and run this command again.' %
4175 current_branch)
4176 return 1
4177
4178 if not options.force:
4179 answer = gclient_utils.AskForData(
4180 '\nProceed with deletion (Y/n)? ').lower()
4181 if answer not in ('y', ''):
4182 print('Aborted.')
4183 return 1
4184
4185 for branch, tagname in proposal:
4186 if not options.notags:
4187 RunGit(['tag', tagname, branch])
4188
4189 if RunGitWithCode(['branch', '-D', branch])[0] != 0:
4190 # Clean up the tag if we failed to delete the branch.
4191 RunGit(['tag', '-d', tagname])
4192
4193 print('\nJob\'s done!')
4194
kmarshall3bff56b2016-06-06 18:31:47 -07004195 return 0
4196
kmarshall3bff56b2016-06-06 18:31:47 -07004197
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004198@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004199def CMDstatus(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004200 """Show status of changelists.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004201
4202 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004203 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004204 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004205 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004206 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004207 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004208 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004209 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004210
4211 Also see 'git cl comments'.
4212 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004213 parser.add_option('--no-branch-color',
4214 action='store_true',
4215 help='Disable colorized branch names')
4216 parser.add_option(
4217 '--field', help='print only specific field (desc|id|patch|status|url)')
4218 parser.add_option('-f',
4219 '--fast',
4220 action='store_true',
4221 help='Do not retrieve review status')
4222 parser.add_option(
4223 '-j',
4224 '--maxjobs',
4225 action='store',
4226 type=int,
4227 help='The maximum number of jobs to use when retrieving review status')
4228 parser.add_option(
4229 '-i',
4230 '--issue',
4231 type=int,
4232 help='Operate on this issue instead of the current branch\'s implicit '
4233 'issue. Requires --field to be set.')
4234 parser.add_option('-d',
4235 '--date-order',
4236 action='store_true',
4237 help='Order branches by committer date.')
4238 options, args = parser.parse_args(args)
4239 if args:
4240 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004241
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004242 if options.issue is not None and not options.field:
4243 parser.error('--field must be given when --issue is set.')
iannucci3c972b92016-08-17 13:24:10 -07004244
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004245 if options.field:
4246 cl = Changelist(issue=options.issue)
4247 if options.field.startswith('desc'):
4248 if cl.GetIssue():
4249 print(cl.FetchDescription())
4250 elif options.field == 'id':
4251 issueid = cl.GetIssue()
4252 if issueid:
4253 print(issueid)
4254 elif options.field == 'patch':
4255 patchset = cl.GetMostRecentPatchset()
4256 if patchset:
4257 print(patchset)
4258 elif options.field == 'status':
4259 print(cl.GetStatus())
4260 elif options.field == 'url':
4261 url = cl.GetIssueURL()
4262 if url:
4263 print(url)
4264 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004265
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004266 branches = RunGit([
4267 'for-each-ref', '--format=%(refname) %(committerdate:unix)',
4268 'refs/heads'
4269 ])
4270 if not branches:
4271 print('No local branch found.')
4272 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004273
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004274 changes = [
4275 Changelist(branchref=b, commit_date=ct)
4276 for b, ct in map(lambda line: line.split(' '), branches.splitlines())
4277 ]
4278 print('Branches associated with reviews:')
4279 output = get_cl_statuses(changes,
4280 fine_grained=not options.fast,
4281 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004282
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004283 current_branch = scm.GIT.GetBranch(settings.GetRoot())
Daniel McArdlea23bf592019-02-12 00:25:12 +00004284
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004285 def FormatBranchName(branch, colorize=False):
4286 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
Daniel McArdlea23bf592019-02-12 00:25:12 +00004287 an asterisk when it is the current branch."""
4288
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004289 asterisk = ""
4290 color = Fore.RESET
4291 if branch == current_branch:
4292 asterisk = "* "
4293 color = Fore.GREEN
4294 branch_name = scm.GIT.ShortBranchName(branch)
Daniel McArdlea23bf592019-02-12 00:25:12 +00004295
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004296 if colorize:
4297 return asterisk + color + branch_name + Fore.RESET
4298 return asterisk + branch_name
Daniel McArdle452a49f2019-02-14 17:28:31 +00004299
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004300 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00004301
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004302 alignment = max(5,
4303 max(len(FormatBranchName(c.GetBranch())) for c in changes))
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +00004304
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004305 if options.date_order or settings.IsStatusCommitOrderByDate():
4306 sorted_changes = sorted(changes,
4307 key=lambda c: c.GetCommitDate(),
4308 reverse=True)
4309 else:
4310 sorted_changes = sorted(changes, key=lambda c: c.GetBranch())
4311 for cl in sorted_changes:
4312 branch = cl.GetBranch()
4313 while branch not in branch_statuses:
4314 c, status = next(output)
4315 branch_statuses[c.GetBranch()] = status
4316 status = branch_statuses.pop(branch)
4317 url = cl.GetIssueURL(short=True)
4318 if url and (not status or status == 'error'):
4319 # The issue probably doesn't exist anymore.
4320 url += ' (broken)'
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004321
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004322 color = color_for_status(status)
4323 # Turn off bold as well as colors.
4324 END = '\033[0m'
4325 reset = Fore.RESET + END
4326 if not setup_color.IS_TTY:
4327 color = ''
4328 reset = ''
4329 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00004330
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004331 branch_display = FormatBranchName(branch)
4332 padding = ' ' * (alignment - len(branch_display))
4333 if not options.no_branch_color:
4334 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00004335
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004336 print(' %s : %s%s %s%s' %
4337 (padding + branch_display, color, url, status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004338
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004339 print()
4340 print('Current branch: %s' % current_branch)
4341 for cl in changes:
4342 if cl.GetBranch() == current_branch:
4343 break
4344 if not cl.GetIssue():
4345 print('No issue assigned.')
4346 return 0
4347 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4348 if not options.fast:
4349 print('Issue description:')
4350 print(cl.FetchDescription(pretty=True))
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004351 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004352
4353
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004354def colorize_CMDstatus_doc():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004355 """To be called once in main() to add colors to git cl status help."""
4356 colors = [i for i in dir(Fore) if i[0].isupper()]
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004357
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004358 def colorize_line(line):
4359 for color in colors:
4360 if color in line.upper():
4361 # Extract whitespace first and the leading '-'.
4362 indent = len(line) - len(line.lstrip(' ')) + 1
4363 return line[:indent] + getattr(
4364 Fore, color) + line[indent:] + Fore.RESET
4365 return line
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004366
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004367 lines = CMDstatus.__doc__.splitlines()
4368 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004369
4370
phajdan.jre328cf92016-08-22 04:12:17 -07004371def write_json(path, contents):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004372 if path == '-':
4373 json.dump(contents, sys.stdout)
4374 else:
4375 with open(path, 'w') as f:
4376 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004377
4378
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004379@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004380@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004381def CMDissue(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004382 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004383
4384 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004385 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004386 parser.add_option('-r',
4387 '--reverse',
4388 action='store_true',
4389 help='Lookup the branch(es) for the specified issues. If '
4390 'no issues are specified, all branches with mapped '
4391 'issues will be listed.')
4392 parser.add_option('--json',
4393 help='Path to JSON output file, or "-" for stdout.')
4394 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004395
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004396 if options.reverse:
4397 branches = RunGit(['for-each-ref', 'refs/heads',
4398 '--format=%(refname)']).splitlines()
4399 # Reverse issue lookup.
4400 issue_branch_map = {}
Arthur Milchior801a9752023-04-07 10:33:54 +00004401
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004402 git_config = {}
4403 for config in RunGit(['config', '--get-regexp',
4404 r'branch\..*issue']).splitlines():
4405 name, _space, val = config.partition(' ')
4406 git_config[name] = val
Arthur Milchior801a9752023-04-07 10:33:54 +00004407
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004408 for branch in branches:
4409 issue = git_config.get(
4410 'branch.%s.%s' %
4411 (scm.GIT.ShortBranchName(branch), ISSUE_CONFIG_KEY))
4412 if issue:
4413 issue_branch_map.setdefault(int(issue), []).append(branch)
4414 if not args:
4415 args = sorted(issue_branch_map.keys())
4416 result = {}
4417 for issue in args:
4418 try:
4419 issue_num = int(issue)
4420 except ValueError:
4421 print('ERROR cannot parse issue number: %s' % issue,
4422 file=sys.stderr)
4423 continue
4424 result[issue_num] = issue_branch_map.get(issue_num)
4425 print('Branch for issue number %s: %s' % (issue, ', '.join(
4426 issue_branch_map.get(issue_num) or ('None', ))))
4427 if options.json:
4428 write_json(options.json, result)
4429 return 0
4430
4431 if len(args) > 0:
4432 issue = ParseIssueNumberArgument(args[0])
4433 if not issue.valid:
4434 DieWithError(
4435 'Pass a url or number to set the issue, 0 to unset it, '
4436 'or no argument to list it.\n'
4437 'Maybe you want to run git cl status?')
4438 cl = Changelist()
4439 cl.SetIssue(issue.issue)
4440 else:
4441 cl = Changelist()
4442 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
Arthur Milchior801a9752023-04-07 10:33:54 +00004443 if options.json:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004444 write_json(
4445 options.json, {
4446 'gerrit_host': cl.GetGerritHost(),
4447 'gerrit_project': cl.GetGerritProject(),
4448 'issue_url': cl.GetIssueURL(),
4449 'issue': cl.GetIssue(),
4450 })
Arthur Milchior801a9752023-04-07 10:33:54 +00004451 return 0
Aaron Gable78753da2017-06-15 10:35:49 -07004452
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004453
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004454@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004455def CMDcomments(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004456 """Shows or posts review comments for any changelist."""
4457 parser.add_option('-a',
4458 '--add-comment',
4459 dest='comment',
4460 help='comment to add to an issue')
4461 parser.add_option('-p',
4462 '--publish',
4463 action='store_true',
4464 help='marks CL as ready and sends comment to reviewers')
4465 parser.add_option('-i',
4466 '--issue',
4467 dest='issue',
4468 help='review issue id (defaults to current issue).')
4469 parser.add_option('-m',
4470 '--machine-readable',
4471 dest='readable',
4472 action='store_false',
4473 default=True,
4474 help='output comments in a format compatible with '
4475 'editor parsing')
4476 parser.add_option('-j',
4477 '--json-file',
4478 help='File to write JSON summary to, or "-" for stdout')
4479 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004480
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004481 issue = None
4482 if options.issue:
4483 try:
4484 issue = int(options.issue)
4485 except ValueError:
4486 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004487
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004488 cl = Changelist(issue=issue)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004489
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004490 if options.comment:
4491 cl.AddComment(options.comment, options.publish)
4492 return 0
4493
4494 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4495 key=lambda c: c.date)
4496 for comment in summary:
4497 if comment.disapproval:
4498 color = Fore.RED
4499 elif comment.approval:
4500 color = Fore.GREEN
4501 elif comment.sender == cl.GetIssueOwner():
4502 color = Fore.MAGENTA
4503 elif comment.autogenerated:
4504 color = Fore.CYAN
4505 else:
4506 color = Fore.BLUE
4507 print('\n%s%s %s%s\n%s' %
4508 (color, comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4509 comment.sender, Fore.RESET, '\n'.join(
4510 ' ' + l for l in comment.message.strip().splitlines())))
4511
4512 if options.json_file:
4513
4514 def pre_serialize(c):
4515 dct = c._asdict().copy()
4516 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4517 return dct
4518
4519 write_json(options.json_file, [pre_serialize(x) for x in summary])
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004520 return 0
4521
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004522
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004523@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004524@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004525def CMDdescription(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004526 """Brings up the editor for the current CL's description."""
4527 parser.add_option(
4528 '-d',
4529 '--display',
4530 action='store_true',
4531 help='Display the description instead of opening an editor')
4532 parser.add_option(
4533 '-n',
4534 '--new-description',
4535 help='New description to set for this issue (- for stdin, '
4536 '+ to load from local commit HEAD)')
4537 parser.add_option('-f',
4538 '--force',
4539 action='store_true',
4540 help='Delete any unpublished Gerrit edits for this issue '
4541 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004542
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004543 options, args = parser.parse_args(args)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004544
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004545 target_issue_arg = None
4546 if len(args) > 0:
4547 target_issue_arg = ParseIssueNumberArgument(args[0])
4548 if not target_issue_arg.valid:
4549 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004550
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004551 kwargs = {}
4552 if target_issue_arg:
4553 kwargs['issue'] = target_issue_arg.issue
4554 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07004555
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004556 cl = Changelist(**kwargs)
4557 if not cl.GetIssue():
4558 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004559
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004560 if args and not args[0].isdigit():
4561 logging.info('canonical issue/change URL: %s\n', cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004562
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004563 description = ChangeDescription(cl.FetchDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004564
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004565 if options.display:
4566 print(description.description)
4567 return 0
4568
4569 if options.new_description:
4570 text = options.new_description
4571 if text == '-':
4572 text = '\n'.join(l.rstrip() for l in sys.stdin)
4573 elif text == '+':
4574 base_branch = cl.GetCommonAncestorWithUpstream()
4575 text = _create_description_from_log([base_branch])
4576
4577 description.set_description(text)
4578 else:
4579 description.prompt()
4580 if cl.FetchDescription().strip() != description.description:
4581 cl.UpdateDescription(description.description, force=options.force)
smut@google.com34fb6b12015-07-13 20:03:26 +00004582 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004583
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004584
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004585@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004586def CMDlint(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004587 """Runs cpplint on the current changelist."""
4588 parser.add_option(
4589 '--filter',
4590 action='append',
4591 metavar='-x,+y',
4592 help='Comma-separated list of cpplint\'s category-filters')
4593 options, args = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004594
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004595 # Access to a protected member _XX of a client class
4596 # pylint: disable=protected-access
4597 try:
4598 import cpplint
4599 import cpplint_chromium
4600 except ImportError:
4601 print(
4602 'Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.'
4603 )
4604 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004605
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004606 # Change the current working directory before calling lint so that it
4607 # shows the correct base.
4608 previous_cwd = os.getcwd()
4609 os.chdir(settings.GetRoot())
4610 try:
4611 cl = Changelist()
4612 files = cl.GetAffectedFiles(cl.GetCommonAncestorWithUpstream())
4613 if not files:
4614 print('Cannot lint an empty CL')
4615 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004616
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004617 # Process cpplint arguments, if any.
4618 filters = presubmit_canned_checks.GetCppLintFilters(options.filter)
4619 command = ['--filter=' + ','.join(filters)]
4620 command.extend(args)
4621 command.extend(files)
4622 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004623
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004624 include_regex = re.compile(settings.GetLintRegex())
4625 ignore_regex = re.compile(settings.GetLintIgnoreRegex())
4626 extra_check_functions = [
4627 cpplint_chromium.CheckPointerDeclarationWhitespace
4628 ]
4629 for filename in filenames:
4630 if not include_regex.match(filename):
4631 print('Skipping file %s' % filename)
4632 continue
Lei Zhang379d1ad2020-07-15 19:40:06 +00004633
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004634 if ignore_regex.match(filename):
4635 print('Ignoring file %s' % filename)
4636 continue
Lei Zhang379d1ad2020-07-15 19:40:06 +00004637
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004638 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4639 extra_check_functions)
4640 finally:
4641 os.chdir(previous_cwd)
4642 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
4643 if cpplint._cpplint_state.error_count != 0:
4644 return 1
4645 return 0
thestig@chromium.org44202a22014-03-11 19:22:18 +00004646
4647
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004648@metrics.collector.collect_metrics('git cl presubmit')
mlcuic601e362023-08-14 23:39:46 +00004649@subcommand.usage('[base branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004650def CMDpresubmit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004651 """Runs presubmit tests on the current changelist."""
4652 parser.add_option('-u',
4653 '--upload',
4654 action='store_true',
4655 help='Run upload hook instead of the push hook')
4656 parser.add_option('-f',
4657 '--force',
4658 action='store_true',
4659 help='Run checks even if tree is dirty')
4660 parser.add_option(
4661 '--all',
4662 action='store_true',
4663 help='Run checks against all files, not just modified ones')
4664 parser.add_option('--files',
4665 nargs=1,
4666 help='Semicolon-separated list of files to be marked as '
4667 'modified when executing presubmit or post-upload hooks. '
4668 'fnmatch wildcards can also be used.')
4669 parser.add_option(
4670 '--parallel',
4671 action='store_true',
4672 help='Run all tests specified by input_api.RunTests in all '
4673 'PRESUBMIT files in parallel.')
4674 parser.add_option('--resultdb',
4675 action='store_true',
4676 help='Run presubmit checks in the ResultSink environment '
4677 'and send results to the ResultDB database.')
4678 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
4679 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004680
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004681 if not options.force and git_common.is_dirty_git_tree('presubmit'):
4682 print('use --force to check even if tree is dirty.')
4683 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004684
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004685 cl = Changelist()
4686 if args:
4687 base_branch = args[0]
Bruce Dawsoneb8426e2022-08-05 23:58:15 +00004688 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004689 # Default to diffing against the common ancestor of the upstream branch.
4690 base_branch = cl.GetCommonAncestorWithUpstream()
Aaron Gable8076c282017-11-29 14:39:41 -08004691
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004692 start = time.time()
4693 try:
4694 if not 'PRESUBMIT_SKIP_NETWORK' in os.environ and cl.GetIssue():
4695 description = cl.FetchDescription()
4696 else:
4697 description = _create_description_from_log([base_branch])
4698 except Exception as e:
4699 print('Failed to fetch CL description - %s' % str(e))
4700 description = _create_description_from_log([base_branch])
4701 elapsed = time.time() - start
4702 if elapsed > 5:
4703 print('%.1f s to get CL description.' % elapsed)
Bruce Dawson13acea32022-05-03 22:13:08 +00004704
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004705 if not base_branch:
4706 if not options.force:
4707 print('use --force to check even when not on a branch.')
4708 return 1
4709 base_branch = 'HEAD'
4710
4711 cl.RunHook(committing=not options.upload,
4712 may_prompt=False,
4713 verbose=options.verbose,
4714 parallel=options.parallel,
4715 upstream=base_branch,
4716 description=description,
4717 all_files=options.all,
4718 files=options.files,
4719 resultdb=options.resultdb,
4720 realm=options.realm)
4721 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004722
4723
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004724def GenerateGerritChangeId(message):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004725 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004726
4727 Works the same way as
4728 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4729 but can be called on demand on all platforms.
4730
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004731 The basic idea is to generate git hash of a state of the tree, original
4732 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004733 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004734 lines = []
4735 tree_hash = RunGitSilent(['write-tree'])
4736 lines.append('tree %s' % tree_hash.strip())
4737 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'],
4738 suppress_stderr=False)
4739 if code == 0:
4740 lines.append('parent %s' % parent.strip())
4741 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4742 lines.append('author %s' % author.strip())
4743 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4744 lines.append('committer %s' % committer.strip())
4745 lines.append('')
4746 # Note: Gerrit's commit-hook actually cleans message of some lines and
4747 # whitespace. This code is not doing this, but it clearly won't decrease
4748 # entropy.
4749 lines.append(message)
4750 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4751 stdin=('\n'.join(lines)).encode())
4752 return 'I%s' % change_hash.strip()
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004753
4754
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004755def GetTargetRef(remote, remote_branch, target_branch):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004756 """Computes the remote branch ref to use for the CL.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004757
4758 Args:
4759 remote (str): The git remote for the CL.
4760 remote_branch (str): The git remote branch for the CL.
4761 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004762 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004763 if not (remote and remote_branch):
4764 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004765
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004766 if target_branch:
4767 # Canonicalize branch references to the equivalent local full symbolic
4768 # refs, which are then translated into the remote full symbolic refs
4769 # below.
4770 if '/' not in target_branch:
4771 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4772 else:
4773 prefix_replacements = (
4774 ('^((refs/)?remotes/)?branch-heads/',
4775 'refs/remotes/branch-heads/'),
4776 ('^((refs/)?remotes/)?%s/' % remote,
4777 'refs/remotes/%s/' % remote),
4778 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4779 )
4780 match = None
4781 for regex, replacement in prefix_replacements:
4782 match = re.search(regex, target_branch)
4783 if match:
4784 remote_branch = target_branch.replace(
4785 match.group(0), replacement)
4786 break
4787 if not match:
4788 # This is a branch path but not one we recognize; use as-is.
4789 remote_branch = target_branch
4790 # pylint: disable=consider-using-get
4791 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4792 # pylint: enable=consider-using-get
4793 # Handle the refs that need to land in different refs.
4794 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004795
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004796 # Create the true path to the remote branch.
4797 # Does the following translation:
4798 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4799 # * refs/remotes/origin/main -> refs/heads/main
4800 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4801 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4802 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4803 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4804 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4805 'refs/heads/')
4806 elif remote_branch.startswith('refs/remotes/branch-heads'):
4807 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004808
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004809 return remote_branch
wittman@chromium.org455dc922015-01-26 20:15:50 +00004810
4811
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004812def cleanup_list(l):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004813 """Fixes a list so that comma separated items are put as individual items.
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004814
4815 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4816 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4817 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004818 items = sum((i.split(',') for i in l), [])
4819 stripped_items = (i.strip() for i in items)
4820 return sorted(filter(None, stripped_items))
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004821
4822
Aaron Gable4db38df2017-11-03 14:59:07 -07004823@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004824@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004825def CMDupload(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004826 """Uploads the current changelist to codereview.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004827
4828 Can skip dependency patchset uploads for a branch by running:
4829 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004830 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00004831 git config --unset branch.branch_name.skip-deps-uploads
4832 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004833
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004834 If the name of the checked out branch starts with "bug-" or "fix-" followed
4835 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02004836 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004837
4838 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004839 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004840 [git-cl] add support for hashtags
4841 Foo bar: implement foo
4842 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004843 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004844 parser.add_option('--bypass-hooks',
4845 action='store_true',
4846 dest='bypass_hooks',
4847 help='bypass upload presubmit hook')
4848 parser.add_option('--bypass-watchlists',
4849 action='store_true',
4850 dest='bypass_watchlists',
4851 help='bypass watchlists auto CC-ing reviewers')
4852 parser.add_option('-f',
4853 '--force',
4854 action='store_true',
4855 dest='force',
4856 help="force yes to questions (don't prompt)")
4857 parser.add_option('--message',
4858 '-m',
4859 dest='message',
4860 help='message for patchset')
4861 parser.add_option('-b',
4862 '--bug',
4863 help='pre-populate the bug number(s) for this issue. '
4864 'If several, separate with commas')
4865 parser.add_option('--message-file',
4866 dest='message_file',
4867 help='file which contains message for patchset')
4868 parser.add_option('--title', '-t', dest='title', help='title for patchset')
4869 parser.add_option('-T',
4870 '--skip-title',
4871 action='store_true',
4872 dest='skip_title',
4873 help='Use the most recent commit message as the title of '
4874 'the patchset')
4875 parser.add_option('-r',
4876 '--reviewers',
4877 action='append',
4878 default=[],
4879 help='reviewer email addresses')
4880 parser.add_option('--cc',
4881 action='append',
4882 default=[],
4883 help='cc email addresses')
4884 parser.add_option('--hashtag',
4885 dest='hashtags',
4886 action='append',
4887 default=[],
4888 help=('Gerrit hashtag for new CL; '
4889 'can be applied multiple times'))
4890 parser.add_option('-s',
4891 '--send-mail',
4892 '--send-email',
4893 dest='send_mail',
4894 action='store_true',
4895 help='send email to reviewer(s) and cc(s) immediately')
4896 parser.add_option('--target_branch',
4897 '--target-branch',
4898 metavar='TARGET',
4899 help='Apply CL to remote ref TARGET. ' +
4900 'Default: remote branch head, or main')
4901 parser.add_option('--squash',
4902 action='store_true',
4903 help='Squash multiple commits into one')
4904 parser.add_option('--no-squash',
4905 action='store_false',
4906 dest='squash',
4907 help='Don\'t squash multiple commits into one')
4908 parser.add_option('--topic',
4909 default=None,
4910 help='Topic to specify when uploading')
4911 parser.add_option('--r-owners',
4912 dest='add_owners_to',
4913 action='store_const',
4914 const='R',
4915 help='add a set of OWNERS to R')
4916 parser.add_option('-c',
4917 '--use-commit-queue',
4918 action='store_true',
4919 default=False,
4920 help='tell the CQ to commit this patchset; '
4921 'implies --send-mail')
4922 parser.add_option('-d',
4923 '--cq-dry-run',
4924 action='store_true',
4925 default=False,
4926 help='Send the patchset to do a CQ dry run right after '
4927 'upload.')
4928 parser.add_option('--set-bot-commit',
4929 action='store_true',
4930 help=optparse.SUPPRESS_HELP)
4931 parser.add_option('--preserve-tryjobs',
4932 action='store_true',
4933 help='instruct the CQ to let tryjobs running even after '
4934 'new patchsets are uploaded instead of canceling '
4935 'prior patchset\' tryjobs')
4936 parser.add_option(
4937 '--dependencies',
4938 action='store_true',
4939 help='Uploads CLs of all the local branches that depend on '
4940 'the current branch')
4941 parser.add_option(
4942 '-a',
4943 '--enable-auto-submit',
4944 action='store_true',
4945 help='Sends your change to the CQ after an approval. Only '
4946 'works on repos that have the Auto-Submit label '
4947 'enabled')
4948 parser.add_option(
4949 '--parallel',
4950 action='store_true',
4951 help='Run all tests specified by input_api.RunTests in all '
4952 'PRESUBMIT files in parallel.')
4953 parser.add_option('--no-autocc',
4954 action='store_true',
4955 help='Disables automatic addition of CC emails')
4956 parser.add_option('--private',
4957 action='store_true',
4958 help='Set the review private. This implies --no-autocc.')
4959 parser.add_option('-R',
4960 '--retry-failed',
4961 action='store_true',
4962 help='Retry failed tryjobs from old patchset immediately '
4963 'after uploading new patchset. Cannot be used with '
4964 '--use-commit-queue or --cq-dry-run.')
4965 parser.add_option('--fixed',
4966 '-x',
4967 help='List of bugs that will be commented on and marked '
4968 'fixed (pre-populates "Fixed:" tag). Same format as '
4969 '-b option / "Bug:" tag. If fixing several issues, '
4970 'separate with commas.')
4971 parser.add_option('--edit-description',
4972 action='store_true',
4973 default=False,
4974 help='Modify description before upload. Cannot be used '
4975 'with --force. It is a noop when --no-squash is set '
4976 'or a new commit is created.')
4977 parser.add_option('--git-completion-helper',
4978 action="store_true",
4979 help=optparse.SUPPRESS_HELP)
4980 parser.add_option('-o',
4981 '--push-options',
4982 action='append',
4983 default=[],
4984 help='Transmit the given string to the server when '
4985 'performing git push (pass-through). See git-push '
4986 'documentation for more details.')
4987 parser.add_option('--no-add-changeid',
4988 action='store_true',
4989 dest='no_add_changeid',
4990 help='Do not add change-ids to messages.')
4991 parser.add_option('--cherry-pick-stacked',
4992 '--cp',
4993 dest='cherry_pick_stacked',
4994 action='store_true',
4995 help='If parent branch has un-uploaded updates, '
4996 'automatically skip parent branches and just upload '
4997 'the current branch cherry-pick on its parent\'s last '
4998 'uploaded commit. Allows users to skip the potential '
4999 'interactive confirmation step.')
5000 # TODO(b/265929888): Add --wip option of --cl-status option.
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00005001
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005002 orig_args = args
5003 (options, args) = parser.parse_args(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005004
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005005 if options.git_completion_helper:
5006 print(' '.join(opt.get_opt_string() for opt in parser.option_list
5007 if opt.help != optparse.SUPPRESS_HELP))
5008 return
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00005009
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005010 # TODO(crbug.com/1475405): Warn users if the project uses submodules and
5011 # they have fsmonitor enabled.
5012 if os.path.isfile('.gitmodules'):
5013 git_common.warn_submodule()
Aravind Vasudevanb8164182023-08-25 21:49:12 +00005014
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005015 if git_common.is_dirty_git_tree('upload'):
5016 return 1
ukai@chromium.orge8077812012-02-03 03:41:46 +00005017
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005018 options.reviewers = cleanup_list(options.reviewers)
5019 options.cc = cleanup_list(options.cc)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005020
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005021 if options.edit_description and options.force:
5022 parser.error('Only one of --force and --edit-description allowed')
Josipe827b0f2020-01-30 00:07:20 +00005023
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005024 if options.message_file:
5025 if options.message:
5026 parser.error('Only one of --message and --message-file allowed.')
5027 options.message = gclient_utils.FileRead(options.message_file)
tandriib80458a2016-06-23 12:20:07 -07005028
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005029 if ([options.cq_dry_run, options.use_commit_queue, options.retry_failed
5030 ].count(True) > 1):
5031 parser.error('Only one of --use-commit-queue, --cq-dry-run or '
5032 '--retry-failed is allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07005033
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005034 if options.skip_title and options.title:
5035 parser.error('Only one of --title and --skip-title allowed.')
Mario Bianuccicebfb4e2020-07-22 23:08:16 +00005036
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005037 if options.use_commit_queue:
5038 options.send_mail = True
Aaron Gableedbc4132017-09-11 13:22:28 -07005039
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005040 if options.squash is None:
5041 # Load default for user, repo, squash=true, in this order.
5042 options.squash = settings.GetSquashGerritUploads()
Edward Lesmes0dd54822020-03-26 18:24:25 +00005043
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005044 cl = Changelist(branchref=options.target_branch)
Joanna Wang5051ffe2023-03-01 22:24:07 +00005045
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005046 # Warm change details cache now to avoid RPCs later, reducing latency for
5047 # developers.
5048 if cl.GetIssue():
5049 cl._GetChangeDetail([
5050 'DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'
5051 ])
Joanna Wang5051ffe2023-03-01 22:24:07 +00005052
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005053 if options.retry_failed and not cl.GetIssue():
5054 print('No previous patchsets, so --retry-failed has no effect.')
5055 options.retry_failed = False
Joanna Wang5051ffe2023-03-01 22:24:07 +00005056
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005057 disable_dogfood_stacked_changes = os.environ.get(
5058 DOGFOOD_STACKED_CHANGES_VAR) == '0'
5059 dogfood_stacked_changes = os.environ.get(DOGFOOD_STACKED_CHANGES_VAR) == '1'
Joanna Wang5051ffe2023-03-01 22:24:07 +00005060
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005061 # Only print message for folks who don't have DOGFOOD_STACKED_CHANGES set
5062 # to an expected value.
5063 if (options.squash and not dogfood_stacked_changes
5064 and not disable_dogfood_stacked_changes):
5065 print(
5066 'This repo has been enrolled in the stacked changes dogfood.\n'
5067 '`git cl upload` now uploads the current branch and all upstream '
5068 'branches that have un-uploaded updates.\n'
5069 'Patches can now be reapplied with --force:\n'
5070 '`git cl patch --reapply --force`.\n'
5071 'Googlers may visit go/stacked-changes-dogfood for more information.\n'
5072 '\n'
5073 'Depot Tools no longer sets new uploads to "WIP". Please update the\n'
5074 '"Set new changes to "work in progress" by default" checkbox at\n'
5075 'https://<host>-review.googlesource.com/settings/\n'
5076 '\n'
5077 'To opt-out use `export DOGFOOD_STACKED_CHANGES=0`.\n'
5078 'To hide this message use `export DOGFOOD_STACKED_CHANGES=1`.\n'
5079 'File bugs at https://bit.ly/3Y6opoI\n')
Joanna Wang4786a412023-05-16 18:23:08 +00005080
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005081 if options.squash and not disable_dogfood_stacked_changes:
5082 if options.dependencies:
5083 parser.error(
5084 '--dependencies is not available for this dogfood workflow.')
Joanna Wang5051ffe2023-03-01 22:24:07 +00005085
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005086 if options.cherry_pick_stacked:
5087 try:
5088 orig_args.remove('--cherry-pick-stacked')
5089 except ValueError:
5090 orig_args.remove('--cp')
5091 UploadAllSquashed(options, orig_args)
5092 return 0
Joanna Wang18de1f62023-01-21 01:24:24 +00005093
Joanna Wangd75fc882023-03-01 21:53:34 +00005094 if options.cherry_pick_stacked:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005095 parser.error(
5096 '--cherry-pick-stacked is not available for this workflow.')
Joanna Wang18de1f62023-01-21 01:24:24 +00005097
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005098 # cl.GetMostRecentPatchset uses cached information, and can return the last
5099 # patchset before upload. Calling it here makes it clear that it's the
5100 # last patchset before upload. Note that GetMostRecentPatchset will fail
5101 # if no CL has been uploaded yet.
5102 if options.retry_failed:
5103 patchset = cl.GetMostRecentPatchset()
Joanna Wangd75fc882023-03-01 21:53:34 +00005104
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005105 ret = cl.CMDUpload(options, args, orig_args)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00005106
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005107 if options.retry_failed:
5108 if ret != 0:
5109 print('Upload failed, so --retry-failed has no effect.')
5110 return ret
5111 builds, _ = _fetch_latest_builds(cl,
5112 DEFAULT_BUILDBUCKET_HOST,
5113 latest_patchset=patchset)
5114 jobs = _filter_failed_for_retry(builds)
5115 if len(jobs) == 0:
5116 print('No failed tryjobs, so --retry-failed has no effect.')
5117 return ret
5118 _trigger_tryjobs(cl, jobs, options, patchset + 1)
Quinten Yearsleya19d3532019-09-30 21:54:39 +00005119
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005120 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00005121
5122
Daniel Cheng66d0f152023-08-29 23:21:58 +00005123def UploadAllSquashed(options: optparse.Values,
5124 orig_args: Sequence[str]) -> int:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005125 """Uploads the current and upstream branches (if necessary)."""
5126 cls, cherry_pick_current = _UploadAllPrecheck(options, orig_args)
Joanna Wang18de1f62023-01-21 01:24:24 +00005127
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005128 # Create commits.
5129 uploads_by_cl: List[Tuple[Changelist, _NewUpload]] = []
5130 if cherry_pick_current:
5131 parent = cls[1]._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
5132 new_upload = cls[0].PrepareCherryPickSquashedCommit(options, parent)
5133 uploads_by_cl.append((cls[0], new_upload))
5134 else:
5135 ordered_cls = list(reversed(cls))
Joanna Wangc710e2d2023-01-25 14:53:22 +00005136
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005137 cl = ordered_cls[0]
5138 # We can only support external changes when we're only uploading one
5139 # branch.
5140 parent = cl._UpdateWithExternalChanges() if len(
5141 ordered_cls) == 1 else None
5142 orig_parent = None
5143 if parent is None:
5144 origin = '.'
5145 branch = cl.GetBranch()
Joanna Wang74c53b62023-03-01 22:00:22 +00005146
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005147 while origin == '.':
5148 # Search for cl's closest ancestor with a gerrit hash.
5149 origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(
5150 branch)
5151 if origin == '.':
5152 upstream_branch = scm.GIT.ShortBranchName(
5153 upstream_branch_ref)
Joanna Wang7603f042023-03-01 22:17:36 +00005154
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005155 # Support the `git merge` and `git pull` workflow.
5156 if upstream_branch in ['master', 'main']:
5157 parent = cl.GetCommonAncestorWithUpstream()
5158 else:
5159 orig_parent = scm.GIT.GetBranchConfig(
5160 settings.GetRoot(), upstream_branch,
5161 LAST_UPLOAD_HASH_CONFIG_KEY)
5162 parent = scm.GIT.GetBranchConfig(
5163 settings.GetRoot(), upstream_branch,
5164 GERRIT_SQUASH_HASH_CONFIG_KEY)
5165 if parent:
5166 break
5167 branch = upstream_branch
5168 else:
5169 # Either the root of the tree is the cl's direct parent and the
5170 # while loop above only found empty branches between cl and the
5171 # root of the tree.
5172 parent = cl.GetCommonAncestorWithUpstream()
Joanna Wang6215dd02023-02-07 15:58:03 +00005173
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005174 if orig_parent is None:
5175 orig_parent = parent
5176 for i, cl in enumerate(ordered_cls):
5177 # If we're in the middle of the stack, set end_commit to
5178 # downstream's direct ancestor.
5179 if i + 1 < len(ordered_cls):
5180 child_base_commit = ordered_cls[
5181 i + 1].GetCommonAncestorWithUpstream()
5182 else:
5183 child_base_commit = None
5184 new_upload = cl.PrepareSquashedCommit(options,
5185 parent,
5186 orig_parent,
5187 end_commit=child_base_commit)
5188 uploads_by_cl.append((cl, new_upload))
5189 parent = new_upload.commit_to_push
5190 orig_parent = child_base_commit
Joanna Wangc710e2d2023-01-25 14:53:22 +00005191
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005192 # Create refspec options
5193 cl, new_upload = uploads_by_cl[-1]
5194 refspec_opts = cl._GetRefSpecOptions(
5195 options,
5196 new_upload.change_desc,
5197 multi_change_upload=len(uploads_by_cl) > 1,
5198 dogfood_path=True)
5199 refspec_suffix = ''
5200 if refspec_opts:
5201 refspec_suffix = '%' + ','.join(refspec_opts)
5202 assert ' ' not in refspec_suffix, (
5203 'spaces not allowed in refspec: "%s"' % refspec_suffix)
Joanna Wangc710e2d2023-01-25 14:53:22 +00005204
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005205 remote, remote_branch = cl.GetRemoteBranch()
5206 branch = GetTargetRef(remote, remote_branch, options.target_branch)
5207 refspec = '%s:refs/for/%s%s' % (new_upload.commit_to_push, branch,
5208 refspec_suffix)
Joanna Wangc710e2d2023-01-25 14:53:22 +00005209
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005210 # Git push
5211 git_push_metadata = {
5212 'gerrit_host':
5213 cl.GetGerritHost(),
5214 'title':
5215 options.title or '<untitled>',
5216 'change_id':
5217 git_footers.get_footer_change_id(new_upload.change_desc.description),
5218 'description':
5219 new_upload.change_desc.description,
5220 }
5221 push_stdout = cl._RunGitPushWithTraces(refspec, refspec_opts,
5222 git_push_metadata,
5223 options.push_options)
Joanna Wangc710e2d2023-01-25 14:53:22 +00005224
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005225 # Post push updates
5226 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
5227 change_numbers = [
5228 m.group(1) for m in map(regex.match, push_stdout.splitlines()) if m
5229 ]
Joanna Wangc710e2d2023-01-25 14:53:22 +00005230
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005231 for i, (cl, new_upload) in enumerate(uploads_by_cl):
5232 cl.PostUploadUpdates(options, new_upload, change_numbers[i])
Joanna Wangc710e2d2023-01-25 14:53:22 +00005233
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005234 return 0
Joanna Wang18de1f62023-01-21 01:24:24 +00005235
5236
5237def _UploadAllPrecheck(options, orig_args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005238 # type: (optparse.Values, Sequence[str]) -> Tuple[Sequence[Changelist],
5239 # bool]
5240 """Checks the state of the tree and gives the user uploading options
Joanna Wang18de1f62023-01-21 01:24:24 +00005241
5242 Returns: A tuple of the ordered list of changes that have new commits
5243 since their last upload and a boolean of whether the user wants to
5244 cherry-pick and upload the current branch instead of uploading all cls.
5245 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005246 cl = Changelist()
5247 if cl.GetBranch() is None:
5248 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
Joanna Wang6b98cdc2023-02-16 00:37:20 +00005249
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005250 branch_ref = None
5251 cls = []
5252 must_upload_upstream = False
5253 first_pass = True
Joanna Wang18de1f62023-01-21 01:24:24 +00005254
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005255 Changelist._GerritCommitMsgHookCheck(offer_removal=not options.force)
Joanna Wang18de1f62023-01-21 01:24:24 +00005256
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005257 while True:
5258 if len(cls) > _MAX_STACKED_BRANCHES_UPLOAD:
5259 DieWithError(
5260 'More than %s branches in the stack have not been uploaded.\n'
5261 'Are your branches in a misconfigured state?\n'
5262 'If not, please upload some upstream changes first.' %
5263 (_MAX_STACKED_BRANCHES_UPLOAD))
Joanna Wang18de1f62023-01-21 01:24:24 +00005264
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005265 cl = Changelist(branchref=branch_ref)
Joanna Wang18de1f62023-01-21 01:24:24 +00005266
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005267 # Only add CL if it has anything to commit.
5268 base_commit = cl.GetCommonAncestorWithUpstream()
5269 end_commit = RunGit(['rev-parse', cl.GetBranchRef()]).strip()
Joanna Wang6215dd02023-02-07 15:58:03 +00005270
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005271 commit_summary = _GetCommitCountSummary(base_commit, end_commit)
5272 if commit_summary:
5273 cls.append(cl)
5274 if (not first_pass and
5275 cl._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
5276 is None):
5277 # We are mid-stack and the user must upload their upstream
5278 # branches.
5279 must_upload_upstream = True
5280 print(f'Found change with {commit_summary}...')
5281 elif first_pass: # The current branch has nothing to commit. Exit.
5282 DieWithError('Branch %s has nothing to commit' % cl.GetBranch())
5283 # Else: A mid-stack branch has nothing to commit. We do not add it to
5284 # cls.
5285 first_pass = False
Joanna Wang6215dd02023-02-07 15:58:03 +00005286
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005287 # Cases below determine if we should continue to traverse up the tree.
5288 origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(
5289 cl.GetBranch())
5290 branch_ref = upstream_branch_ref # set branch for next run.
Joanna Wang18de1f62023-01-21 01:24:24 +00005291
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005292 upstream_branch = scm.GIT.ShortBranchName(upstream_branch_ref)
5293 upstream_last_upload = scm.GIT.GetBranchConfig(
5294 settings.GetRoot(), upstream_branch, LAST_UPLOAD_HASH_CONFIG_KEY)
Joanna Wang6215dd02023-02-07 15:58:03 +00005295
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005296 # Case 1: We've reached the beginning of the tree.
5297 if origin != '.':
5298 break
Joanna Wang18de1f62023-01-21 01:24:24 +00005299
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005300 # Case 2: If any upstream branches have never been uploaded,
5301 # the user MUST upload them unless they are empty. Continue to
5302 # next loop to add upstream if it is not empty.
5303 if not upstream_last_upload:
5304 continue
Joanna Wang18de1f62023-01-21 01:24:24 +00005305
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005306 # Case 3: If upstream's last_upload == cl.base_commit we do
5307 # not need to upload any more upstreams from this point on.
5308 # (Even if there may be diverged branches higher up the tree)
5309 if base_commit == upstream_last_upload:
5310 break
Joanna Wang18de1f62023-01-21 01:24:24 +00005311
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005312 # Case 4: If upstream's last_upload < cl.base_commit we are
5313 # uploading cl and upstream_cl.
5314 # Continue up the tree to check other branch relations.
5315 if scm.GIT.IsAncestor(upstream_last_upload, base_commit):
5316 continue
Joanna Wang18de1f62023-01-21 01:24:24 +00005317
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005318 # Case 5: If cl.base_commit < upstream's last_upload the user
5319 # must rebase before uploading.
5320 if scm.GIT.IsAncestor(base_commit, upstream_last_upload):
5321 DieWithError(
5322 'At least one branch in the stack has diverged from its upstream '
5323 'branch and does not contain its upstream\'s last upload.\n'
5324 'Please rebase the stack with `git rebase-update` before uploading.'
5325 )
Joanna Wang18de1f62023-01-21 01:24:24 +00005326
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005327 # The tree went through a rebase. LAST_UPLOAD_HASH_CONFIG_KEY no longer
5328 # has any relation to commits in the tree. Continue up the tree until we
5329 # hit the root.
Joanna Wang18de1f62023-01-21 01:24:24 +00005330
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005331 # We assume all cls in the stack have the same auth requirements and only
5332 # check this once.
5333 cls[0].EnsureAuthenticated(force=options.force)
Joanna Wang18de1f62023-01-21 01:24:24 +00005334
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005335 cherry_pick = False
5336 if len(cls) > 1:
5337 opt_message = ''
5338 branches = ', '.join([cl.branch for cl in cls])
5339 if len(orig_args):
5340 opt_message = ('options %s will be used for all uploads.\n' %
5341 orig_args)
5342 if must_upload_upstream:
5343 msg = ('At least one parent branch in `%s` has never been uploaded '
5344 'and must be uploaded before/with `%s`.\n' %
5345 (branches, cls[1].branch))
5346 if options.cherry_pick_stacked:
5347 DieWithError(msg)
5348 if not options.force:
5349 confirm_or_exit('\n' + opt_message + msg)
5350 else:
5351 if options.cherry_pick_stacked:
5352 print('cherry-picking `%s` on %s\'s last upload' %
5353 (cls[0].branch, cls[1].branch))
5354 cherry_pick = True
5355 elif not options.force:
5356 answer = gclient_utils.AskForData(
5357 '\n' + opt_message +
5358 'Press enter to update branches %s.\nOr type `n` to upload only '
5359 '`%s` cherry-picked on %s\'s last upload:' %
5360 (branches, cls[0].branch, cls[1].branch))
5361 if answer.lower() == 'n':
5362 cherry_pick = True
5363 return cls, cherry_pick
Joanna Wang18de1f62023-01-21 01:24:24 +00005364
5365
Francois Dorayd42c6812017-05-30 15:10:20 -04005366@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005367@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04005368def CMDsplit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005369 """Splits a branch into smaller branches and uploads CLs.
Francois Dorayd42c6812017-05-30 15:10:20 -04005370
5371 Creates a branch and uploads a CL for each group of files modified in the
5372 current branch that share a common OWNERS file. In the CL description and
Edward Lemurac5c55f2020-02-29 00:17:16 +00005373 comment, the string '$directory', is replaced with the directory containing
5374 the shared OWNERS file.
Francois Dorayd42c6812017-05-30 15:10:20 -04005375 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005376 parser.add_option('-d',
5377 '--description',
5378 dest='description_file',
5379 help='A text file containing a CL description in which '
5380 '$directory will be replaced by each CL\'s directory.')
5381 parser.add_option('-c',
5382 '--comment',
5383 dest='comment_file',
5384 help='A text file containing a CL comment.')
5385 parser.add_option(
5386 '-n',
5387 '--dry-run',
5388 dest='dry_run',
5389 action='store_true',
5390 default=False,
5391 help='List the files and reviewers for each CL that would '
5392 'be created, but don\'t create branches or CLs.')
5393 parser.add_option('--cq-dry-run',
5394 action='store_true',
5395 help='If set, will do a cq dry run for each uploaded CL. '
5396 'Please be careful when doing this; more than ~10 CLs '
5397 'has the potential to overload our build '
5398 'infrastructure. Try to upload these not during high '
5399 'load times (usually 11-3 Mountain View time). Email '
5400 'infra-dev@chromium.org with any questions.')
5401 parser.add_option(
5402 '-a',
5403 '--enable-auto-submit',
5404 action='store_true',
5405 dest='enable_auto_submit',
5406 default=True,
5407 help='Sends your change to the CQ after an approval. Only '
5408 'works on repos that have the Auto-Submit label '
5409 'enabled')
5410 parser.add_option(
5411 '--disable-auto-submit',
5412 action='store_false',
5413 dest='enable_auto_submit',
5414 help='Disables automatic sending of the changes to the CQ '
5415 'after approval. Note that auto-submit only works for '
5416 'repos that have the Auto-Submit label enabled.')
5417 parser.add_option('--max-depth',
5418 type='int',
5419 default=0,
5420 help='The max depth to look for OWNERS files. Useful for '
5421 'controlling the granularity of the split CLs, e.g. '
5422 '--max-depth=1 will only split by top-level '
5423 'directory. Specifying a value less than 1 means no '
5424 'limit on max depth.')
5425 parser.add_option('--topic',
5426 default=None,
5427 help='Topic to specify when uploading')
5428 options, _ = parser.parse_args(args)
Francois Dorayd42c6812017-05-30 15:10:20 -04005429
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005430 if not options.description_file:
5431 parser.error('No --description flag specified.')
Francois Dorayd42c6812017-05-30 15:10:20 -04005432
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005433 def WrappedCMDupload(args):
5434 return CMDupload(OptionParser(), args)
Francois Dorayd42c6812017-05-30 15:10:20 -04005435
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005436 return split_cl.SplitCl(options.description_file, options.comment_file,
5437 Changelist, WrappedCMDupload, options.dry_run,
5438 options.cq_dry_run, options.enable_auto_submit,
5439 options.max_depth, options.topic,
5440 settings.GetRoot())
Francois Dorayd42c6812017-05-30 15:10:20 -04005441
5442
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005443@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005444@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005445def CMDdcommit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005446 """DEPRECATED: Used to commit the current changelist via git-svn."""
5447 message = ('git-cl no longer supports committing to SVN repositories via '
5448 'git-svn. You probably want to use `git cl land` instead.')
5449 print(message)
5450 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005451
5452
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005453@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005454@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005455def CMDland(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005456 """Commits the current changelist via git.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005457
5458 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5459 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005460 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005461 parser.add_option('--bypass-hooks',
5462 action='store_true',
5463 dest='bypass_hooks',
5464 help='bypass upload presubmit hook')
5465 parser.add_option('-f',
5466 '--force',
5467 action='store_true',
5468 dest='force',
5469 help="force yes to questions (don't prompt)")
5470 parser.add_option(
5471 '--parallel',
5472 action='store_true',
5473 help='Run all tests specified by input_api.RunTests in all '
5474 'PRESUBMIT files in parallel.')
5475 parser.add_option('--resultdb',
5476 action='store_true',
5477 help='Run presubmit checks in the ResultSink environment '
5478 'and send results to the ResultDB database.')
5479 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
5480 (options, args) = parser.parse_args(args)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005481
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005482 cl = Changelist()
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005483
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005484 if not cl.GetIssue():
5485 DieWithError('You must upload the change first to Gerrit.\n'
5486 ' If you would rather have `git cl land` upload '
5487 'automatically for you, see http://crbug.com/642759')
5488 return cl.CMDLand(options.force, options.bypass_hooks, options.verbose,
5489 options.parallel, options.resultdb, options.realm)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005490
5491
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005492@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005493@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005494def CMDpatch(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005495 """Applies (cherry-picks) a Gerrit changelist locally."""
5496 parser.add_option('-b',
5497 dest='newbranch',
5498 help='create a new branch off trunk for the patch')
5499 parser.add_option('-f',
5500 '--force',
5501 action='store_true',
5502 help='overwrite state on the current or chosen branch')
5503 parser.add_option('-n',
5504 '--no-commit',
5505 action='store_true',
5506 dest='nocommit',
5507 help='don\'t commit after patch applies.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005508
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005509 group = optparse.OptionGroup(
5510 parser,
5511 'Options for continuing work on the current issue uploaded from a '
5512 'different clone (e.g. different machine). Must be used independently '
5513 'from the other options. No issue number should be specified, and the '
5514 'branch must have an issue number associated with it')
5515 group.add_option('--reapply',
5516 action='store_true',
5517 dest='reapply',
5518 help='Reset the branch and reapply the issue.\n'
5519 'CAUTION: This will undo any local changes in this '
5520 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005521
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005522 group.add_option('--pull',
5523 action='store_true',
5524 dest='pull',
5525 help='Performs a pull before reapplying.')
5526 parser.add_option_group(group)
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005527
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005528 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005529
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005530 if options.reapply:
5531 if options.newbranch:
5532 parser.error('--reapply works on the current branch only.')
5533 if len(args) > 0:
5534 parser.error('--reapply implies no additional arguments.')
5535
5536 cl = Changelist()
5537 if not cl.GetIssue():
5538 parser.error('Current branch must have an associated issue.')
5539
5540 upstream = cl.GetUpstreamBranch()
5541 if upstream is None:
5542 parser.error('No upstream branch specified. Cannot reset branch.')
5543
5544 RunGit(['reset', '--hard', upstream])
5545 if options.pull:
5546 RunGit(['pull'])
5547
5548 target_issue_arg = ParseIssueNumberArgument(cl.GetIssue())
5549 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit,
5550 options.force, False)
5551
5552 if len(args) != 1 or not args[0]:
5553 parser.error('Must specify issue number or URL.')
5554
5555 target_issue_arg = ParseIssueNumberArgument(args[0])
5556 if not target_issue_arg.valid:
5557 parser.error('Invalid issue ID or URL.')
5558
5559 # We don't want uncommitted changes mixed up with the patch.
5560 if git_common.is_dirty_git_tree('patch'):
5561 return 1
5562
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005563 if options.newbranch:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005564 if options.force:
5565 RunGit(['branch', '-D', options.newbranch],
5566 stderr=subprocess2.PIPE,
5567 error_ok=True)
5568 git_new_branch.create_new_branch(options.newbranch)
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005569
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005570 cl = Changelist(codereview_host=target_issue_arg.hostname,
5571 issue=target_issue_arg.issue)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005572
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005573 if not args[0].isdigit():
5574 print('canonical issue/change URL: %s\n' % cl.GetIssueURL())
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005575
Joanna Wang44e9bee2023-01-25 21:51:42 +00005576 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit,
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005577 options.force, options.newbranch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005578
5579
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005580def GetTreeStatus(url=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005581 """Fetches the tree status and returns either 'open', 'closed',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005582 'unknown' or 'unset'."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005583 url = url or settings.GetTreeStatusUrl(error_ok=True)
5584 if url:
5585 status = str(urllib.request.urlopen(url).read().lower())
5586 if status.find('closed') != -1 or status == '0':
5587 return 'closed'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005588
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005589 if status.find('open') != -1 or status == '1':
5590 return 'open'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005591
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005592 return 'unknown'
5593 return 'unset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005594
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005595
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005596def GetTreeStatusReason():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005597 """Fetches the tree status from a json url and returns the message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005598 with the reason for the tree to be opened or closed."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005599 url = settings.GetTreeStatusUrl()
5600 json_url = urllib.parse.urljoin(url, '/current?format=json')
5601 connection = urllib.request.urlopen(json_url)
5602 status = json.loads(connection.read())
5603 connection.close()
5604 return status['message']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005605
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005606
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005607@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005608def CMDtree(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005609 """Shows the status of the tree."""
5610 _, args = parser.parse_args(args)
5611 status = GetTreeStatus()
5612 if 'unset' == status:
5613 print(
5614 'You must configure your tree status URL by running "git cl config".'
5615 )
5616 return 2
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005617
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005618 print('The tree is %s' % status)
5619 print()
5620 print(GetTreeStatusReason())
5621 if status != 'open':
5622 return 1
5623 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005624
5625
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005626@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005627def CMDtry(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005628 """Triggers tryjobs using either Buildbucket or CQ dry run."""
5629 group = optparse.OptionGroup(parser, 'Tryjob options')
5630 group.add_option(
5631 '-b',
5632 '--bot',
5633 action='append',
5634 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5635 'times to specify multiple builders. ex: '
5636 '"-b win_rel -b win_layout". See '
5637 'the try server waterfall for the builders name and the tests '
5638 'available.'))
5639 group.add_option(
5640 '-B',
5641 '--bucket',
5642 default='',
5643 help=('Buildbucket bucket to send the try requests. Format: '
5644 '"luci.$LUCI_PROJECT.$LUCI_BUCKET". eg: "luci.chromium.try"'))
5645 group.add_option(
5646 '-r',
5647 '--revision',
5648 help='Revision to use for the tryjob; default: the revision will '
5649 'be determined by the try recipe that builder runs, which usually '
5650 'defaults to HEAD of origin/master or origin/main')
5651 group.add_option(
5652 '-c',
5653 '--clobber',
5654 action='store_true',
5655 default=False,
5656 help='Force a clobber before building; that is don\'t do an '
5657 'incremental build')
5658 group.add_option('--category',
5659 default='git_cl_try',
5660 help='Specify custom build category.')
5661 group.add_option(
5662 '--project',
5663 help='Override which project to use. Projects are defined '
5664 'in recipe to determine to which repository or directory to '
5665 'apply the patch')
5666 group.add_option(
5667 '-p',
5668 '--property',
5669 dest='properties',
5670 action='append',
5671 default=[],
5672 help='Specify generic properties in the form -p key1=value1 -p '
5673 'key2=value2 etc. The value will be treated as '
5674 'json if decodable, or as string otherwise. '
5675 'NOTE: using this may make your tryjob not usable for CQ, '
5676 'which will then schedule another tryjob with default properties')
5677 group.add_option('--buildbucket-host',
5678 default='cr-buildbucket.appspot.com',
5679 help='Host of buildbucket. The default host is %default.')
5680 parser.add_option_group(group)
5681 parser.add_option('-R',
5682 '--retry-failed',
5683 action='store_true',
5684 default=False,
5685 help='Retry failed jobs from the latest set of tryjobs. '
5686 'Not allowed with --bucket and --bot options.')
5687 parser.add_option(
5688 '-i',
5689 '--issue',
5690 type=int,
5691 help='Operate on this issue instead of the current branch\'s implicit '
5692 'issue.')
5693 options, args = parser.parse_args(args)
maruel@chromium.org15192402012-09-06 12:38:29 +00005694
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005695 # Make sure that all properties are prop=value pairs.
5696 bad_params = [x for x in options.properties if '=' not in x]
5697 if bad_params:
5698 parser.error('Got properties with missing "=": %s' % bad_params)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005699
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005700 if args:
5701 parser.error('Unknown arguments: %s' % args)
maruel@chromium.org15192402012-09-06 12:38:29 +00005702
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005703 cl = Changelist(issue=options.issue)
5704 if not cl.GetIssue():
5705 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005706
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005707 # HACK: warm up Gerrit change detail cache to save on RPCs.
5708 cl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005709
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005710 error_message = cl.CannotTriggerTryJobReason()
5711 if error_message:
5712 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005713
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005714 if options.bot:
5715 if options.retry_failed:
5716 parser.error('--bot is not compatible with --retry-failed.')
5717 if not options.bucket:
5718 parser.error('A bucket (e.g. "chromium/try") is required.')
Edward Lemur45768512020-03-02 19:03:14 +00005719
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005720 triggered = [b for b in options.bot if 'triggered' in b]
5721 if triggered:
5722 parser.error(
5723 'Cannot schedule builds on triggered bots: %s.\n'
5724 'This type of bot requires an initial job from a parent (usually a '
5725 'builder). Schedule a job on the parent instead.\n' % triggered)
Edward Lemur45768512020-03-02 19:03:14 +00005726
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005727 if options.bucket.startswith('.master'):
5728 parser.error('Buildbot masters are not supported.')
Edward Lemur45768512020-03-02 19:03:14 +00005729
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005730 project, bucket = _parse_bucket(options.bucket)
5731 if project is None or bucket is None:
5732 parser.error('Invalid bucket: %s.' % options.bucket)
5733 jobs = sorted((project, bucket, bot) for bot in options.bot)
5734 elif options.retry_failed:
5735 print('Searching for failed tryjobs...')
5736 builds, patchset = _fetch_latest_builds(cl, DEFAULT_BUILDBUCKET_HOST)
5737 if options.verbose:
5738 print('Got %d builds in patchset #%d' % (len(builds), patchset))
5739 jobs = _filter_failed_for_retry(builds)
5740 if not jobs:
5741 print('There are no failed jobs in the latest set of jobs '
5742 '(patchset #%d), doing nothing.' % patchset)
5743 return 0
5744 num_builders = len(jobs)
5745 if num_builders > 10:
5746 confirm_or_exit('There are %d builders with failed builds.' %
5747 num_builders,
5748 action='continue')
5749 else:
5750 if options.verbose:
5751 print('git cl try with no bots now defaults to CQ dry run.')
5752 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5753 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005754
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005755 patchset = cl.GetMostRecentPatchset()
5756 try:
5757 _trigger_tryjobs(cl, jobs, options, patchset)
5758 except BuildbucketResponseException as ex:
5759 print('ERROR: %s' % ex)
5760 return 1
5761 return 0
maruel@chromium.org15192402012-09-06 12:38:29 +00005762
5763
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005764@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005765def CMDtry_results(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005766 """Prints info about results for tryjobs associated with the current CL."""
5767 group = optparse.OptionGroup(parser, 'Tryjob results options')
5768 group.add_option('-p',
5769 '--patchset',
5770 type=int,
5771 help='patchset number if not current.')
5772 group.add_option('--print-master',
5773 action='store_true',
5774 help='print master name as well.')
5775 group.add_option('--color',
5776 action='store_true',
5777 default=setup_color.IS_TTY,
5778 help='force color output, useful when piping output.')
5779 group.add_option('--buildbucket-host',
5780 default='cr-buildbucket.appspot.com',
5781 help='Host of buildbucket. The default host is %default.')
5782 group.add_option(
5783 '--json',
5784 help=('Path of JSON output file to write tryjob results to,'
5785 'or "-" for stdout.'))
5786 parser.add_option_group(group)
5787 parser.add_option(
5788 '-i',
5789 '--issue',
5790 type=int,
5791 help='Operate on this issue instead of the current branch\'s implicit '
5792 'issue.')
5793 options, args = parser.parse_args(args)
5794 if args:
5795 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005796
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005797 cl = Changelist(issue=options.issue)
5798 if not cl.GetIssue():
5799 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005800
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005801 patchset = options.patchset
tandrii221ab252016-10-06 08:12:04 -07005802 if not patchset:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005803 patchset = cl.GetMostRecentDryRunPatchset()
5804 if not patchset:
5805 parser.error('Code review host doesn\'t know about issue %s. '
5806 'No access to issue or wrong issue number?\n'
5807 'Either upload first, or pass --patchset explicitly.' %
5808 cl.GetIssue())
tandrii221ab252016-10-06 08:12:04 -07005809
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005810 try:
5811 jobs = _fetch_tryjobs(cl, DEFAULT_BUILDBUCKET_HOST, patchset)
5812 except BuildbucketResponseException as ex:
5813 print('Buildbucket error: %s' % ex)
5814 return 1
5815 if options.json:
5816 write_json(options.json, jobs)
5817 else:
5818 _print_tryjobs(options, jobs)
5819 return 0
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005820
5821
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005822@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005823@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005824def CMDupstream(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005825 """Prints or sets the name of the upstream branch, if any."""
5826 _, args = parser.parse_args(args)
5827 if len(args) > 1:
5828 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005829
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005830 cl = Changelist()
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005831 if args:
5832 # One arg means set upstream branch.
5833 branch = cl.GetBranch()
5834 RunGit(['branch', '--set-upstream-to', args[0], branch])
5835 cl = Changelist()
5836 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(), ))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005837
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005838 # Clear configured merge-base, if there is one.
5839 git_common.remove_merge_base(branch)
5840 else:
5841 print(cl.GetUpstreamBranch())
5842 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005843
5844
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005845@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005846def CMDweb(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005847 """Opens the current CL in the web browser."""
5848 parser.add_option('-p',
5849 '--print-only',
5850 action='store_true',
5851 dest='print_only',
5852 help='Only print the Gerrit URL, don\'t open it in the '
5853 'browser.')
5854 (options, args) = parser.parse_args(args)
5855 if args:
5856 parser.error('Unrecognized args: %s' % ' '.join(args))
thestig@chromium.org00858c82013-12-02 23:08:03 +00005857
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005858 issue_url = Changelist().GetIssueURL()
5859 if not issue_url:
5860 print('ERROR No issue to open', file=sys.stderr)
5861 return 1
thestig@chromium.org00858c82013-12-02 23:08:03 +00005862
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005863 if options.print_only:
5864 print(issue_url)
5865 return 0
5866
5867 # Redirect I/O before invoking browser to hide its output. For example, this
5868 # allows us to hide the "Created new window in existing browser session."
5869 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
5870 saved_stdout = os.dup(1)
5871 saved_stderr = os.dup(2)
5872 os.close(1)
5873 os.close(2)
5874 os.open(os.devnull, os.O_RDWR)
5875 try:
5876 webbrowser.open(issue_url)
5877 finally:
5878 os.dup2(saved_stdout, 1)
5879 os.dup2(saved_stderr, 2)
Orr Bernstein0b960582022-12-22 20:16:18 +00005880 return 0
5881
thestig@chromium.org00858c82013-12-02 23:08:03 +00005882
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005883@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005884def CMDset_commit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005885 """Sets the commit bit to trigger the CQ."""
5886 parser.add_option('-d',
5887 '--dry-run',
5888 action='store_true',
5889 help='trigger in dry run mode')
5890 parser.add_option('-c',
5891 '--clear',
5892 action='store_true',
5893 help='stop CQ run, if any')
5894 parser.add_option(
5895 '-i',
5896 '--issue',
5897 type=int,
5898 help='Operate on this issue instead of the current branch\'s implicit '
5899 'issue.')
5900 options, args = parser.parse_args(args)
5901 if args:
5902 parser.error('Unrecognized args: %s' % ' '.join(args))
5903 if [options.dry_run, options.clear].count(True) > 1:
5904 parser.error('Only one of --dry-run, and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005905
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005906 cl = Changelist(issue=options.issue)
5907 if not cl.GetIssue():
5908 parser.error('Must upload the issue first.')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005909
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005910 if options.clear:
5911 state = _CQState.NONE
5912 elif options.dry_run:
5913 state = _CQState.DRY_RUN
5914 else:
5915 state = _CQState.COMMIT
5916 cl.SetCQState(state)
5917 return 0
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005918
5919
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005920@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005921def CMDset_close(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005922 """Closes the issue."""
5923 parser.add_option(
5924 '-i',
5925 '--issue',
5926 type=int,
5927 help='Operate on this issue instead of the current branch\'s implicit '
5928 'issue.')
5929 options, args = parser.parse_args(args)
5930 if args:
5931 parser.error('Unrecognized args: %s' % ' '.join(args))
5932 cl = Changelist(issue=options.issue)
5933 # Ensure there actually is an issue to close.
5934 if not cl.GetIssue():
5935 DieWithError('ERROR: No issue to close.')
5936 cl.CloseIssue()
5937 return 0
groby@chromium.org411034a2013-02-26 15:12:01 +00005938
5939
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005940@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005941def CMDdiff(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005942 """Shows differences between local tree and last upload."""
5943 parser.add_option('--stat',
5944 action='store_true',
5945 dest='stat',
5946 help='Generate a diffstat')
5947 options, args = parser.parse_args(args)
5948 if args:
5949 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005950
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005951 cl = Changelist()
5952 issue = cl.GetIssue()
5953 branch = cl.GetBranch()
5954 if not issue:
5955 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005956
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005957 base = cl._GitGetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY)
5958 if not base:
5959 base = cl._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
5960 if not base:
5961 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5962 revision_info = detail['revisions'][detail['current_revision']]
5963 fetch_info = revision_info['fetch']['http']
5964 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5965 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005966
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005967 cmd = ['git', 'diff']
5968 if options.stat:
5969 cmd.append('--stat')
5970 cmd.append(base)
5971 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005972
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005973 return 0
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005974
5975
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005976@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005977def CMDowners(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005978 """Finds potential owners for reviewing."""
5979 parser.add_option(
5980 '--ignore-current',
5981 action='store_true',
5982 help='Ignore the CL\'s current reviewers and start from scratch.')
5983 parser.add_option('--ignore-self',
5984 action='store_true',
5985 help='Do not consider CL\'s author as an owners.')
5986 parser.add_option('--no-color',
5987 action='store_true',
5988 help='Use this option to disable color output')
5989 parser.add_option('--batch',
5990 action='store_true',
5991 help='Do not run interactively, just suggest some')
5992 # TODO: Consider moving this to another command, since other
5993 # git-cl owners commands deal with owners for a given CL.
5994 parser.add_option('--show-all',
5995 action='store_true',
5996 help='Show all owners for a particular file')
5997 options, args = parser.parse_args(args)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005998
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005999 cl = Changelist()
6000 author = cl.GetAuthor()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00006001
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006002 if options.show_all:
6003 if len(args) == 0:
6004 print('No files specified for --show-all. Nothing to do.')
6005 return 0
6006 owners_by_path = cl.owners_client.BatchListOwners(args)
6007 for path in args:
6008 print('Owners for %s:' % path)
6009 print('\n'.join(
6010 ' - %s' % owner
6011 for owner in owners_by_path.get(path, ['No owners found'])))
6012 return 0
Yang Guo6e269a02019-06-26 11:17:02 +00006013
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006014 if args:
6015 if len(args) > 1:
6016 parser.error('Unknown args.')
6017 base_branch = args[0]
6018 else:
6019 # Default to diffing against the common ancestor of the upstream branch.
6020 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00006021
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006022 affected_files = cl.GetAffectedFiles(base_branch)
Dirk Prankebf980882017-09-02 15:08:00 -07006023
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006024 if options.batch:
6025 owners = cl.owners_client.SuggestOwners(affected_files,
6026 exclude=[author])
6027 print('\n'.join(owners))
6028 return 0
Dirk Prankebf980882017-09-02 15:08:00 -07006029
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006030 return owners_finder.OwnersFinder(
6031 affected_files,
6032 author, [] if options.ignore_current else cl.GetReviewers(),
6033 cl.owners_client,
6034 disable_color=options.no_color,
6035 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00006036
6037
Aiden Bennerc08566e2018-10-03 17:52:42 +00006038def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006039 """Generates a diff command."""
6040 # Generate diff for the current branch's changes.
6041 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
Aiden Bennerc08566e2018-10-03 17:52:42 +00006042
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006043 if allow_prefix:
6044 # explicitly setting --src-prefix and --dst-prefix is necessary in the
6045 # case that diff.noprefix is set in the user's git config.
6046 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
6047 else:
6048 diff_cmd += ['--no-prefix']
Aiden Bennerc08566e2018-10-03 17:52:42 +00006049
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006050 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006051
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006052 if args:
6053 for arg in args:
6054 if os.path.isdir(arg) or os.path.isfile(arg):
6055 diff_cmd.append(arg)
6056 else:
6057 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006058
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006059 return diff_cmd
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006060
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006061
Jamie Madill5e96ad12020-01-13 16:08:35 +00006062def _RunClangFormatDiff(opts, clang_diff_files, top_dir, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006063 """Runs clang-format-diff and sets a return value if necessary."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006064 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
6065 # formatted. This is used to block during the presubmit.
6066 return_value = 0
Jamie Madill5e96ad12020-01-13 16:08:35 +00006067
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006068 # Locate the clang-format binary in the checkout
Jamie Madill5e96ad12020-01-13 16:08:35 +00006069 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006070 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
Jamie Madill5e96ad12020-01-13 16:08:35 +00006071 except clang_format.NotFoundError as e:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006072 DieWithError(e)
Jamie Madill5e96ad12020-01-13 16:08:35 +00006073
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006074 if opts.full or settings.GetFormatFullByDefault():
6075 cmd = [clang_format_tool]
6076 if not opts.dry_run and not opts.diff:
6077 cmd.append('-i')
6078 if opts.dry_run:
6079 for diff_file in clang_diff_files:
6080 with open(diff_file, 'r') as myfile:
6081 code = myfile.read().replace('\r\n', '\n')
6082 stdout = RunCommand(cmd + [diff_file], cwd=top_dir)
6083 stdout = stdout.replace('\r\n', '\n')
6084 if opts.diff:
6085 sys.stdout.write(stdout)
6086 if code != stdout:
6087 return_value = 2
6088 else:
6089 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
6090 if opts.diff:
6091 sys.stdout.write(stdout)
6092 else:
6093 try:
6094 script = clang_format.FindClangFormatScriptInChromiumTree(
6095 'clang-format-diff.py')
6096 except clang_format.NotFoundError as e:
6097 DieWithError(e)
Jamie Madill5e96ad12020-01-13 16:08:35 +00006098
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006099 cmd = ['vpython3', script, '-p0']
6100 if not opts.dry_run and not opts.diff:
6101 cmd.append('-i')
Jamie Madill5e96ad12020-01-13 16:08:35 +00006102
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006103 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
6104 diff_output = RunGit(diff_cmd).encode('utf-8')
Jamie Madill5e96ad12020-01-13 16:08:35 +00006105
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006106 env = os.environ.copy()
6107 env['PATH'] = (str(os.path.dirname(clang_format_tool)) + os.pathsep +
6108 env['PATH'])
6109 stdout = RunCommand(cmd,
6110 stdin=diff_output,
6111 cwd=top_dir,
6112 env=env,
6113 shell=sys.platform.startswith('win32'))
6114 if opts.diff:
6115 sys.stdout.write(stdout)
6116 if opts.dry_run and len(stdout) > 0:
6117 return_value = 2
6118
6119 return return_value
Jamie Madill5e96ad12020-01-13 16:08:35 +00006120
6121
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006122def _FindGoogleJavaFormat():
6123 primary_solution_path = gclient_paths.GetPrimarySolutionPath()
6124 if primary_solution_path:
6125 path = os.path.join(primary_solution_path, 'third_party',
6126 'google-java-format', 'google-java-format')
6127 if os.path.exists(path):
6128 return path
6129
6130 return shutil.which('google-java-format')
6131
6132
6133def _RunGoogleJavaFormat(opts, paths, top_dir, upstream_commit):
6134 """Runs google-java-format and sets a return value if necessary."""
6135 google_java_format = _FindGoogleJavaFormat()
6136 if google_java_format is None:
6137 DieWithError('Could not find google-java-format.')
6138
6139 base_cmd = [google_java_format, '--aosp']
6140 if opts.dry_run or opts.diff:
6141 base_cmd += ['--dry-run']
6142 else:
6143 base_cmd += ['--replace']
6144
6145 changed_lines_only = not (opts.full or settings.GetFormatFullByDefault())
6146 if changed_lines_only:
6147 line_diffs = _ComputeFormatDiffLineRanges(paths, upstream_commit)
6148
6149 results = []
6150 kwds = {'error_ok': True, 'cwd': top_dir}
6151 with multiprocessing.pool.ThreadPool() as pool:
6152 for path in paths:
6153 cmd = base_cmd.copy()
6154 if changed_lines_only:
6155 ranges = line_diffs.get(path)
6156 if not ranges:
6157 # E.g. There were only deleted lines.
6158 continue
6159 cmd.extend('--lines={}:{}'.format(a, b) for a, b in ranges)
6160
6161 results.append(
6162 pool.apply_async(RunCommand, args=[cmd + [path]], kwds=kwds))
6163
6164 return_value = 0
6165 for result in results:
6166 stdout = result.get()
6167 if stdout:
6168 if opts.diff:
6169 sys.stdout.write('Requires formatting: ' + stdout)
6170 else:
6171 return_value = 2
6172
6173 return return_value
6174
6175
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006176def _RunRustFmt(opts, rust_diff_files, top_dir, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006177 """Runs rustfmt. Just like _RunClangFormatDiff returns 2 to indicate that
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006178 presubmit checks have failed (and returns 0 otherwise)."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006179 # Locate the rustfmt binary.
6180 try:
6181 rustfmt_tool = rustfmt.FindRustfmtToolInChromiumTree()
6182 except rustfmt.NotFoundError as e:
6183 DieWithError(e)
6184
6185 # TODO(crbug.com/1440869): Support formatting only the changed lines
6186 # if `opts.full or settings.GetFormatFullByDefault()` is False.
6187 cmd = [rustfmt_tool]
6188 if opts.dry_run:
6189 cmd.append('--check')
6190 cmd += rust_diff_files
6191 rustfmt_exitcode = subprocess2.call(cmd)
6192
6193 if opts.presubmit and rustfmt_exitcode != 0:
6194 return 2
6195
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006196 return 0
6197
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006198
Olivier Robin0a6b5442022-04-07 07:25:04 +00006199def _RunSwiftFormat(opts, swift_diff_files, top_dir, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006200 """Runs swift-format. Just like _RunClangFormatDiff returns 2 to indicate
Olivier Robin0a6b5442022-04-07 07:25:04 +00006201 that presubmit checks have failed (and returns 0 otherwise)."""
Andrew Grievecca48db2023-09-14 14:12:23 +00006202 if sys.platform != 'darwin':
6203 DieWithError('swift-format is only supported on macOS.')
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006204 # Locate the swift-format binary.
6205 try:
6206 swift_format_tool = swift_format.FindSwiftFormatToolInChromiumTree()
6207 except swift_format.NotFoundError as e:
6208 DieWithError(e)
6209
6210 cmd = [swift_format_tool]
6211 if opts.dry_run:
6212 cmd += ['lint', '-s']
6213 else:
6214 cmd += ['format', '-i']
6215 cmd += swift_diff_files
6216 swift_format_exitcode = subprocess2.call(cmd)
6217
6218 if opts.presubmit and swift_format_exitcode != 0:
6219 return 2
6220
Olivier Robin0a6b5442022-04-07 07:25:04 +00006221 return 0
6222
Olivier Robin0a6b5442022-04-07 07:25:04 +00006223
Andrew Grievecca48db2023-09-14 14:12:23 +00006224def _RunYapf(opts, paths, top_dir, upstream_commit):
6225 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
6226 yapf_tool = os.path.join(depot_tools_path, 'yapf')
6227
6228 # Used for caching.
6229 yapf_configs = {}
6230 for p in paths:
6231 # Find the yapf style config for the current file, defaults to depot
6232 # tools default.
6233 _FindYapfConfigFile(p, yapf_configs, top_dir)
6234
6235 # Turn on python formatting by default if a yapf config is specified.
6236 # This breaks in the case of this repo though since the specified
6237 # style file is also the global default.
6238 if opts.python is None:
6239 paths = [
6240 p for p in paths
6241 if _FindYapfConfigFile(p, yapf_configs, top_dir) is not None
6242 ]
6243
6244 # Note: yapf still seems to fix indentation of the entire file
6245 # even if line ranges are specified.
6246 # See https://github.com/google/yapf/issues/499
6247 if not opts.full and paths:
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006248 line_diffs = _ComputeFormatDiffLineRanges(paths, upstream_commit)
Andrew Grievecca48db2023-09-14 14:12:23 +00006249
6250 yapfignore_patterns = _GetYapfIgnorePatterns(top_dir)
6251 paths = _FilterYapfIgnoredFiles(paths, yapfignore_patterns)
6252
6253 return_value = 0
6254 for path in paths:
6255 yapf_style = _FindYapfConfigFile(path, yapf_configs, top_dir)
6256 # Default to pep8 if not .style.yapf is found.
6257 if not yapf_style:
6258 yapf_style = 'pep8'
6259
6260 with open(path, 'r') as py_f:
6261 if 'python2' in py_f.readline():
6262 vpython_script = 'vpython'
6263 else:
6264 vpython_script = 'vpython3'
6265
6266 cmd = [vpython_script, yapf_tool, '--style', yapf_style, path]
6267
Andrew Grievecca48db2023-09-14 14:12:23 +00006268 if not opts.full:
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006269 ranges = line_diffs.get(path)
6270 if not ranges:
Andrew Grievecca48db2023-09-14 14:12:23 +00006271 continue
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006272 # Only run yapf over changed line ranges.
6273 for diff_start, diff_end in ranges:
6274 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
Andrew Grievecca48db2023-09-14 14:12:23 +00006275
6276 if opts.diff or opts.dry_run:
6277 cmd += ['--diff']
6278 # Will return non-zero exit code if non-empty diff.
6279 stdout = RunCommand(cmd,
6280 error_ok=True,
6281 stderr=subprocess2.PIPE,
6282 cwd=top_dir,
6283 shell=sys.platform.startswith('win32'))
6284 if opts.diff:
6285 sys.stdout.write(stdout)
6286 elif len(stdout) > 0:
6287 return_value = 2
6288 else:
6289 cmd += ['-i']
6290 RunCommand(cmd, cwd=top_dir, shell=sys.platform.startswith('win32'))
6291 return return_value
6292
6293
6294def _RunGnFormat(opts, paths, top_dir, upstream_commit):
6295 cmd = ['gn', 'format']
6296 if opts.dry_run or opts.diff:
6297 cmd.append('--dry-run')
6298 return_value = 0
6299 for path in paths:
6300 gn_ret = subprocess2.call(cmd + [path],
6301 shell=sys.platform.startswith('win'),
6302 cwd=top_dir)
6303 if opts.dry_run and gn_ret == 2:
6304 return_value = 2 # Not formatted.
6305 elif opts.diff and gn_ret == 2:
6306 # TODO this should compute and print the actual diff.
6307 print('This change has GN build file diff for ' + path)
6308 elif gn_ret != 0:
6309 # For non-dry run cases (and non-2 return values for dry-run), a
6310 # nonzero error code indicates a failure, probably because the
6311 # file doesn't parse.
6312 DieWithError('gn format failed on ' + path +
6313 '\nTry running `gn format` on this file manually.')
6314 return return_value
6315
6316
6317def _FormatXml(opts, paths, top_dir, upstream_commit):
6318 # Skip the metrics formatting from the global presubmit hook. These files
6319 # have a separate presubmit hook that issues an error if the files need
6320 # formatting, whereas the top-level presubmit script merely issues a
6321 # warning. Formatting these files is somewhat slow, so it's important not to
6322 # duplicate the work.
6323 if opts.presubmit:
6324 return 0
6325
6326 return_value = 0
6327 for path in paths:
6328 xml_dir = GetMetricsDir(path)
6329 if not xml_dir:
6330 continue
6331
6332 tool_dir = os.path.join(top_dir, xml_dir)
6333 pretty_print_tool = os.path.join(tool_dir, 'pretty_print.py')
6334 cmd = [shutil.which('vpython3'), pretty_print_tool, '--non-interactive']
6335
6336 # If the XML file is histograms.xml or enums.xml, add the xml path
6337 # to the command as histograms/pretty_print.py now needs a relative
6338 # path argument after splitting the histograms into multiple
6339 # directories. For example, in tools/metrics/ukm, pretty-print could
6340 # be run using: $ python pretty_print.py But in
6341 # tools/metrics/histogrmas, pretty-print should be run with an
6342 # additional relative path argument, like: $ python pretty_print.py
6343 # metadata/UMA/histograms.xml $ python pretty_print.py enums.xml
6344 if xml_dir == os.path.join('tools', 'metrics', 'histograms'):
6345 if os.path.basename(path) not in ('histograms.xml', 'enums.xml',
6346 'histogram_suffixes_list.xml'):
6347 # Skip this XML file if it's not one of the known types.
6348 continue
6349 cmd.append(path)
6350
6351 if opts.dry_run or opts.diff:
6352 cmd.append('--diff')
6353
6354 stdout = RunCommand(cmd, cwd=top_dir)
6355 if opts.diff:
6356 sys.stdout.write(stdout)
6357 if opts.dry_run and stdout:
6358 return_value = 2 # Not formatted.
6359 return return_value
6360
6361
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006362def MatchingFileType(file_name, extensions):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006363 """Returns True if the file name ends with one of the given extensions."""
6364 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006365
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006366
enne@chromium.org555cfe42014-01-29 18:21:39 +00006367@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006368@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006369def CMDformat(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006370 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Andrew Grievecca48db2023-09-14 14:12:23 +00006371 clang_exts = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006372 GN_EXTS = ['.gn', '.gni', '.typemap']
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006373 parser.add_option('--full',
6374 action='store_true',
6375 help='Reformat the full content of all touched files')
6376 parser.add_option('--upstream', help='Branch to check against')
6377 parser.add_option('--dry-run',
6378 action='store_true',
6379 help='Don\'t modify any file on disk.')
6380 parser.add_option(
6381 '--no-clang-format',
6382 dest='clang_format',
6383 action='store_false',
6384 default=True,
6385 help='Disables formatting of various file types using clang-format.')
6386 parser.add_option('--python',
6387 action='store_true',
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006388 help='Enables python formatting on all python files.')
6389 parser.add_option(
6390 '--no-python',
Andrew Grievecca48db2023-09-14 14:12:23 +00006391 action='store_false',
6392 dest='python',
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006393 help='Disables python formatting on all python files. '
6394 'If neither --python or --no-python are set, python files that have a '
6395 '.style.yapf file in an ancestor directory will be formatted. '
6396 'It is an error to set both.')
6397 parser.add_option('--js',
6398 action='store_true',
6399 help='Format javascript code with clang-format. '
6400 'Has no effect if --no-clang-format is set.')
6401 parser.add_option('--diff',
6402 action='store_true',
6403 help='Print diff to stdout rather than modifying files.')
6404 parser.add_option('--presubmit',
6405 action='store_true',
6406 help='Used when running the script from a presubmit.')
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006407
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006408 parser.add_option(
6409 '--rust-fmt',
6410 dest='use_rust_fmt',
6411 action='store_true',
6412 default=rustfmt.IsRustfmtSupported(),
6413 help='Enables formatting of Rust file types using rustfmt.')
6414 parser.add_option(
6415 '--no-rust-fmt',
6416 dest='use_rust_fmt',
6417 action='store_false',
6418 help='Disables formatting of Rust file types using rustfmt.')
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006419
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006420 parser.add_option(
6421 '--swift-format',
6422 dest='use_swift_format',
6423 action='store_true',
6424 default=swift_format.IsSwiftFormatSupported(),
6425 help='Enables formatting of Swift file types using swift-format '
6426 '(macOS host only).')
6427 parser.add_option(
6428 '--no-swift-format',
6429 dest='use_swift_format',
6430 action='store_false',
6431 help='Disables formatting of Swift file types using swift-format.')
Olivier Robin0a6b5442022-04-07 07:25:04 +00006432
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006433 # Temporary flag to test with google-java-format.
6434 parser.add_option('--google-java-format',
6435 action='store_true',
6436 help=optparse.SUPPRESS_HELP)
6437
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006438 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006439
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006440 # Normalize any remaining args against the current path, so paths relative
6441 # to the current directory are still resolved as expected.
6442 args = [os.path.join(os.getcwd(), arg) for arg in args]
Daniel Chengc55eecf2016-12-30 03:11:02 -08006443
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006444 # git diff generates paths against the root of the repository. Change
6445 # to that directory so clang-format can find files even within subdirs.
6446 rel_base_path = settings.GetRelativeRoot()
6447 if rel_base_path:
6448 os.chdir(rel_base_path)
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00006449
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006450 # Grab the merge-base commit, i.e. the upstream commit of the current
6451 # branch when it was created or the last time it was rebased. This is
6452 # to cover the case where the user may have called "git fetch origin",
6453 # moving the origin branch to a newer commit, but hasn't rebased yet.
6454 upstream_commit = None
6455 upstream_branch = opts.upstream
6456 if not upstream_branch:
6457 cl = Changelist()
6458 upstream_branch = cl.GetUpstreamBranch()
6459 if upstream_branch:
6460 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
6461 upstream_commit = upstream_commit.strip()
digit@chromium.org29e47272013-05-17 17:01:46 +00006462
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006463 if not upstream_commit:
6464 DieWithError('Could not find base commit for this branch. '
6465 'Are you in detached state?')
digit@chromium.org29e47272013-05-17 17:01:46 +00006466
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006467 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
6468 diff_output = RunGit(changed_files_cmd)
6469 diff_files = diff_output.splitlines()
6470 # Filter out files deleted by this CL
6471 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006472
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006473 if opts.js:
Andrew Grievecca48db2023-09-14 14:12:23 +00006474 clang_exts.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11006475
Andrew Grievecca48db2023-09-14 14:12:23 +00006476 formatters = [
6477 (GN_EXTS, _RunGnFormat),
6478 (['.xml'], _FormatXml),
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00006479 ]
Andrew Grieved7ba85d2023-09-15 18:28:33 +00006480 if opts.google_java_format:
6481 clang_exts.remove('.java')
6482 formatters += [(['.java'], _RunGoogleJavaFormat)]
Andrew Grievecca48db2023-09-14 14:12:23 +00006483 if opts.clang_format:
6484 formatters += [(clang_exts, _RunClangFormatDiff)]
6485 if opts.use_rust_fmt:
6486 formatters += [(['.rs'], _RunRustFmt)]
6487 if opts.use_swift_format:
6488 formatters += [(['.swift'], _RunSwiftFormat)]
6489 if opts.python is not False:
6490 formatters += [(['.py'], _RunYapf)]
digit@chromium.org29e47272013-05-17 17:01:46 +00006491
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006492 top_dir = settings.GetRoot()
Andrew Grievecca48db2023-09-14 14:12:23 +00006493 return_value = 0
6494 for file_types, format_func in formatters:
6495 paths = [p for p in diff_files if MatchingFileType(p, file_types)]
6496 if not paths:
6497 continue
6498 ret = format_func(opts, paths, top_dir, upstream_commit)
6499 return_value = return_value or ret
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006500
6501 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006502
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006503
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006504def GetMetricsDir(diff_xml):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006505 metrics_xml_dirs = [
6506 os.path.join('tools', 'metrics', 'actions'),
6507 os.path.join('tools', 'metrics', 'histograms'),
6508 os.path.join('tools', 'metrics', 'structured'),
6509 os.path.join('tools', 'metrics', 'ukm'),
6510 ]
6511 for xml_dir in metrics_xml_dirs:
6512 if diff_xml.startswith(xml_dir):
6513 return xml_dir
6514 return None
Steven Holte2e664bf2017-04-21 13:10:47 -07006515
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006516
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006517@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006518@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006519def CMDcheckout(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006520 """Checks out a branch associated with a given Gerrit issue."""
6521 _, args = parser.parse_args(args)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006522
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006523 if len(args) != 1:
6524 parser.print_help()
6525 return 1
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006526
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006527 issue_arg = ParseIssueNumberArgument(args[0])
6528 if not issue_arg.valid:
6529 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006530
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006531 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006532
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006533 output = RunGit([
6534 'config', '--local', '--get-regexp', r'branch\..*\.' + ISSUE_CONFIG_KEY
6535 ],
6536 error_ok=True)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006537
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006538 branches = []
6539 for key, issue in [x.split() for x in output.splitlines()]:
6540 if issue == target_issue:
6541 branches.append(
6542 re.sub(r'branch\.(.*)\.' + ISSUE_CONFIG_KEY, r'\1', key))
Edward Lemur52969c92020-02-06 18:15:28 +00006543
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006544 if len(branches) == 0:
6545 print('No branch found for issue %s.' % target_issue)
6546 return 1
6547 if len(branches) == 1:
6548 RunGit(['checkout', branches[0]])
6549 else:
6550 print('Multiple branches match issue %s:' % target_issue)
6551 for i in range(len(branches)):
6552 print('%d: %s' % (i, branches[i]))
6553 which = gclient_utils.AskForData('Choose by index: ')
6554 try:
6555 RunGit(['checkout', branches[int(which)]])
6556 except (IndexError, ValueError):
6557 print('Invalid selection, not checking out any branch.')
6558 return 1
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006559
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006560 return 0
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006561
6562
maruel@chromium.org29404b52014-09-08 22:58:00 +00006563def CMDlol(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006564 # This command is intentionally undocumented.
6565 print(
6566 zlib.decompress(
6567 base64.b64decode(
6568 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6569 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6570 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
6571 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')).decode('utf-8'))
6572 return 0
maruel@chromium.org29404b52014-09-08 22:58:00 +00006573
6574
Josip Sokcevic0399e172022-03-21 23:11:51 +00006575def CMDversion(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006576 import utils
6577 print(utils.depot_tools_version())
Josip Sokcevic0399e172022-03-21 23:11:51 +00006578
6579
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006580class OptionParser(optparse.OptionParser):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006581 """Creates the option parse and add --verbose support."""
6582 def __init__(self, *args, **kwargs):
6583 optparse.OptionParser.__init__(self,
6584 *args,
6585 prog='git cl',
6586 version=__version__,
6587 **kwargs)
6588 self.add_option('-v',
6589 '--verbose',
6590 action='count',
6591 default=0,
6592 help='Use 2 times for more debugging info')
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00006593
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006594 def parse_args(self, args=None, _values=None):
Joanna Wangc5b38322023-03-15 20:38:46 +00006595 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006596 return self._parse_args(args)
6597 finally:
6598 # Regardless of success or failure of args parsing, we want to
6599 # report metrics, but only after logging has been initialized (if
6600 # parsing succeeded).
6601 global settings
6602 settings = Settings()
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00006603
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006604 if metrics.collector.config.should_collect_metrics:
6605 try:
6606 # GetViewVCUrl ultimately calls logging method.
6607 project_url = settings.GetViewVCUrl().strip('/+')
6608 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
6609 metrics.collector.add('project_urls', [project_url])
6610 except subprocess2.CalledProcessError:
6611 # Occurs when command is not executed in a git repository
6612 # We should not fail here. If the command needs to be
6613 # executed in a repo, it will be raised later.
6614 pass
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006615
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006616 def _parse_args(self, args=None):
6617 # Create an optparse.Values object that will store only the actual
6618 # passed options, without the defaults.
6619 actual_options = optparse.Values()
6620 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
6621 # Create an optparse.Values object with the default options.
6622 options = optparse.Values(self.get_default_values().__dict__)
6623 # Update it with the options passed by the user.
6624 options._update_careful(actual_options.__dict__)
6625 # Store the options passed by the user in an _actual_options attribute.
6626 # We store only the keys, and not the values, since the values can
6627 # contain arbitrary information, which might be PII.
6628 metrics.collector.add('arguments', list(actual_options.__dict__.keys()))
Edward Lemur83bd7f42018-10-10 00:14:21 +00006629
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006630 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
6631 logging.basicConfig(
6632 level=levels[min(options.verbose,
6633 len(levels) - 1)],
6634 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6635 '%(filename)s] %(message)s')
6636
6637 return options, args
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006638
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006639
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006640def main(argv):
Gavin Mak7f5b53f2023-09-07 18:13:01 +00006641 if sys.version_info[0] < 3:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006642 print('\nYour Python version %s is unsupported, please upgrade.\n' %
6643 (sys.version.split(' ', 1)[0], ),
6644 file=sys.stderr)
6645 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006646
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006647 colorize_CMDstatus_doc()
6648 dispatcher = subcommand.CommandDispatcher(__name__)
6649 try:
6650 return dispatcher.execute(OptionParser(), argv)
6651 except auth.LoginRequiredError as e:
6652 DieWithError(str(e))
6653 except urllib.error.HTTPError as e:
6654 if e.code != 500:
6655 raise
6656 DieWithError((
6657 'App Engine is misbehaving and returned HTTP %d, again. Keep faith '
6658 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
6659 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006660
6661
6662if __name__ == '__main__':
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006663 # These affect sys.stdout, so do it outside of main() to simplify mocks in
6664 # the unit tests.
6665 fix_encoding.fix_encoding()
6666 setup_color.init()
6667 with metrics.collector.print_notice_and_exit():
6668 sys.exit(main(sys.argv[1:]))