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