blob: c4789df95a406437203ca1ad51ed515f6d89a8ee [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
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000044import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000045import gerrit_util
iannucci@chromium.org9e849272014-04-04 00:31:55 +000046import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000047import git_footers
Edward Lemur85153282020-02-14 22:06:29 +000048import git_new_branch
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000049import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000050import metrics_utils
Edward Lesmeseeca9c62020-11-20 00:00:17 +000051import owners_client
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import owners_finder
Lei Zhangb8c62cf2020-07-15 20:09:37 +000053import presubmit_canned_checks
Josip Sokcevic7958e302023-03-01 23:02:21 +000054import presubmit_support
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +000055import rustfmt
Josip Sokcevic7958e302023-03-01 23:02:21 +000056import scm
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000057import setup_color
Francois Dorayd42c6812017-05-30 15:10:20 -040058import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
Olivier Robin0a6b5442022-04-07 07:25:04 +000061import swift_format
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import watchlists
63
Edward Lemur79d4f992019-11-11 23:49:02 +000064
tandrii7400cf02016-06-21 08:48:07 -070065__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000066
Mike Frysinger124bb8e2023-09-06 05:48:55 +000067# TODO: Should fix these warnings.
68# pylint: disable=line-too-long
69
Edward Lemur0f58ae42019-04-30 17:24:12 +000070# Traces for git push will be stored in a traces directory inside the
71# depot_tools checkout.
72DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
73TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
Edward Lemur227d5102020-02-25 23:45:35 +000074PRESUBMIT_SUPPORT = os.path.join(DEPOT_TOOLS, 'presubmit_support.py')
Edward Lemur0f58ae42019-04-30 17:24:12 +000075
76# When collecting traces, Git hashes will be reduced to 6 characters to reduce
77# the size after compression.
78GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
79# Used to redact the cookies from the gitcookies file.
80GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
81
Edward Lemurd4d1ba42019-09-20 21:46:37 +000082MAX_ATTEMPTS = 3
83
Edward Lemur1b52d872019-05-09 21:12:12 +000084# The maximum number of traces we will keep. Multiplied by 3 since we store
85# 3 files per trace.
86MAX_TRACES = 3 * 10
Edward Lemur5737f022019-05-17 01:24:00 +000087# Message to be displayed to the user to inform where to find the traces for a
88# git-cl upload execution.
Edward Lemur0f58ae42019-04-30 17:24:12 +000089TRACES_MESSAGE = (
Mike Frysinger124bb8e2023-09-06 05:48:55 +000090 '\n'
91 'The traces of this git-cl execution have been recorded at:\n'
92 ' %(trace_name)s-traces.zip\n'
93 'Copies of your gitcookies file and git config have been recorded at:\n'
94 ' %(trace_name)s-git-info.zip\n')
Edward Lemur1b52d872019-05-09 21:12:12 +000095# Format of the message to be stored as part of the traces to give developers a
96# better context when they go through traces.
Mike Frysinger124bb8e2023-09-06 05:48:55 +000097TRACES_README_FORMAT = ('Date: %(now)s\n'
98 '\n'
99 'Change: https://%(gerrit_host)s/q/%(change_id)s\n'
100 'Title: %(title)s\n'
101 '\n'
102 '%(description)s\n'
103 '\n'
104 'Execution time: %(execution_time)s\n'
105 'Exit code: %(exit_code)s\n') + TRACES_MESSAGE
Edward Lemur0f58ae42019-04-30 17:24:12 +0000106
Aaron Gable1bc7bfe2016-12-19 10:08:14 -0800107POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
Henrique Ferreiroff249622019-11-28 23:19:29 +0000108DESCRIPTION_BACKUP_FILE = '.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +0000109REFS_THAT_ALIAS_TO_OTHER_REFS = {
Josip Sokcevic7e133ff2021-07-13 17:44:53 +0000110 'refs/remotes/origin/lkgr': 'refs/remotes/origin/main',
111 'refs/remotes/origin/lkcr': 'refs/remotes/origin/main',
rmistry@google.comc68112d2015-03-03 12:48:06 +0000112}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
Josip Sokcevicc39ab992020-09-24 20:09:15 +0000114DEFAULT_OLD_BRANCH = 'refs/remotes/origin/master'
115DEFAULT_NEW_BRANCH = 'refs/remotes/origin/main'
116
Joanna Wanga8db0cb2023-01-24 15:43:17 +0000117DEFAULT_BUILDBUCKET_HOST = 'cr-buildbucket.appspot.com'
118
thestig@chromium.org44202a22014-03-11 19:22:18 +0000119# Valid extensions for files we want to lint.
120DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
121DEFAULT_LINT_IGNORE_REGEX = r"$^"
122
Aiden Bennerc08566e2018-10-03 17:52:42 +0000123# File name for yapf style config files.
124YAPF_CONFIG_FILENAME = '.style.yapf'
125
Edward Lesmes50da7702020-03-30 19:23:43 +0000126# The issue, patchset and codereview server are stored on git config for each
127# branch under branch.<branch-name>.<config-key>.
128ISSUE_CONFIG_KEY = 'gerritissue'
129PATCHSET_CONFIG_KEY = 'gerritpatchset'
130CODEREVIEW_SERVER_CONFIG_KEY = 'gerritserver'
Gavin Makbe2e9262022-11-08 23:41:55 +0000131# When using squash workflow, _CMDUploadChange doesn't simply push the commit(s)
132# you make to Gerrit. Instead, it creates a new commit object that contains all
133# changes you've made, diffed against a parent/merge base.
134# This is the hash of the new squashed commit and you can find this on Gerrit.
135GERRIT_SQUASH_HASH_CONFIG_KEY = 'gerritsquashhash'
136# This is the latest uploaded local commit hash.
137LAST_UPLOAD_HASH_CONFIG_KEY = 'last-upload-hash'
Edward Lesmes50da7702020-03-30 19:23:43 +0000138
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000139# Shortcut since it quickly becomes repetitive.
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000140Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000141
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000142# Initialized in main()
143settings = None
144
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100145# Used by tests/git_cl_test.py to add extra logging.
146# Inside the weirdly failing test, add this:
147# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700148# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100149_IS_BEING_TESTED = False
150
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000151_GOOGLESOURCE = 'googlesource.com'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000152
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000153_KNOWN_GERRIT_TO_SHORT_URLS = {
154 'https://chrome-internal-review.googlesource.com': 'https://crrev.com/i',
155 'https://chromium-review.googlesource.com': 'https://crrev.com/c',
156}
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000157assert len(_KNOWN_GERRIT_TO_SHORT_URLS) == len(
158 set(_KNOWN_GERRIT_TO_SHORT_URLS.values())), 'must have unique values'
Andrii Shyshkalov1ee78cd2020-03-12 01:31:53 +0000159
Joanna Wang18de1f62023-01-21 01:24:24 +0000160# Maximum number of branches in a stack that can be traversed and uploaded
161# at once. Picked arbitrarily.
162_MAX_STACKED_BRANCHES_UPLOAD = 20
163
Joanna Wang892f2ce2023-03-14 21:39:47 +0000164# Environment variable to indicate if user is participating in the stcked
165# changes dogfood.
166DOGFOOD_STACKED_CHANGES_VAR = 'DOGFOOD_STACKED_CHANGES'
167
168
Josip Sokcevicf736cab2020-10-20 23:41:38 +0000169class GitPushError(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000170 pass
Josip Sokcevicf736cab2020-10-20 23:41:38 +0000171
172
Daniel Chengabf48472023-08-30 15:45:13 +0000173def DieWithError(message, change_desc=None) -> NoReturn:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000174 if change_desc:
175 SaveDescriptionBackup(change_desc)
176 print('\n ** Content of CL description **\n' + '=' * 72 + '\n' +
177 change_desc.description + '\n' + '=' * 72 + '\n')
Christopher Lamf732cd52017-01-24 12:40:11 +1100178
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000179 print(message, file=sys.stderr)
180 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000181
182
Christopher Lamf732cd52017-01-24 12:40:11 +1100183def SaveDescriptionBackup(change_desc):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000184 backup_path = os.path.join(DEPOT_TOOLS, DESCRIPTION_BACKUP_FILE)
185 print('\nsaving CL description to %s\n' % backup_path)
186 with open(backup_path, 'wb') as backup_file:
187 backup_file.write(change_desc.description.encode('utf-8'))
Christopher Lamf732cd52017-01-24 12:40:11 +1100188
189
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000190def GetNoGitPagerEnv():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000191 env = os.environ.copy()
192 # 'cat' is a magical git string that disables pagers on all platforms.
193 env['GIT_PAGER'] = 'cat'
194 return env
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000195
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000196
bsep@chromium.org627d9002016-04-29 00:00:52 +0000197def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000198 try:
199 stdout = subprocess2.check_output(args, shell=shell, **kwargs)
200 return stdout.decode('utf-8', 'replace')
201 except subprocess2.CalledProcessError as e:
202 logging.debug('Failed running %s', args)
203 if not error_ok:
204 message = error_message or e.stdout.decode('utf-8', 'replace') or ''
205 DieWithError('Command "%s" failed.\n%s' % (' '.join(args), message))
206 out = e.stdout.decode('utf-8', 'replace')
207 if e.stderr:
208 out += e.stderr.decode('utf-8', 'replace')
209 return out
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000210
211
212def RunGit(args, **kwargs):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000213 """Returns stdout."""
214 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000215
216
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000217def RunGitWithCode(args, suppress_stderr=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000218 """Returns return code and stdout."""
219 if suppress_stderr:
220 stderr = subprocess2.DEVNULL
221 else:
222 stderr = sys.stderr
223 try:
224 (out, _), code = subprocess2.communicate(['git'] + args,
225 env=GetNoGitPagerEnv(),
226 stdout=subprocess2.PIPE,
227 stderr=stderr)
228 return code, out.decode('utf-8', 'replace')
229 except subprocess2.CalledProcessError as e:
230 logging.debug('Failed running %s', ['git'] + args)
231 return e.returncode, e.stdout.decode('utf-8', 'replace')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000232
233
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000234def RunGitSilent(args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000235 """Returns stdout, suppresses stderr and ignores the return code."""
236 return RunGitWithCode(args, suppress_stderr=True)[1]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000237
238
tandrii2a16b952016-10-19 07:09:44 -0700239def time_sleep(seconds):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000240 # Use this so that it can be mocked in tests without interfering with python
241 # system machinery.
242 return time.sleep(seconds)
tandrii2a16b952016-10-19 07:09:44 -0700243
244
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000245def time_time():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000246 # Use this so that it can be mocked in tests without interfering with python
247 # system machinery.
248 return time.time()
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000249
250
Edward Lemur1b52d872019-05-09 21:12:12 +0000251def datetime_now():
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000252 # Use this so that it can be mocked in tests without interfering with python
253 # system machinery.
254 return datetime.datetime.now()
Edward Lemur1b52d872019-05-09 21:12:12 +0000255
256
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100257def confirm_or_exit(prefix='', action='confirm'):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000258 """Asks user to press enter to continue or press Ctrl+C to abort."""
259 if not prefix or prefix.endswith('\n'):
260 mid = 'Press'
261 elif prefix.endswith('.') or prefix.endswith('?'):
262 mid = ' Press'
263 elif prefix.endswith(' '):
264 mid = 'press'
265 else:
266 mid = ' press'
267 gclient_utils.AskForData('%s%s Enter to %s, or Ctrl+C to abort' %
268 (prefix, mid, action))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100269
270
271def ask_for_explicit_yes(prompt):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000272 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
273 result = gclient_utils.AskForData(prompt + ' [Yes/No]: ').lower()
274 while True:
275 if 'yes'.startswith(result):
276 return True
277 if 'no'.startswith(result):
278 return False
279 result = gclient_utils.AskForData('Please, type yes or no: ').lower()
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100280
281
machenbach@chromium.org45453142015-09-15 08:45:22 +0000282def _get_properties_from_options(options):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000283 prop_list = getattr(options, 'properties', [])
284 properties = dict(x.split('=', 1) for x in prop_list)
285 for key, val in properties.items():
286 try:
287 properties[key] = json.loads(val)
288 except ValueError:
289 pass # If a value couldn't be evaluated, treat it as a string.
290 return properties
machenbach@chromium.org45453142015-09-15 08:45:22 +0000291
292
Edward Lemur4c707a22019-09-24 21:13:43 +0000293def _call_buildbucket(http, buildbucket_host, method, request):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000294 """Calls a buildbucket v2 method and returns the parsed json response."""
295 headers = {
296 'Accept': 'application/json',
297 'Content-Type': 'application/json',
298 }
299 request = json.dumps(request)
300 url = 'https://%s/prpc/buildbucket.v2.Builds/%s' % (buildbucket_host,
301 method)
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000302
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000303 logging.info('POST %s with %s' % (url, request))
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000304
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000305 attempts = 1
306 time_to_sleep = 1
307 while True:
308 response, content = http.request(url,
309 'POST',
310 body=request,
311 headers=headers)
312 if response.status == 200:
313 return json.loads(content[4:])
314 if attempts >= MAX_ATTEMPTS or 400 <= response.status < 500:
315 msg = '%s error when calling POST %s with %s: %s' % (
316 response.status, url, request, content)
317 raise BuildbucketResponseException(msg)
318 logging.debug('%s error when calling POST %s with %s. '
319 'Sleeping for %d seconds and retrying...' %
320 (response.status, url, request, time_to_sleep))
321 time.sleep(time_to_sleep)
322 time_to_sleep *= 2
323 attempts += 1
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000324
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000325 assert False, 'unreachable'
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000326
327
Edward Lemur6215c792019-10-03 21:59:05 +0000328def _parse_bucket(raw_bucket):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000329 legacy = True
330 project = bucket = None
331 if '/' in raw_bucket:
332 legacy = False
333 project, bucket = raw_bucket.split('/', 1)
334 # Assume luci.<project>.<bucket>.
335 elif raw_bucket.startswith('luci.'):
336 project, bucket = raw_bucket[len('luci.'):].split('.', 1)
337 # Otherwise, assume prefix is also the project name.
338 elif '.' in raw_bucket:
339 project = raw_bucket.split('.')[0]
340 bucket = raw_bucket
341 # Legacy buckets.
342 if legacy and project and bucket:
343 print('WARNING Please use %s/%s to specify the bucket.' %
344 (project, bucket))
345 return project, bucket
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000346
347
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000348def _canonical_git_googlesource_host(host):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000349 """Normalizes Gerrit hosts (with '-review') to Git host."""
350 assert host.endswith(_GOOGLESOURCE)
351 # Prefix doesn't include '.' at the end.
352 prefix = host[:-(1 + len(_GOOGLESOURCE))]
353 if prefix.endswith('-review'):
354 prefix = prefix[:-len('-review')]
355 return prefix + '.' + _GOOGLESOURCE
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000356
357
358def _canonical_gerrit_googlesource_host(host):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000359 git_host = _canonical_git_googlesource_host(host)
360 prefix = git_host.split('.', 1)[0]
361 return prefix + '-review.' + _GOOGLESOURCE
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000362
363
364def _get_counterpart_host(host):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000365 assert host.endswith(_GOOGLESOURCE)
366 git = _canonical_git_googlesource_host(host)
367 gerrit = _canonical_gerrit_googlesource_host(git)
368 return git if gerrit == host else gerrit
Garrett Beaty08bb5c42022-09-21 17:34:20 +0000369
370
Quinten Yearsley777660f2020-03-04 23:37:06 +0000371def _trigger_tryjobs(changelist, jobs, options, patchset):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000372 """Sends a request to Buildbucket to trigger tryjobs for a changelist.
qyearsley1fdfcb62016-10-24 13:22:03 -0700373
374 Args:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000375 changelist: Changelist that the tryjobs are associated with.
Edward Lemur45768512020-03-02 19:03:14 +0000376 jobs: A list of (project, bucket, builder).
qyearsley1fdfcb62016-10-24 13:22:03 -0700377 options: Command-line options.
378 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000379 print('Scheduling jobs on:')
380 for project, bucket, builder in jobs:
381 print(' %s/%s: %s' % (project, bucket, builder))
382 print('To see results here, run: git cl try-results')
383 print('To see results in browser, run: git cl web')
tandriide281ae2016-10-12 06:02:30 -0700384
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000385 requests = _make_tryjob_schedule_requests(changelist, jobs, options,
386 patchset)
387 if not requests:
388 return
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000389
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000390 http = auth.Authenticator().authorize(httplib2.Http())
391 http.force_exception_to_status_code = True
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000392
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000393 batch_request = {'requests': requests}
394 batch_response = _call_buildbucket(http, DEFAULT_BUILDBUCKET_HOST, 'Batch',
395 batch_request)
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000396
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000397 errors = [
398 ' ' + response['error']['message']
399 for response in batch_response.get('responses', [])
400 if 'error' in response
401 ]
402 if errors:
403 raise BuildbucketResponseException(
404 'Failed to schedule builds for some bots:\n%s' % '\n'.join(errors))
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000405
406
Quinten Yearsley777660f2020-03-04 23:37:06 +0000407def _make_tryjob_schedule_requests(changelist, jobs, options, patchset):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000408 """Constructs requests for Buildbucket to trigger tryjobs."""
409 gerrit_changes = [changelist.GetGerritChange(patchset)]
410 shared_properties = {
411 'category': options.ensure_value('category', 'git_cl_try')
412 }
413 if options.ensure_value('clobber', False):
414 shared_properties['clobber'] = True
415 shared_properties.update(_get_properties_from_options(options) or {})
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000416
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000417 shared_tags = [{'key': 'user_agent', 'value': 'git_cl_try'}]
418 if options.ensure_value('retry_failed', False):
419 shared_tags.append({'key': 'retry_failed', 'value': '1'})
Andrii Shyshkalovaeee6a82019-10-09 21:56:25 +0000420
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000421 requests = []
422 for (project, bucket, builder) in jobs:
423 properties = shared_properties.copy()
424 if 'presubmit' in builder.lower():
425 properties['dry_run'] = 'true'
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000426
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000427 requests.append({
428 'scheduleBuild': {
429 'requestId': str(uuid.uuid4()),
430 'builder': {
431 'project': getattr(options, 'project', None) or project,
432 'bucket': bucket,
433 'builder': builder,
434 },
435 'gerritChanges': gerrit_changes,
436 'properties': properties,
437 'tags': [
438 {
439 'key': 'builder',
440 'value': builder
441 },
442 ] + shared_tags,
443 }
444 })
Edward Lemurd4d1ba42019-09-20 21:46:37 +0000445
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000446 if options.ensure_value('revision', None):
447 remote, remote_branch = changelist.GetRemoteBranch()
448 requests[-1]['scheduleBuild']['gitilesCommit'] = {
449 'host':
450 _canonical_git_googlesource_host(gerrit_changes[0]['host']),
451 'project': gerrit_changes[0]['project'],
452 'id': options.revision,
453 'ref': GetTargetRef(remote, remote_branch, None)
454 }
Anthony Polito1a5fe232020-01-24 23:17:52 +0000455
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000456 return requests
kjellander@chromium.org44424542015-06-02 18:35:29 +0000457
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000458
Quinten Yearsley777660f2020-03-04 23:37:06 +0000459def _fetch_tryjobs(changelist, buildbucket_host, patchset=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000460 """Fetches tryjobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000461
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000462 Returns list of buildbucket.v2.Build with the try jobs for the changelist.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000463 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000464 fields = ['id', 'builder', 'status', 'createTime', 'tags']
465 request = {
466 'predicate': {
467 'gerritChanges': [changelist.GetGerritChange(patchset)],
468 },
469 'fields': ','.join('builds.*.' + field for field in fields),
470 }
tandrii221ab252016-10-06 08:12:04 -0700471
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000472 authenticator = auth.Authenticator()
473 if authenticator.has_cached_credentials():
474 http = authenticator.authorize(httplib2.Http())
475 else:
476 print('Warning: Some results might be missing because %s' %
477 # Get the message on how to login.
478 (
479 str(auth.LoginRequiredError()), ))
480 http = httplib2.Http()
481 http.force_exception_to_status_code = True
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000482
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000483 response = _call_buildbucket(http, buildbucket_host, 'SearchBuilds',
484 request)
485 return response.get('builds', [])
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000486
Edward Lemur45768512020-03-02 19:03:14 +0000487
Edward Lemur5b929a42019-10-21 17:57:39 +0000488def _fetch_latest_builds(changelist, buildbucket_host, latest_patchset=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000489 """Fetches builds from the latest patchset that has builds (within
Quinten Yearsley983111f2019-09-26 17:18:48 +0000490 the last few patchsets).
491
492 Args:
Quinten Yearsley983111f2019-09-26 17:18:48 +0000493 changelist (Changelist): The CL to fetch builds for
494 buildbucket_host (str): Buildbucket host, e.g. "cr-buildbucket.appspot.com"
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000495 lastest_patchset(int|NoneType): the patchset to start fetching builds from.
496 If None (default), starts with the latest available patchset.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000497 Returns:
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000498 A tuple (builds, patchset) where builds is a list of buildbucket.v2.Build,
499 and patchset is the patchset number where those builds came from.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000500 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000501 assert buildbucket_host
502 assert changelist.GetIssue(), 'CL must be uploaded first'
503 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
504 if latest_patchset is None:
505 assert changelist.GetMostRecentPatchset()
506 ps = changelist.GetMostRecentPatchset()
507 else:
508 assert latest_patchset > 0, latest_patchset
509 ps = latest_patchset
Andrii Shyshkalov1ad58112019-10-08 01:46:14 +0000510
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000511 min_ps = max(1, ps - 5)
512 while ps >= min_ps:
513 builds = _fetch_tryjobs(changelist, buildbucket_host, patchset=ps)
514 if len(builds):
515 return builds, ps
516 ps -= 1
517 return [], 0
Quinten Yearsley983111f2019-09-26 17:18:48 +0000518
519
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000520def _filter_failed_for_retry(all_builds):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000521 """Returns a list of buckets/builders that are worth retrying.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000522
523 Args:
Quinten Yearsley777660f2020-03-04 23:37:06 +0000524 all_builds (list): Builds, in the format returned by _fetch_tryjobs,
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000525 i.e. a list of buildbucket.v2.Builds which includes status and builder
526 info.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000527
528 Returns:
Edward Lemur45768512020-03-02 19:03:14 +0000529 A dict {(proj, bucket): [builders]}. This is the same format accepted by
Quinten Yearsley777660f2020-03-04 23:37:06 +0000530 _trigger_tryjobs.
Quinten Yearsley983111f2019-09-26 17:18:48 +0000531 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000532 grouped = {}
533 for build in all_builds:
534 builder = build['builder']
535 key = (builder['project'], builder['bucket'], builder['builder'])
536 grouped.setdefault(key, []).append(build)
Andrii Shyshkalov2cbae8a2019-10-11 21:30:27 +0000537
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000538 jobs = []
539 for (project, bucket, builder), builds in grouped.items():
540 if 'triggered' in builder:
541 print(
542 'WARNING: Not scheduling %s. Triggered bots require an initial job '
543 'from a parent. Please schedule a manual job for the parent '
544 'instead.')
545 continue
546 if any(b['status'] in ('STARTED', 'SCHEDULED') for b in builds):
547 # Don't retry if any are running.
548 continue
549 # If builder had several builds, retry only if the last one failed.
550 # This is a bit different from CQ, which would re-use *any* SUCCESS-full
551 # build, but in case of retrying failed jobs retrying a flaky one makes
552 # sense.
553 builds = sorted(builds, key=lambda b: b['createTime'])
554 if builds[-1]['status'] not in ('FAILURE', 'INFRA_FAILURE'):
555 continue
556 # Don't retry experimental build previously triggered by CQ.
557 if any(t['key'] == 'cq_experimental' and t['value'] == 'true'
558 for t in builds[-1]['tags']):
559 continue
560 jobs.append((project, bucket, builder))
Edward Lemur45768512020-03-02 19:03:14 +0000561
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000562 # Sort the jobs to make testing easier.
563 return sorted(jobs)
Quinten Yearsley983111f2019-09-26 17:18:48 +0000564
565
Quinten Yearsley777660f2020-03-04 23:37:06 +0000566def _print_tryjobs(options, builds):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000567 """Prints nicely result of _fetch_tryjobs."""
Edward Lemurbaaf6be2019-10-09 18:00:44 +0000568 if not builds:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000569 print('No tryjobs scheduled.')
570 return
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000571
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000572 longest_builder = max(len(b['builder']['builder']) for b in builds)
573 name_fmt = '{builder:<%d}' % longest_builder
574 if options.print_master:
575 longest_bucket = max(len(b['builder']['bucket']) for b in builds)
576 name_fmt = ('{bucket:>%d} ' % longest_bucket) + name_fmt
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000577
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000578 builds_by_status = {}
579 for b in builds:
580 builds_by_status.setdefault(b['status'], []).append({
581 'id':
582 b['id'],
583 'name':
584 name_fmt.format(builder=b['builder']['builder'],
585 bucket=b['builder']['bucket']),
586 })
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000587
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000588 sort_key = lambda b: (b['name'], b['id'])
589
590 def print_builds(title, builds, fmt=None, color=None):
591 """Pop matching builds from `builds` dict and print them."""
592 if not builds:
593 return
594
595 fmt = fmt or '{name} https://ci.chromium.org/b/{id}'
596 if not options.color or color is None:
597 colorize = lambda x: x
598 else:
599 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
600
601 print(colorize(title))
602 for b in sorted(builds, key=sort_key):
603 print(' ', colorize(fmt.format(**b)))
604
605 total = len(builds)
606 print_builds('Successes:',
607 builds_by_status.pop('SUCCESS', []),
608 color=Fore.GREEN)
609 print_builds('Infra Failures:',
610 builds_by_status.pop('INFRA_FAILURE', []),
611 color=Fore.MAGENTA)
612 print_builds('Failures:',
613 builds_by_status.pop('FAILURE', []),
614 color=Fore.RED)
615 print_builds('Canceled:',
616 builds_by_status.pop('CANCELED', []),
617 fmt='{name}',
618 color=Fore.MAGENTA)
619 print_builds('Started:',
620 builds_by_status.pop('STARTED', []),
621 color=Fore.YELLOW)
622 print_builds('Scheduled:',
623 builds_by_status.pop('SCHEDULED', []),
624 fmt='{name} id={id}')
625 # The last section is just in case buildbucket API changes OR there is a
626 # bug.
627 print_builds('Other:',
628 sum(builds_by_status.values(), []),
629 fmt='{name} id={id}')
630 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000631
632
Aiden Bennerc08566e2018-10-03 17:52:42 +0000633def _ComputeDiffLineRanges(files, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000634 """Gets the changed line ranges for each file since upstream_commit.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000635
636 Parses a git diff on provided files and returns a dict that maps a file name
637 to an ordered list of range tuples in the form (start_line, count).
638 Ranges are in the same format as a git diff.
639 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000640 # If files is empty then diff_output will be a full diff.
641 if len(files) == 0:
642 return {}
Aiden Bennerc08566e2018-10-03 17:52:42 +0000643
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000644 # Take the git diff and find the line ranges where there are changes.
645 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
646 diff_output = RunGit(diff_cmd)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000647
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000648 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
649 # 2 capture groups
650 # 0 == fname of diff file
651 # 1 == 'diff_start,diff_count' or 'diff_start'
652 # will match each of
653 # diff --git a/foo.foo b/foo.py
654 # @@ -12,2 +14,3 @@
655 # @@ -12,2 +17 @@
656 # running re.findall on the above string with pattern will give
657 # [('foo.py', ''), ('', '14,3'), ('', '17')]
Aiden Bennerc08566e2018-10-03 17:52:42 +0000658
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000659 curr_file = None
660 line_diffs = {}
661 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
662 if match[0] != '':
663 # Will match the second filename in diff --git a/a.py b/b.py.
664 curr_file = match[0]
665 line_diffs[curr_file] = []
666 else:
667 # Matches +14,3
668 if ',' in match[1]:
669 diff_start, diff_count = match[1].split(',')
670 else:
671 # Single line changes are of the form +12 instead of +12,1.
672 diff_start = match[1]
673 diff_count = 1
Aiden Bennerc08566e2018-10-03 17:52:42 +0000674
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000675 diff_start = int(diff_start)
676 diff_count = int(diff_count)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000677
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000678 # If diff_count == 0 this is a removal we can ignore.
679 line_diffs[curr_file].append((diff_start, diff_count))
Aiden Bennerc08566e2018-10-03 17:52:42 +0000680
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000681 return line_diffs
Aiden Bennerc08566e2018-10-03 17:52:42 +0000682
683
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000684def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000685 """Checks if a yapf file is in any parent directory of fpath until top_dir.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000686
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000687 Recursively checks parent directories to find yapf file and if no yapf file
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000688 is found returns None. Uses yapf_config_cache as a cache for previously found
689 configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000690 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000691 fpath = os.path.abspath(fpath)
692 # Return result if we've already computed it.
693 if fpath in yapf_config_cache:
694 return yapf_config_cache[fpath]
Aiden Bennerc08566e2018-10-03 17:52:42 +0000695
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000696 parent_dir = os.path.dirname(fpath)
697 if os.path.isfile(fpath):
698 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000699 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000700 # Otherwise fpath is a directory
701 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
702 if os.path.isfile(yapf_file):
703 ret = yapf_file
704 elif fpath in (top_dir, parent_dir):
705 # If we're at the top level directory, or if we're at root
706 # there is no provided style.
707 ret = None
708 else:
709 # Otherwise recurse on the current directory.
710 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
711 yapf_config_cache[fpath] = ret
712 return ret
Aiden Bennerc08566e2018-10-03 17:52:42 +0000713
714
Brian Sheedyb4307d52019-12-02 19:18:17 +0000715def _GetYapfIgnorePatterns(top_dir):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000716 """Returns all patterns in the .yapfignore file.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000717
718 yapf is supposed to handle the ignoring of files listed in .yapfignore itself,
719 but this functionality appears to break when explicitly passing files to
720 yapf for formatting. According to
Josip Sokcevicc39ab992020-09-24 20:09:15 +0000721 https://github.com/google/yapf/blob/HEAD/README.rst#excluding-files-from-formatting-yapfignore,
Brian Sheedy59b06a82019-10-14 17:03:29 +0000722 the .yapfignore file should be in the directory that yapf is invoked from,
723 which we assume to be the top level directory in this case.
724
725 Args:
726 top_dir: The top level directory for the repository being formatted.
727
728 Returns:
Brian Sheedyb4307d52019-12-02 19:18:17 +0000729 A set of all fnmatch patterns to be ignored.
Brian Sheedy59b06a82019-10-14 17:03:29 +0000730 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000731 yapfignore_file = os.path.join(top_dir, '.yapfignore')
732 ignore_patterns = set()
733 if not os.path.exists(yapfignore_file):
734 return ignore_patterns
Brian Sheedy59b06a82019-10-14 17:03:29 +0000735
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000736 for line in gclient_utils.FileRead(yapfignore_file).split('\n'):
737 stripped_line = line.strip()
738 # Comments and blank lines should be ignored.
739 if stripped_line.startswith('#') or stripped_line == '':
740 continue
741 ignore_patterns.add(stripped_line)
742 return ignore_patterns
Brian Sheedyb4307d52019-12-02 19:18:17 +0000743
744
745def _FilterYapfIgnoredFiles(filepaths, patterns):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000746 """Filters out any filepaths that match any of the given patterns.
Brian Sheedyb4307d52019-12-02 19:18:17 +0000747
748 Args:
749 filepaths: An iterable of strings containing filepaths to filter.
750 patterns: An iterable of strings containing fnmatch patterns to filter on.
751
752 Returns:
753 A list of strings containing all the elements of |filepaths| that did not
754 match any of the patterns in |patterns|.
755 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000756 # Not inlined so that tests can use the same implementation.
757 return [
758 f for f in filepaths
759 if not any(fnmatch.fnmatch(f, p) for p in patterns)
760 ]
Brian Sheedy59b06a82019-10-14 17:03:29 +0000761
762
Daniel Cheng66d0f152023-08-29 23:21:58 +0000763def _GetCommitCountSummary(begin_commit: str,
764 end_commit: str = "HEAD") -> Optional[str]:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000765 """Generate a summary of the number of commits in (begin_commit, end_commit).
Daniel Cheng66d0f152023-08-29 23:21:58 +0000766
767 Returns a string containing the summary, or None if the range is empty.
768 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000769 count = int(
770 RunGitSilent(['rev-list', '--count', f'{begin_commit}..{end_commit}']))
Daniel Cheng66d0f152023-08-29 23:21:58 +0000771
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000772 if not count:
773 return None
Daniel Cheng66d0f152023-08-29 23:21:58 +0000774
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000775 return f'{count} commit{"s"[:count!=1]}'
Daniel Cheng66d0f152023-08-29 23:21:58 +0000776
777
Aaron Gable13101a62018-02-09 13:20:41 -0800778def print_stats(args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000779 """Prints statistics about the change to the user."""
780 # --no-ext-diff is broken in some versions of Git, so try to work around
781 # this by overriding the environment (but there is still a problem if the
782 # git config key "diff.external" is used).
783 env = GetNoGitPagerEnv()
784 if 'GIT_EXTERNAL_DIFF' in env:
785 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000786
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000787 return subprocess2.call(
788 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
789 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000790
791
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000792class BuildbucketResponseException(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000793 pass
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000794
795
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000796class Settings(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000797 def __init__(self):
798 self.cc = None
799 self.root = None
800 self.tree_status_url = None
801 self.viewvc_url = None
802 self.updated = False
803 self.is_gerrit = None
804 self.squash_gerrit_uploads = None
805 self.gerrit_skip_ensure_authenticated = None
806 self.git_editor = None
807 self.format_full_by_default = None
808 self.is_status_commit_order_by_date = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000809
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000810 def _LazyUpdateIfNeeded(self):
811 """Updates the settings from a codereview.settings file, if available."""
812 if self.updated:
813 return
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000814
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000815 # The only value that actually changes the behavior is
816 # autoupdate = "false". Everything else means "true".
817 autoupdate = (scm.GIT.GetConfig(self.GetRoot(), 'rietveld.autoupdate',
818 '').lower())
Edward Lemur26964072020-02-19 19:18:51 +0000819
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000820 cr_settings_file = FindCodereviewSettingsFile()
821 if autoupdate != 'false' and cr_settings_file:
822 LoadCodereviewSettingsFromFile(cr_settings_file)
823 cr_settings_file.close()
Edward Lemur26964072020-02-19 19:18:51 +0000824
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000825 self.updated = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000827 @staticmethod
828 def GetRelativeRoot():
829 return scm.GIT.GetCheckoutRoot('.')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000830
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000831 def GetRoot(self):
832 if self.root is None:
833 self.root = os.path.abspath(self.GetRelativeRoot())
834 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000835
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000836 def GetTreeStatusUrl(self, error_ok=False):
837 if not self.tree_status_url:
838 self.tree_status_url = self._GetConfig('rietveld.tree-status-url')
839 if self.tree_status_url is None and not error_ok:
840 DieWithError(
841 'You must configure your tree status URL by running '
842 '"git cl config".')
843 return self.tree_status_url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000844
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000845 def GetViewVCUrl(self):
846 if not self.viewvc_url:
847 self.viewvc_url = self._GetConfig('rietveld.viewvc-url')
848 return self.viewvc_url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000849
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000850 def GetBugPrefix(self):
851 return self._GetConfig('rietveld.bug-prefix')
rmistry@google.com78948ed2015-07-08 23:09:57 +0000852
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000853 def GetRunPostUploadHook(self):
854 run_post_upload_hook = self._GetConfig('rietveld.run-post-upload-hook')
855 return run_post_upload_hook == "True"
rmistry@google.com5626a922015-02-26 14:03:30 +0000856
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000857 def GetDefaultCCList(self):
858 return self._GetConfig('rietveld.cc')
Joanna Wangc8f23e22023-01-19 21:18:10 +0000859
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000860 def GetSquashGerritUploads(self):
861 """Returns True if uploads to Gerrit should be squashed by default."""
862 if self.squash_gerrit_uploads is None:
863 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
864 if self.squash_gerrit_uploads is None:
865 # Default is squash now (http://crbug.com/611892#c23).
866 self.squash_gerrit_uploads = self._GetConfig(
867 'gerrit.squash-uploads').lower() != 'false'
868 return self.squash_gerrit_uploads
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000869
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000870 def GetSquashGerritUploadsOverride(self):
871 """Return True or False if codereview.settings should be overridden.
Edward Lesmes4de54132020-05-05 19:41:33 +0000872
873 Returns None if no override has been defined.
874 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000875 # See also http://crbug.com/611892#c23
876 result = self._GetConfig('gerrit.override-squash-uploads').lower()
877 if result == 'true':
878 return True
879 if result == 'false':
880 return False
881 return None
Edward Lesmes4de54132020-05-05 19:41:33 +0000882
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000883 def GetIsGerrit(self):
884 """Return True if gerrit.host is set."""
885 if self.is_gerrit is None:
886 self.is_gerrit = bool(self._GetConfig('gerrit.host', False))
887 return self.is_gerrit
Aleksey Khoroshilov35ef5ad2022-06-03 18:29:25 +0000888
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000889 def GetGerritSkipEnsureAuthenticated(self):
890 """Return True if EnsureAuthenticated should not be done for Gerrit
tandrii@chromium.org28253532016-04-14 13:46:56 +0000891 uploads."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000892 if self.gerrit_skip_ensure_authenticated is None:
893 self.gerrit_skip_ensure_authenticated = self._GetConfig(
894 'gerrit.skip-ensure-authenticated').lower() == 'true'
895 return self.gerrit_skip_ensure_authenticated
tandrii@chromium.org28253532016-04-14 13:46:56 +0000896
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000897 def GetGitEditor(self):
898 """Returns the editor specified in the git config, or None if none is."""
899 if self.git_editor is None:
900 # Git requires single quotes for paths with spaces. We need to
901 # replace them with double quotes for Windows to treat such paths as
902 # a single path.
903 self.git_editor = self._GetConfig('core.editor').replace('\'', '"')
904 return self.git_editor or None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000905
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000906 def GetLintRegex(self):
907 return self._GetConfig('rietveld.cpplint-regex', DEFAULT_LINT_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000908
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000909 def GetLintIgnoreRegex(self):
910 return self._GetConfig('rietveld.cpplint-ignore-regex',
911 DEFAULT_LINT_IGNORE_REGEX)
thestig@chromium.org44202a22014-03-11 19:22:18 +0000912
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000913 def GetFormatFullByDefault(self):
914 if self.format_full_by_default is None:
915 self._LazyUpdateIfNeeded()
916 result = (RunGit(
917 ['config', '--bool', 'rietveld.format-full-by-default'],
918 error_ok=True).strip())
919 self.format_full_by_default = (result == 'true')
920 return self.format_full_by_default
Jamie Madilldc4d19e2019-10-24 21:50:02 +0000921
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000922 def IsStatusCommitOrderByDate(self):
923 if self.is_status_commit_order_by_date is None:
924 result = (RunGit(['config', '--bool', 'cl.date-order'],
925 error_ok=True).strip())
926 self.is_status_commit_order_by_date = (result == 'true')
927 return self.is_status_commit_order_by_date
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +0000928
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000929 def _GetConfig(self, key, default=''):
930 self._LazyUpdateIfNeeded()
931 return scm.GIT.GetConfig(self.GetRoot(), key, default)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000932
933
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000934class _CQState(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000935 """Enum for states of CL with respect to CQ."""
936 NONE = 'none'
937 DRY_RUN = 'dry_run'
938 COMMIT = 'commit'
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000939
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000940 ALL_STATES = [NONE, DRY_RUN, COMMIT]
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000941
942
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000943class _ParsedIssueNumberArgument(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000944 def __init__(self, issue=None, patchset=None, hostname=None):
945 self.issue = issue
946 self.patchset = patchset
947 self.hostname = hostname
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000948
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000949 @property
950 def valid(self):
951 return self.issue is not None
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000952
953
Edward Lemurf38bc172019-09-03 21:02:13 +0000954def ParseIssueNumberArgument(arg):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000955 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
956 fail_result = _ParsedIssueNumberArgument()
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000957
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000958 if isinstance(arg, int):
959 return _ParsedIssueNumberArgument(issue=arg)
960 if not isinstance(arg, str):
961 return fail_result
Edward Lemur678a6842019-10-03 22:25:05 +0000962
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000963 if arg.isdigit():
964 return _ParsedIssueNumberArgument(issue=int(arg))
Aaron Gableaee6c852017-06-26 12:49:01 -0700965
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000966 url = gclient_utils.UpgradeToHttps(arg)
967 if not url.startswith('http'):
968 return fail_result
969 for gerrit_url, short_url in _KNOWN_GERRIT_TO_SHORT_URLS.items():
970 if url.startswith(short_url):
971 url = gerrit_url + url[len(short_url):]
972 break
Andrii Shyshkalov8aebb602020-04-16 22:10:27 +0000973
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000974 try:
975 parsed_url = urllib.parse.urlparse(url)
976 except ValueError:
977 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +0200978
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000979 # If "https://" was automatically added, fail if `arg` looks unlikely to be
980 # a URL.
981 if not arg.startswith('http') and '.' not in parsed_url.netloc:
982 return fail_result
Alex Turner30ae6372022-01-04 02:32:52 +0000983
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000984 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
985 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
986 # Short urls like https://domain/<issue_number> can be used, but don't allow
987 # specifying the patchset (you'd 404), but we allow that here.
988 if parsed_url.path == '/':
989 part = parsed_url.fragment
990 else:
991 part = parsed_url.path
Edward Lemur678a6842019-10-03 22:25:05 +0000992
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000993 match = re.match(r'(/c(/.*/\+)?)?/(?P<issue>\d+)(/(?P<patchset>\d+)?/?)?$',
994 part)
995 if not match:
996 return fail_result
Edward Lemur678a6842019-10-03 22:25:05 +0000997
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000998 issue = int(match.group('issue'))
999 patchset = match.group('patchset')
1000 return _ParsedIssueNumberArgument(
1001 issue=issue,
1002 patchset=int(patchset) if patchset else None,
1003 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001004
1005
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001006def _create_description_from_log(args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001007 """Pulls out the commit log to use as a base for the CL description."""
1008 log_args = []
1009 if len(args) == 1 and args[0] == None:
1010 # Handle the case where None is passed as the branch.
1011 return ''
1012 if len(args) == 1 and not args[0].endswith('.'):
1013 log_args = [args[0] + '..']
1014 elif len(args) == 1 and args[0].endswith('...'):
1015 log_args = [args[0][:-1]]
1016 elif len(args) == 2:
1017 log_args = [args[0] + '..' + args[1]]
1018 else:
1019 log_args = args[:] # Hope for the best!
1020 return RunGit(['log', '--pretty=format:%B%n'] + log_args)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001021
1022
Aaron Gablea45ee112016-11-22 15:14:38 -08001023class GerritChangeNotExists(Exception):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001024 def __init__(self, issue, url):
1025 self.issue = issue
1026 self.url = url
1027 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001028
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001029 def __str__(self):
1030 return 'change %s at %s does not exist or you have no access to it' % (
1031 self.issue, self.url)
tandriic2405f52016-10-10 08:13:15 -07001032
1033
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001034_CommentSummary = collections.namedtuple(
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001035 '_CommentSummary',
1036 [
1037 'date',
1038 'message',
1039 'sender',
1040 'autogenerated',
1041 # TODO(tandrii): these two aren't known in Gerrit.
1042 'approval',
1043 'disapproval'
1044 ])
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001045
Joanna Wang6215dd02023-02-07 15:58:03 +00001046# TODO(b/265929888): Change `parent` to `pushed_commit_base`.
Joanna Wange8523912023-01-21 02:05:40 +00001047_NewUpload = collections.namedtuple('NewUpload', [
Joanna Wang40497912023-01-24 21:18:16 +00001048 'reviewers', 'ccs', 'commit_to_push', 'new_last_uploaded_commit', 'parent',
Joanna Wang7603f042023-03-01 22:17:36 +00001049 'change_desc', 'prev_patchset'
Joanna Wange8523912023-01-21 02:05:40 +00001050])
1051
1052
Daniel Chengabf48472023-08-30 15:45:13 +00001053class ChangeDescription(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001054 """Contains a parsed form of the change description."""
1055 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
1056 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
1057 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
1058 FIXED_LINE = r'^[ \t]*Fixed[ \t]*:[ \t]*(.*?)[ \t]*$'
1059 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
1060 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
1061 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
1062 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):($|[^:])'
1063 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
Daniel Chengabf48472023-08-30 15:45:13 +00001064
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001065 def __init__(self, description, bug=None, fixed=None):
1066 self._description_lines = (description or '').strip().splitlines()
1067 if bug:
1068 regexp = re.compile(self.BUG_LINE)
1069 prefix = settings.GetBugPrefix()
1070 if not any(
1071 (regexp.match(line) for line in self._description_lines)):
1072 values = list(_get_bug_line_values(prefix, bug))
1073 self.append_footer('Bug: %s' % ', '.join(values))
1074 if fixed:
1075 regexp = re.compile(self.FIXED_LINE)
1076 prefix = settings.GetBugPrefix()
1077 if not any(
1078 (regexp.match(line) for line in self._description_lines)):
1079 values = list(_get_bug_line_values(prefix, fixed))
1080 self.append_footer('Fixed: %s' % ', '.join(values))
Daniel Chengabf48472023-08-30 15:45:13 +00001081
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001082 @property # www.logilab.org/ticket/89786
1083 def description(self): # pylint: disable=method-hidden
1084 return '\n'.join(self._description_lines)
Daniel Chengabf48472023-08-30 15:45:13 +00001085
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001086 def set_description(self, desc):
1087 if isinstance(desc, str):
1088 lines = desc.splitlines()
1089 else:
1090 lines = [line.rstrip() for line in desc]
1091 while lines and not lines[0]:
1092 lines.pop(0)
1093 while lines and not lines[-1]:
1094 lines.pop(-1)
1095 self._description_lines = lines
Daniel Chengabf48472023-08-30 15:45:13 +00001096
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001097 def ensure_change_id(self, change_id):
1098 description = self.description
1099 footer_change_ids = git_footers.get_footer_change_id(description)
1100 # Make sure that the Change-Id in the description matches the given one.
1101 if footer_change_ids != [change_id]:
1102 if footer_change_ids:
1103 # Remove any existing Change-Id footers since they don't match
1104 # the expected change_id footer.
1105 description = git_footers.remove_footer(description,
1106 'Change-Id')
1107 print(
1108 'WARNING: Change-Id has been set to %s. Use `git cl issue 0` '
1109 'if you want to set a new one.')
1110 # Add the expected Change-Id footer.
1111 description = git_footers.add_footer_change_id(
1112 description, change_id)
1113 self.set_description(description)
Daniel Chengabf48472023-08-30 15:45:13 +00001114
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001115 def update_reviewers(self, reviewers):
1116 """Rewrites the R= line(s) as a single line each.
Daniel Chengabf48472023-08-30 15:45:13 +00001117
1118 Args:
1119 reviewers (list(str)) - list of additional emails to use for reviewers.
1120 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001121 if not reviewers:
1122 return
Daniel Chengabf48472023-08-30 15:45:13 +00001123
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001124 reviewers = set(reviewers)
Daniel Chengabf48472023-08-30 15:45:13 +00001125
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001126 # Get the set of R= lines and remove them from the description.
1127 regexp = re.compile(self.R_LINE)
1128 matches = [regexp.match(line) for line in self._description_lines]
1129 new_desc = [
1130 l for i, l in enumerate(self._description_lines) if not matches[i]
1131 ]
1132 self.set_description(new_desc)
Daniel Chengabf48472023-08-30 15:45:13 +00001133
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001134 # Construct new unified R= lines.
Daniel Chengabf48472023-08-30 15:45:13 +00001135
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001136 # First, update reviewers with names from the R= lines (if any).
1137 for match in matches:
1138 if not match:
1139 continue
1140 reviewers.update(cleanup_list([match.group(2).strip()]))
Daniel Chengabf48472023-08-30 15:45:13 +00001141
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001142 new_r_line = 'R=' + ', '.join(sorted(reviewers))
Daniel Chengabf48472023-08-30 15:45:13 +00001143
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001144 # Put the new lines in the description where the old first R= line was.
1145 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1146 if 0 <= line_loc < len(self._description_lines):
1147 self._description_lines.insert(line_loc, new_r_line)
1148 else:
1149 self.append_footer(new_r_line)
Daniel Chengabf48472023-08-30 15:45:13 +00001150
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001151 def set_preserve_tryjobs(self):
1152 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
1153 footers = git_footers.parse_footers(self.description)
1154 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
1155 if v.lower() == 'true':
1156 return
1157 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
Daniel Chengabf48472023-08-30 15:45:13 +00001158
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001159 def prompt(self):
1160 """Asks the user to update the description."""
1161 self.set_description([
1162 '# Enter a description of the change.',
1163 '# This will be displayed on the codereview site.',
1164 '# The first line will also be used as the subject of the review.',
1165 '#--------------------This line is 72 characters long'
1166 '--------------------',
1167 ] + self._description_lines)
1168 bug_regexp = re.compile(self.BUG_LINE)
1169 fixed_regexp = re.compile(self.FIXED_LINE)
1170 prefix = settings.GetBugPrefix()
1171 has_issue = lambda l: bug_regexp.match(l) or fixed_regexp.match(l)
Daniel Chengabf48472023-08-30 15:45:13 +00001172
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001173 if not any((has_issue(line) for line in self._description_lines)):
1174 self.append_footer('Bug: %s' % prefix)
Daniel Chengabf48472023-08-30 15:45:13 +00001175
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001176 print('Waiting for editor...')
1177 content = gclient_utils.RunEditor(self.description,
1178 True,
1179 git_editor=settings.GetGitEditor())
1180 if not content:
1181 DieWithError('Running editor failed')
1182 lines = content.splitlines()
Daniel Chengabf48472023-08-30 15:45:13 +00001183
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001184 # Strip off comments and default inserted "Bug:" line.
1185 clean_lines = [
1186 line.rstrip() for line in lines
1187 if not (line.startswith('#') or line.rstrip() == "Bug:"
1188 or line.rstrip() == "Bug: " + prefix)
1189 ]
1190 if not clean_lines:
1191 DieWithError('No CL description, aborting')
1192 self.set_description(clean_lines)
Daniel Chengabf48472023-08-30 15:45:13 +00001193
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001194 def append_footer(self, line):
1195 """Adds a footer line to the description.
Daniel Chengabf48472023-08-30 15:45:13 +00001196
1197 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
1198 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
1199 that Gerrit footers are always at the end.
1200 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001201 parsed_footer_line = git_footers.parse_footer(line)
1202 if parsed_footer_line:
1203 # Line is a gerrit footer in the form: Footer-Key: any value.
1204 # Thus, must be appended observing Gerrit footer rules.
1205 self.set_description(
1206 git_footers.add_footer(self.description,
1207 key=parsed_footer_line[0],
1208 value=parsed_footer_line[1]))
1209 return
Daniel Chengabf48472023-08-30 15:45:13 +00001210
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001211 if not self._description_lines:
1212 self._description_lines.append(line)
1213 return
Daniel Chengabf48472023-08-30 15:45:13 +00001214
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001215 top_lines, gerrit_footers, _ = git_footers.split_footers(
1216 self.description)
1217 if gerrit_footers:
1218 # git_footers.split_footers ensures that there is an empty line
1219 # before actual (gerrit) footers, if any. We have to keep it that
1220 # way.
1221 assert top_lines and top_lines[-1] == ''
1222 top_lines, separator = top_lines[:-1], top_lines[-1:]
1223 else:
1224 separator = [
1225 ] # No need for separator if there are no gerrit_footers.
Daniel Chengabf48472023-08-30 15:45:13 +00001226
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001227 prev_line = top_lines[-1] if top_lines else ''
1228 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line)
1229 or not presubmit_support.Change.TAG_LINE_RE.match(line)):
1230 top_lines.append('')
1231 top_lines.append(line)
1232 self._description_lines = top_lines + separator + gerrit_footers
Daniel Chengabf48472023-08-30 15:45:13 +00001233
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001234 def get_reviewers(self, tbr_only=False):
1235 """Retrieves the list of reviewers."""
1236 matches = [
1237 re.match(self.R_LINE, line) for line in self._description_lines
1238 ]
1239 reviewers = [
1240 match.group(2).strip() for match in matches
1241 if match and (not tbr_only or match.group(1).upper() == 'TBR')
1242 ]
1243 return cleanup_list(reviewers)
Daniel Chengabf48472023-08-30 15:45:13 +00001244
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001245 def get_cced(self):
1246 """Retrieves the list of reviewers."""
1247 matches = [
1248 re.match(self.CC_LINE, line) for line in self._description_lines
1249 ]
1250 cced = [match.group(2).strip() for match in matches if match]
1251 return cleanup_list(cced)
Daniel Chengabf48472023-08-30 15:45:13 +00001252
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001253 def get_hash_tags(self):
1254 """Extracts and sanitizes a list of Gerrit hashtags."""
1255 subject = (self._description_lines or ('', ))[0]
1256 subject = re.sub(self.STRIP_HASH_TAG_PREFIX,
1257 '',
1258 subject,
1259 flags=re.IGNORECASE)
Daniel Chengabf48472023-08-30 15:45:13 +00001260
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001261 tags = []
1262 start = 0
1263 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
1264 while True:
1265 m = bracket_exp.match(subject, start)
1266 if not m:
1267 break
1268 tags.append(self.sanitize_hash_tag(m.group(1)))
1269 start = m.end()
Daniel Chengabf48472023-08-30 15:45:13 +00001270
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001271 if not tags:
1272 # Try "Tag: " prefix.
1273 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
1274 if m:
1275 tags.append(self.sanitize_hash_tag(m.group(1)))
1276 return tags
Daniel Chengabf48472023-08-30 15:45:13 +00001277
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001278 @classmethod
1279 def sanitize_hash_tag(cls, tag):
1280 """Returns a sanitized Gerrit hash tag.
Daniel Chengabf48472023-08-30 15:45:13 +00001281
1282 A sanitized hashtag can be used as a git push refspec parameter value.
1283 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001284 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
Daniel Chengabf48472023-08-30 15:45:13 +00001285
1286
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001287class Changelist(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001288 """Changelist works with one changelist in local branch.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001289
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001290 Notes:
1291 * Not safe for concurrent multi-{thread,process} use.
1292 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001293 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001294 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001295 def __init__(self,
1296 branchref=None,
1297 issue=None,
1298 codereview_host=None,
1299 commit_date=None):
1300 """Create a new ChangeList instance.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001301
Edward Lemurf38bc172019-09-03 21:02:13 +00001302 **kwargs will be passed directly to Gerrit implementation.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001303 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001304 # Poke settings so we get the "configure your server" message if
1305 # necessary.
1306 global settings
1307 if not settings:
1308 # Happens when git_cl.py is used as a utility library.
1309 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001310
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001311 self.branchref = branchref
1312 if self.branchref:
1313 assert branchref.startswith('refs/heads/')
1314 self.branch = scm.GIT.ShortBranchName(self.branchref)
1315 else:
1316 self.branch = None
1317 self.commit_date = commit_date
1318 self.upstream_branch = None
1319 self.lookedup_issue = False
1320 self.issue = issue or None
1321 self.description = None
1322 self.lookedup_patchset = False
1323 self.patchset = None
1324 self.cc = None
1325 self.more_cc = []
1326 self._remote = None
1327 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001328
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001329 # Lazily cached values.
1330 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1331 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1332 self._owners_client = None
1333 # Map from change number (issue) to its detail cache.
1334 self._detail_cache = {}
Edward Lemur125d60a2019-09-13 18:25:41 +00001335
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001336 if codereview_host is not None:
1337 assert not codereview_host.startswith('https://'), codereview_host
1338 self._gerrit_host = codereview_host
1339 self._gerrit_server = 'https://%s' % codereview_host
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001340
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001341 @property
1342 def owners_client(self):
1343 if self._owners_client is None:
1344 remote, remote_branch = self.GetRemoteBranch()
1345 branch = GetTargetRef(remote, remote_branch, None)
1346 self._owners_client = owners_client.GetCodeOwnersClient(
1347 host=self.GetGerritHost(),
1348 project=self.GetGerritProject(),
1349 branch=branch)
1350 return self._owners_client
Edward Lesmese1576912021-02-16 21:53:34 +00001351
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001352 def GetCCList(self):
1353 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001354
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001355 The return value is a string suitable for passing to git cl with the --cc
1356 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001357 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001358 if self.cc is None:
1359 base_cc = settings.GetDefaultCCList()
1360 more_cc = ','.join(self.more_cc)
1361 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1362 return self.cc
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001363
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001364 def ExtendCC(self, more_cc):
1365 """Extends the list of users to cc on this CL based on the changed files."""
1366 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001367
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001368 def GetCommitDate(self):
1369 """Returns the commit date as provided in the constructor"""
1370 return self.commit_date
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00001371
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001372 def GetBranch(self):
1373 """Returns the short branch name, e.g. 'main'."""
1374 if not self.branch:
1375 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
1376 if not branchref:
1377 return None
1378 self.branchref = branchref
1379 self.branch = scm.GIT.ShortBranchName(self.branchref)
1380 return self.branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001381
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001382 def GetBranchRef(self):
1383 """Returns the full branch name, e.g. 'refs/heads/main'."""
1384 self.GetBranch() # Poke the lazy loader.
1385 return self.branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001386
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001387 def _GitGetBranchConfigValue(self, key, default=None):
1388 return scm.GIT.GetBranchConfig(settings.GetRoot(), self.GetBranch(),
1389 key, default)
tandrii5d48c322016-08-18 16:19:37 -07001390
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001391 def _GitSetBranchConfigValue(self, key, value):
1392 action = 'set %s to %r' % (key, value)
1393 if not value:
1394 action = 'unset %s' % key
1395 assert self.GetBranch(), 'a branch is needed to ' + action
1396 return scm.GIT.SetBranchConfig(settings.GetRoot(), self.GetBranch(),
1397 key, value)
tandrii5d48c322016-08-18 16:19:37 -07001398
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001399 @staticmethod
1400 def FetchUpstreamTuple(branch):
1401 """Returns a tuple containing remote and remote ref,
Josip Sokcevicc39ab992020-09-24 20:09:15 +00001402 e.g. 'origin', 'refs/heads/main'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001403 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001404 remote, upstream_branch = scm.GIT.FetchUpstreamTuple(
1405 settings.GetRoot(), branch)
1406 if not remote or not upstream_branch:
1407 DieWithError(
1408 'Unable to determine default branch to diff against.\n'
1409 'Verify this branch is set up to track another \n'
1410 '(via the --track argument to "git checkout -b ..."). \n'
1411 'or pass complete "git diff"-style arguments if supported, like\n'
1412 ' git cl upload origin/main\n')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001414 return remote, upstream_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001416 def GetCommonAncestorWithUpstream(self):
1417 upstream_branch = self.GetUpstreamBranch()
1418 if not scm.GIT.IsValidRevision(settings.GetRoot(), upstream_branch):
1419 DieWithError(
1420 'The upstream for the current branch (%s) does not exist '
1421 'anymore.\nPlease fix it and try again.' % self.GetBranch())
1422 return git_common.get_or_create_merge_base(self.GetBranch(),
1423 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001424
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001425 def GetUpstreamBranch(self):
1426 if self.upstream_branch is None:
1427 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
1428 if remote != '.':
1429 upstream_branch = upstream_branch.replace(
1430 'refs/heads/', 'refs/remotes/%s/' % remote)
1431 upstream_branch = upstream_branch.replace(
1432 'refs/branch-heads/', 'refs/remotes/branch-heads/')
1433 self.upstream_branch = upstream_branch
1434 return self.upstream_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001435
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001436 def GetRemoteBranch(self):
1437 if not self._remote:
1438 remote, branch = None, self.GetBranch()
1439 seen_branches = set()
1440 while branch not in seen_branches:
1441 seen_branches.add(branch)
1442 remote, branch = self.FetchUpstreamTuple(branch)
1443 branch = scm.GIT.ShortBranchName(branch)
1444 if remote != '.' or branch.startswith('refs/remotes'):
1445 break
1446 else:
1447 remotes = RunGit(['remote'], error_ok=True).split()
1448 if len(remotes) == 1:
1449 remote, = remotes
1450 elif 'origin' in remotes:
1451 remote = 'origin'
1452 logging.warning(
1453 'Could not determine which remote this change is '
1454 'associated with, so defaulting to "%s".' %
1455 self._remote)
1456 else:
1457 logging.warning(
1458 'Could not determine which remote this change is '
1459 'associated with.')
1460 branch = 'HEAD'
1461 if branch.startswith('refs/remotes'):
1462 self._remote = (remote, branch)
1463 elif branch.startswith('refs/branch-heads/'):
1464 self._remote = (remote, branch.replace('refs/',
1465 'refs/remotes/'))
1466 else:
1467 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
1468 return self._remote
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001469
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001470 def GetRemoteUrl(self) -> Optional[str]:
1471 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001472
1473 Returns None if there is no remote.
1474 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001475 is_cached, value = self._cached_remote_url
1476 if is_cached:
1477 return value
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001478
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001479 remote, _ = self.GetRemoteBranch()
1480 url = scm.GIT.GetConfig(settings.GetRoot(), 'remote.%s.url' % remote,
1481 '')
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001482
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001483 # Check if the remote url can be parsed as an URL.
1484 host = urllib.parse.urlparse(url).netloc
1485 if host:
1486 self._cached_remote_url = (True, url)
1487 return url
Edward Lemur298f2cf2019-02-22 21:40:39 +00001488
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001489 # If it cannot be parsed as an url, assume it is a local directory,
1490 # probably a git cache.
1491 logging.warning(
1492 '"%s" doesn\'t appear to point to a git host. '
1493 'Interpreting it as a local directory.', url)
1494 if not os.path.isdir(url):
1495 logging.error(
1496 'Remote "%(remote)s" for branch "%(branch)s" points to "%(url)s", '
1497 'but it doesn\'t exist.', {
1498 'remote': remote,
1499 'branch': self.GetBranch(),
1500 'url': url
1501 })
1502 return None
Edward Lemur298f2cf2019-02-22 21:40:39 +00001503
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001504 cache_path = url
1505 url = scm.GIT.GetConfig(url, 'remote.%s.url' % remote, '')
Edward Lemur298f2cf2019-02-22 21:40:39 +00001506
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001507 host = urllib.parse.urlparse(url).netloc
1508 if not host:
1509 logging.error(
1510 'Remote "%(remote)s" for branch "%(branch)s" points to '
1511 '"%(cache_path)s", but it is misconfigured.\n'
1512 '"%(cache_path)s" must be a git repo and must have a remote named '
1513 '"%(remote)s" pointing to the git host.', {
1514 'remote': remote,
1515 'cache_path': cache_path,
1516 'branch': self.GetBranch()
1517 })
1518 return None
Edward Lemur298f2cf2019-02-22 21:40:39 +00001519
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001520 self._cached_remote_url = (True, url)
1521 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001522
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001523 def GetIssue(self):
1524 """Returns the issue number as a int or None if not set."""
1525 if self.issue is None and not self.lookedup_issue:
1526 if self.GetBranch():
1527 self.issue = self._GitGetBranchConfigValue(ISSUE_CONFIG_KEY)
1528 if self.issue is not None:
1529 self.issue = int(self.issue)
1530 self.lookedup_issue = True
1531 return self.issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001532
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001533 def GetIssueURL(self, short=False):
1534 """Get the URL for a particular issue."""
1535 issue = self.GetIssue()
1536 if not issue:
1537 return None
1538 server = self.GetCodereviewServer()
1539 if short:
1540 server = _KNOWN_GERRIT_TO_SHORT_URLS.get(server, server)
1541 return '%s/%s' % (server, issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001542
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001543 def FetchDescription(self, pretty=False):
1544 assert self.GetIssue(), 'issue is required to query Gerrit'
Edward Lemur6c6827c2020-02-06 21:15:18 +00001545
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001546 if self.description is None:
1547 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
1548 current_rev = data['current_revision']
1549 self.description = data['revisions'][current_rev]['commit'][
1550 'message']
Edward Lemur6c6827c2020-02-06 21:15:18 +00001551
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001552 if not pretty:
1553 return self.description
Edward Lemur6c6827c2020-02-06 21:15:18 +00001554
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001555 # Set width to 72 columns + 2 space indent.
1556 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
1557 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1558 lines = self.description.splitlines()
1559 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001560
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001561 def GetPatchset(self):
1562 """Returns the patchset number as a int or None if not set."""
1563 if self.patchset is None and not self.lookedup_patchset:
1564 if self.GetBranch():
1565 self.patchset = self._GitGetBranchConfigValue(
1566 PATCHSET_CONFIG_KEY)
1567 if self.patchset is not None:
1568 self.patchset = int(self.patchset)
1569 self.lookedup_patchset = True
1570 return self.patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001571
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001572 def GetAuthor(self):
1573 return scm.GIT.GetConfig(settings.GetRoot(), 'user.email')
Edward Lemur9aa1a962020-02-25 00:58:38 +00001574
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001575 def SetPatchset(self, patchset):
1576 """Set this branch's patchset. If patchset=0, clears the patchset."""
1577 assert self.GetBranch()
1578 if not patchset:
1579 self.patchset = None
1580 else:
1581 self.patchset = int(patchset)
1582 self._GitSetBranchConfigValue(PATCHSET_CONFIG_KEY, str(self.patchset))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001583
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001584 def SetIssue(self, issue=None):
1585 """Set this branch's issue. If issue isn't given, clears the issue."""
1586 assert self.GetBranch()
1587 if issue:
1588 issue = int(issue)
1589 self._GitSetBranchConfigValue(ISSUE_CONFIG_KEY, str(issue))
1590 self.issue = issue
1591 codereview_server = self.GetCodereviewServer()
1592 if codereview_server:
1593 self._GitSetBranchConfigValue(CODEREVIEW_SERVER_CONFIG_KEY,
1594 codereview_server)
1595 else:
1596 # Reset all of these just to be clean.
1597 reset_suffixes = [
1598 LAST_UPLOAD_HASH_CONFIG_KEY,
1599 ISSUE_CONFIG_KEY,
1600 PATCHSET_CONFIG_KEY,
1601 CODEREVIEW_SERVER_CONFIG_KEY,
1602 GERRIT_SQUASH_HASH_CONFIG_KEY,
1603 ]
1604 for prop in reset_suffixes:
1605 try:
1606 self._GitSetBranchConfigValue(prop, None)
1607 except subprocess2.CalledProcessError:
1608 pass
1609 msg = RunGit(['log', '-1', '--format=%B']).strip()
1610 if msg and git_footers.get_footer_change_id(msg):
1611 print(
1612 'WARNING: The change patched into this branch has a Change-Id. '
1613 'Removing it.')
1614 RunGit([
1615 'commit', '--amend', '-m',
1616 git_footers.remove_footer(msg, 'Change-Id')
1617 ])
1618 self.lookedup_issue = True
1619 self.issue = None
1620 self.patchset = None
1621
1622 def GetAffectedFiles(self,
1623 upstream: str,
1624 end_commit: Optional[str] = None) -> Sequence[str]:
1625 """Returns the list of affected files for the given commit range."""
Edward Lemur85153282020-02-14 22:06:29 +00001626 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001627 return [
1628 f for _, f in scm.GIT.CaptureStatus(
1629 settings.GetRoot(), upstream, end_commit=end_commit)
1630 ]
Edward Lemur85153282020-02-14 22:06:29 +00001631 except subprocess2.CalledProcessError:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001632 DieWithError(
1633 ('\nFailed to diff against upstream branch %s\n\n'
1634 'This branch probably doesn\'t exist anymore. To reset the\n'
1635 'tracking branch, please run\n'
1636 ' git branch --set-upstream-to origin/main %s\n'
1637 'or replace origin/main with the relevant branch') %
1638 (upstream, self.GetBranch()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001639
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001640 def UpdateDescription(self, description, force=False):
1641 assert self.GetIssue(), 'issue is required to update description'
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001642
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001643 if gerrit_util.HasPendingChangeEdit(self.GetGerritHost(),
1644 self._GerritChangeIdentifier()):
1645 if not force:
1646 confirm_or_exit(
1647 'The description cannot be modified while the issue has a pending '
1648 'unpublished edit. Either publish the edit in the Gerrit web UI '
1649 'or delete it.\n\n',
1650 action='delete the unpublished edit')
Edward Lemur6c6827c2020-02-06 21:15:18 +00001651
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001652 gerrit_util.DeletePendingChangeEdit(self.GetGerritHost(),
1653 self._GerritChangeIdentifier())
1654 gerrit_util.SetCommitMessage(self.GetGerritHost(),
1655 self._GerritChangeIdentifier(),
1656 description,
1657 notify='NONE')
Edward Lemur6c6827c2020-02-06 21:15:18 +00001658
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001659 self.description = description
Edward Lemur6c6827c2020-02-06 21:15:18 +00001660
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001661 def _GetCommonPresubmitArgs(self, verbose, upstream):
1662 args = [
1663 '--root',
1664 settings.GetRoot(),
1665 '--upstream',
1666 upstream,
1667 ]
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001668
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001669 args.extend(['--verbose'] * verbose)
Edward Lemur227d5102020-02-25 23:45:35 +00001670
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001671 remote, remote_branch = self.GetRemoteBranch()
1672 target_ref = GetTargetRef(remote, remote_branch, None)
1673 if settings.GetIsGerrit():
1674 args.extend(['--gerrit_url', self.GetCodereviewServer()])
1675 args.extend(['--gerrit_project', self.GetGerritProject()])
1676 args.extend(['--gerrit_branch', target_ref])
Edward Lemur227d5102020-02-25 23:45:35 +00001677
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001678 author = self.GetAuthor()
1679 issue = self.GetIssue()
1680 patchset = self.GetPatchset()
1681 if author:
1682 args.extend(['--author', author])
1683 if issue:
1684 args.extend(['--issue', str(issue)])
1685 if patchset:
1686 args.extend(['--patchset', str(patchset)])
Edward Lesmeseb1bd622021-03-01 19:54:07 +00001687
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001688 return args
Edward Lemur227d5102020-02-25 23:45:35 +00001689
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001690 def RunHook(self,
1691 committing,
1692 may_prompt,
1693 verbose,
1694 parallel,
1695 upstream,
1696 description,
1697 all_files,
1698 files=None,
1699 resultdb=False,
1700 realm=None):
1701 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1702 args = self._GetCommonPresubmitArgs(verbose, upstream)
1703 args.append('--commit' if committing else '--upload')
1704 if may_prompt:
1705 args.append('--may_prompt')
1706 if parallel:
1707 args.append('--parallel')
1708 if all_files:
1709 args.append('--all_files')
1710 if files:
1711 args.extend(files.split(';'))
1712 args.append('--source_controlled_only')
1713 if files or all_files:
1714 args.append('--no_diffs')
Edward Lemur75526302020-02-27 22:31:05 +00001715
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001716 if resultdb and not realm:
1717 # TODO (crbug.com/1113463): store realm somewhere and look it up so
1718 # it is not required to pass the realm flag
1719 print(
1720 'Note: ResultDB reporting will NOT be performed because --realm'
1721 ' was not specified. To enable ResultDB, please run the command'
1722 ' again with the --realm argument to specify the LUCI realm.')
Edward Lemur227d5102020-02-25 23:45:35 +00001723
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001724 return self._RunPresubmit(args,
1725 description,
1726 resultdb=resultdb,
1727 realm=realm)
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001728
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001729 def _RunPresubmit(self,
1730 args: Sequence[str],
1731 description: str,
1732 resultdb: bool = False,
1733 realm: Optional[str] = None) -> Mapping[str, Any]:
1734 args = list(args)
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001735
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001736 with gclient_utils.temporary_file() as description_file:
1737 with gclient_utils.temporary_file() as json_output:
1738 gclient_utils.FileWrite(description_file, description)
1739 args.extend(['--json_output', json_output])
1740 args.extend(['--description_file', description_file])
1741 start = time_time()
1742 cmd = ['vpython3', PRESUBMIT_SUPPORT] + args
1743 if resultdb and realm:
1744 cmd = ['rdb', 'stream', '-new', '-realm', realm, '--'] + cmd
Dirk Pranke61bf6e82021-04-23 00:50:21 +00001745
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001746 p = subprocess2.Popen(cmd)
1747 exit_code = p.wait()
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001748
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001749 metrics.collector.add_repeated(
1750 'sub_commands', {
1751 'command': 'presubmit',
1752 'execution_time': time_time() - start,
1753 'exit_code': exit_code,
1754 })
Saagar Sanghavi9949ab72020-07-20 20:56:40 +00001755
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001756 if exit_code:
1757 sys.exit(exit_code)
Edward Lemur227d5102020-02-25 23:45:35 +00001758
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001759 json_results = gclient_utils.FileRead(json_output)
1760 return json.loads(json_results)
Edward Lemur227d5102020-02-25 23:45:35 +00001761
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001762 def RunPostUploadHook(self, verbose, upstream, description):
1763 args = self._GetCommonPresubmitArgs(verbose, upstream)
1764 args.append('--post_upload')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001765
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001766 with gclient_utils.temporary_file() as description_file:
1767 gclient_utils.FileWrite(description_file, description)
1768 args.extend(['--description_file', description_file])
1769 subprocess2.Popen(['vpython3', PRESUBMIT_SUPPORT] + args).wait()
Edward Lemur75526302020-02-27 22:31:05 +00001770
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001771 def _GetDescriptionForUpload(self, options: optparse.Values,
1772 git_diff_args: Sequence[str],
1773 files: Sequence[str]) -> ChangeDescription:
1774 """Get description message for upload."""
1775 if self.GetIssue():
1776 description = self.FetchDescription()
1777 elif options.message:
1778 description = options.message
1779 else:
1780 description = _create_description_from_log(git_diff_args)
1781 if options.title and options.squash:
1782 description = options.title + '\n\n' + description
Edward Lemur75526302020-02-27 22:31:05 +00001783
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001784 bug = options.bug
1785 fixed = options.fixed
1786 if not self.GetIssue():
1787 # Extract bug number from branch name, but only if issue is being
1788 # created. It must start with bug or fix, followed by _ or - and
1789 # number. Optionally, it may contain _ or - after number with
1790 # arbitrary text. Examples: bug-123 bug_123 fix-123
1791 # fix-123-some-description
1792 branch = self.GetBranch()
1793 if branch is not None:
1794 match = re.match(
1795 r'^(?P<type>bug|fix(?:e[sd])?)[_-]?(?P<bugnum>\d+)([-_]|$)',
1796 branch)
1797 if not bug and not fixed and match:
1798 if match.group('type') == 'bug':
1799 bug = match.group('bugnum')
1800 else:
1801 fixed = match.group('bugnum')
Edward Lemur5a644f82020-03-18 16:44:57 +00001802
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001803 change_description = ChangeDescription(description, bug, fixed)
Edward Lemur5a644f82020-03-18 16:44:57 +00001804
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001805 # Fill gaps in OWNERS coverage to reviewers if requested.
1806 if options.add_owners_to:
1807 assert options.add_owners_to in ('R'), options.add_owners_to
1808 status = self.owners_client.GetFilesApprovalStatus(
1809 files, [], options.reviewers)
1810 missing_files = [
1811 f for f in files
1812 if status[f] == self._owners_client.INSUFFICIENT_REVIEWERS
1813 ]
1814 owners = self.owners_client.SuggestOwners(
1815 missing_files, exclude=[self.GetAuthor()])
1816 assert isinstance(options.reviewers, list), options.reviewers
1817 options.reviewers.extend(owners)
Edward Lemur5a644f82020-03-18 16:44:57 +00001818
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001819 # Set the reviewer list now so that presubmit checks can access it.
1820 if options.reviewers:
1821 change_description.update_reviewers(options.reviewers)
Edward Lesmes8c43c3f2021-01-20 00:20:26 +00001822
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001823 return change_description
Edward Lemur5a644f82020-03-18 16:44:57 +00001824
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001825 def _GetTitleForUpload(self, options, multi_change_upload=False):
1826 # type: (optparse.Values, Optional[bool]) -> str
Edward Lemur5a644f82020-03-18 16:44:57 +00001827
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001828 # Getting titles for multipl commits is not supported so we return the
1829 # default.
1830 if not options.squash or multi_change_upload or options.title:
1831 return options.title
Joanna Wanga1abbed2023-01-24 01:41:05 +00001832
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001833 # On first upload, patchset title is always this string, while
1834 # options.title gets converted to first line of message.
1835 if not self.GetIssue():
1836 return 'Initial upload'
Edward Lemur5a644f82020-03-18 16:44:57 +00001837
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001838 # When uploading subsequent patchsets, options.message is taken as the
1839 # title if options.title is not provided.
1840 if options.message:
1841 return options.message.strip()
Edward Lemur5a644f82020-03-18 16:44:57 +00001842
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001843 # Use the subject of the last commit as title by default.
1844 title = RunGit(['show', '-s', '--format=%s', 'HEAD']).strip()
1845 if options.force or options.skip_title:
1846 return title
1847 user_title = gclient_utils.AskForData('Title for patchset [%s]: ' %
1848 title)
Edward Lemur5a644f82020-03-18 16:44:57 +00001849
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001850 # Use the default title if the user confirms the default with a 'y'.
1851 if user_title.lower() == 'y':
1852 return title
1853 return user_title or title
mlcui3da91712021-05-05 10:00:30 +00001854
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001855 def _GetRefSpecOptions(self,
1856 options: optparse.Values,
1857 change_desc: ChangeDescription,
1858 multi_change_upload: bool = False,
1859 dogfood_path: bool = False) -> List[str]:
1860 # Extra options that can be specified at push time. Doc:
1861 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
1862 refspec_opts = []
Edward Lemur5a644f82020-03-18 16:44:57 +00001863
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001864 # By default, new changes are started in WIP mode, and subsequent
1865 # patchsets don't send email. At any time, passing --send-mail or
1866 # --send-email will mark the change ready and send email for that
1867 # particular patch.
1868 if options.send_mail:
1869 refspec_opts.append('ready')
1870 refspec_opts.append('notify=ALL')
1871 elif (not self.GetIssue() and options.squash and not dogfood_path):
1872 refspec_opts.append('wip')
Joanna Wanga1abbed2023-01-24 01:41:05 +00001873
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001874 # TODO(tandrii): options.message should be posted as a comment if
1875 # --send-mail or --send-email is set on non-initial upload as Rietveld
1876 # used to do it.
Joanna Wanga1abbed2023-01-24 01:41:05 +00001877
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001878 # Set options.title in case user was prompted in _GetTitleForUpload and
1879 # _CMDUploadChange needs to be called again.
1880 options.title = self._GetTitleForUpload(
1881 options, multi_change_upload=multi_change_upload)
Joanna Wanga1abbed2023-01-24 01:41:05 +00001882
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001883 if options.title:
1884 # Punctuation and whitespace in |title| must be percent-encoded.
1885 refspec_opts.append(
1886 'm=' + gerrit_util.PercentEncodeForGitRef(options.title))
Joanna Wanga1abbed2023-01-24 01:41:05 +00001887
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001888 if options.private:
1889 refspec_opts.append('private')
Joanna Wanga1abbed2023-01-24 01:41:05 +00001890
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001891 if options.topic:
1892 # Documentation on Gerrit topics is here:
1893 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
1894 refspec_opts.append('topic=%s' % options.topic)
Joanna Wanga1abbed2023-01-24 01:41:05 +00001895
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001896 if options.enable_auto_submit:
1897 refspec_opts.append('l=Auto-Submit+1')
1898 if options.set_bot_commit:
1899 refspec_opts.append('l=Bot-Commit+1')
1900 if options.use_commit_queue:
1901 refspec_opts.append('l=Commit-Queue+2')
1902 elif options.cq_dry_run:
1903 refspec_opts.append('l=Commit-Queue+1')
Joanna Wanga1abbed2023-01-24 01:41:05 +00001904
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001905 if change_desc.get_reviewers(tbr_only=True):
1906 score = gerrit_util.GetCodeReviewTbrScore(self.GetGerritHost(),
1907 self.GetGerritProject())
1908 refspec_opts.append('l=Code-Review+%s' % score)
Joanna Wanga1abbed2023-01-24 01:41:05 +00001909
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001910 # Gerrit sorts hashtags, so order is not important.
1911 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
1912 # We check GetIssue because we only add hashtags from the
1913 # description on the first upload.
1914 # TODO(b/265929888): When we fully launch the new path:
1915 # 1) remove fetching hashtags from description alltogether
1916 # 2) Or use descrtiption hashtags for:
1917 # `not (self.GetIssue() and multi_change_upload)`
1918 # 3) Or enabled change description tags for multi and single changes
1919 # by adding them post `git push`.
1920 if not (self.GetIssue() and dogfood_path):
1921 hashtags.update(change_desc.get_hash_tags())
1922 refspec_opts.extend(['hashtag=%s' % t for t in hashtags])
Joanna Wanga1abbed2023-01-24 01:41:05 +00001923
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001924 # Note: Reviewers, and ccs are handled individually for each
1925 # branch/change.
1926 return refspec_opts
Joanna Wang40497912023-01-24 21:18:16 +00001927
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001928 def PrepareSquashedCommit(self,
1929 options: optparse.Values,
1930 parent: str,
1931 orig_parent: str,
1932 end_commit: Optional[str] = None) -> _NewUpload:
1933 """Create a squashed commit to upload.
Joanna Wang05b60342023-03-29 20:25:57 +00001934
1935
1936 Args:
1937 parent: The commit to use as the parent for the new squashed.
1938 orig_parent: The commit that is an actual ancestor of `end_commit`. It
1939 is part of the same original tree as end_commit, which does not
1940 contain squashed commits. This is used to create the change
1941 description for the new squashed commit with:
1942 `git log orig_parent..end_commit`.
1943 end_commit: The commit to use as the end of the new squashed commit.
1944 """
Joanna Wangb88a4342023-01-24 01:28:22 +00001945
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001946 if end_commit is None:
1947 end_commit = RunGit(['rev-parse', self.branchref]).strip()
Joanna Wangb88a4342023-01-24 01:28:22 +00001948
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001949 reviewers, ccs, change_desc = self._PrepareChange(
1950 options, orig_parent, end_commit)
1951 latest_tree = RunGit(['rev-parse', end_commit + ':']).strip()
1952 with gclient_utils.temporary_file() as desc_tempfile:
1953 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
1954 commit_to_push = RunGit(
1955 ['commit-tree', latest_tree, '-p', parent, '-F',
1956 desc_tempfile]).strip()
Joanna Wangb88a4342023-01-24 01:28:22 +00001957
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001958 # Gerrit may or may not update fast enough to return the correct
1959 # patchset number after we push. Get the pre-upload patchset and
1960 # increment later.
1961 prev_patchset = self.GetMostRecentPatchset(update=False) or 0
1962 return _NewUpload(reviewers, ccs, commit_to_push, end_commit, parent,
1963 change_desc, prev_patchset)
Joanna Wangb88a4342023-01-24 01:28:22 +00001964
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001965 def PrepareCherryPickSquashedCommit(self, options: optparse.Values,
1966 parent: str) -> _NewUpload:
1967 """Create a commit cherry-picked on parent to push."""
Joanna Wange8523912023-01-21 02:05:40 +00001968
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001969 # The `parent` is what we will cherry-pick on top of.
1970 # The `cherry_pick_base` is the beginning range of what
1971 # we are cherry-picking.
1972 cherry_pick_base = self.GetCommonAncestorWithUpstream()
1973 reviewers, ccs, change_desc = self._PrepareChange(
1974 options, cherry_pick_base, self.branchref)
Joanna Wange8523912023-01-21 02:05:40 +00001975
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001976 new_upload_hash = RunGit(['rev-parse', self.branchref]).strip()
1977 latest_tree = RunGit(['rev-parse', self.branchref + ':']).strip()
1978 with gclient_utils.temporary_file() as desc_tempfile:
1979 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
1980 commit_to_cp = RunGit([
1981 'commit-tree', latest_tree, '-p', cherry_pick_base, '-F',
1982 desc_tempfile
1983 ]).strip()
Joanna Wange8523912023-01-21 02:05:40 +00001984
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001985 RunGit(['checkout', '-q', parent])
1986 ret, _out = RunGitWithCode(['cherry-pick', commit_to_cp])
1987 if ret:
1988 RunGit(['cherry-pick', '--abort'])
1989 RunGit(['checkout', '-q', self.branch])
1990 DieWithError('Could not cleanly cherry-pick')
Joanna Wange8523912023-01-21 02:05:40 +00001991
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001992 commit_to_push = RunGit(['rev-parse', 'HEAD']).strip()
1993 RunGit(['checkout', '-q', self.branch])
Joanna Wange8523912023-01-21 02:05:40 +00001994
Mike Frysinger124bb8e2023-09-06 05:48:55 +00001995 # Gerrit may or may not update fast enough to return the correct
1996 # patchset number after we push. Get the pre-upload patchset and
1997 # increment later.
1998 prev_patchset = self.GetMostRecentPatchset(update=False) or 0
1999 return _NewUpload(reviewers, ccs, commit_to_push, new_upload_hash,
2000 cherry_pick_base, change_desc, prev_patchset)
Joanna Wange8523912023-01-21 02:05:40 +00002001
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002002 def _PrepareChange(
2003 self, options: optparse.Values, parent: str, end_commit: str
2004 ) -> Tuple[Sequence[str], Sequence[str], ChangeDescription]:
2005 """Prepares the change to be uploaded."""
2006 self.EnsureCanUploadPatchset(options.force)
Joanna Wangb46232e2023-01-21 01:58:46 +00002007
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002008 files = self.GetAffectedFiles(parent, end_commit=end_commit)
2009 change_desc = self._GetDescriptionForUpload(options,
2010 [parent, end_commit], files)
Joanna Wangb46232e2023-01-21 01:58:46 +00002011
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002012 watchlist = watchlists.Watchlists(settings.GetRoot())
2013 self.ExtendCC(watchlist.GetWatchersForPaths(files))
2014 if not options.bypass_hooks:
2015 hook_results = self.RunHook(committing=False,
2016 may_prompt=not options.force,
2017 verbose=options.verbose,
2018 parallel=options.parallel,
2019 upstream=parent,
2020 description=change_desc.description,
2021 all_files=False)
2022 self.ExtendCC(hook_results['more_cc'])
Joanna Wangb46232e2023-01-21 01:58:46 +00002023
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002024 # Update the change description and ensure we have a Change Id.
2025 if self.GetIssue():
2026 if options.edit_description:
2027 change_desc.prompt()
2028 change_detail = self._GetChangeDetail(['CURRENT_REVISION'])
2029 change_id = change_detail['change_id']
2030 change_desc.ensure_change_id(change_id)
Joanna Wangb46232e2023-01-21 01:58:46 +00002031
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002032 else: # No change issue. First time uploading
2033 if not options.force and not options.message_file:
2034 change_desc.prompt()
Joanna Wangb46232e2023-01-21 01:58:46 +00002035
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002036 # Check if user added a change_id in the descripiton.
2037 change_ids = git_footers.get_footer_change_id(
2038 change_desc.description)
2039 if len(change_ids) == 1:
2040 change_id = change_ids[0]
2041 else:
2042 change_id = GenerateGerritChangeId(change_desc.description)
2043 change_desc.ensure_change_id(change_id)
Joanna Wangb46232e2023-01-21 01:58:46 +00002044
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002045 if options.preserve_tryjobs:
2046 change_desc.set_preserve_tryjobs()
Joanna Wangb46232e2023-01-21 01:58:46 +00002047
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002048 SaveDescriptionBackup(change_desc)
Joanna Wangb46232e2023-01-21 01:58:46 +00002049
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002050 # Add ccs
2051 ccs = []
2052 # Add default, watchlist, presubmit ccs if this is the initial upload
2053 # and CL is not private and auto-ccing has not been disabled.
2054 if not options.private and not options.no_autocc and not self.GetIssue(
2055 ):
2056 ccs = self.GetCCList().split(',')
2057 if len(ccs) > 100:
2058 lsc = (
2059 'https://chromium.googlesource.com/chromium/src/+/HEAD/docs/'
2060 'process/lsc/lsc_workflow.md')
2061 print('WARNING: This will auto-CC %s users.' % len(ccs))
2062 print('LSC may be more appropriate: %s' % lsc)
2063 print(
2064 'You can also use the --no-autocc flag to disable auto-CC.')
2065 confirm_or_exit(action='continue')
Joanna Wangb46232e2023-01-21 01:58:46 +00002066
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002067 # Add ccs from the --cc flag.
2068 if options.cc:
2069 ccs.extend(options.cc)
Joanna Wangb46232e2023-01-21 01:58:46 +00002070
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002071 ccs = [email.strip() for email in ccs if email.strip()]
2072 if change_desc.get_cced():
2073 ccs.extend(change_desc.get_cced())
Joanna Wangb46232e2023-01-21 01:58:46 +00002074
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002075 return change_desc.get_reviewers(), ccs, change_desc
Joanna Wangb46232e2023-01-21 01:58:46 +00002076
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002077 def PostUploadUpdates(self, options: optparse.Values,
2078 new_upload: _NewUpload, change_number: str) -> None:
2079 """Makes necessary post upload changes to the local and remote cl."""
2080 if not self.GetIssue():
2081 self.SetIssue(change_number)
Joanna Wang40497912023-01-24 21:18:16 +00002082
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002083 self.SetPatchset(new_upload.prev_patchset + 1)
Joanna Wang7603f042023-03-01 22:17:36 +00002084
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002085 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY,
2086 new_upload.commit_to_push)
2087 self._GitSetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY,
2088 new_upload.new_last_uploaded_commit)
Joanna Wang40497912023-01-24 21:18:16 +00002089
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002090 if settings.GetRunPostUploadHook():
2091 self.RunPostUploadHook(options.verbose, new_upload.parent,
2092 new_upload.change_desc.description)
Joanna Wang40497912023-01-24 21:18:16 +00002093
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002094 if new_upload.reviewers or new_upload.ccs:
2095 gerrit_util.AddReviewers(self.GetGerritHost(),
2096 self._GerritChangeIdentifier(),
2097 reviewers=new_upload.reviewers,
2098 ccs=new_upload.ccs,
2099 notify=bool(options.send_mail))
Joanna Wang40497912023-01-24 21:18:16 +00002100
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002101 def CMDUpload(self, options, git_diff_args, orig_args):
2102 """Uploads a change to codereview."""
2103 custom_cl_base = None
2104 if git_diff_args:
2105 custom_cl_base = base_branch = git_diff_args[0]
2106 else:
2107 if self.GetBranch() is None:
2108 DieWithError(
2109 'Can\'t upload from detached HEAD state. Get on a branch!')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002110
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002111 # Default to diffing against common ancestor of upstream branch
2112 base_branch = self.GetCommonAncestorWithUpstream()
2113 git_diff_args = [base_branch, 'HEAD']
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002114
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002115 # Fast best-effort checks to abort before running potentially expensive
2116 # hooks if uploading is likely to fail anyway. Passing these checks does
2117 # not guarantee that uploading will not fail.
2118 self.EnsureAuthenticated(force=options.force)
2119 self.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002120
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002121 print(f'Processing {_GetCommitCountSummary(*git_diff_args)}...')
Daniel Cheng66d0f152023-08-29 23:21:58 +00002122
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002123 # Apply watchlists on upload.
2124 watchlist = watchlists.Watchlists(settings.GetRoot())
2125 files = self.GetAffectedFiles(base_branch)
2126 if not options.bypass_watchlists:
2127 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002128
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002129 change_desc = self._GetDescriptionForUpload(options, git_diff_args,
2130 files)
2131 if not options.bypass_hooks:
2132 hook_results = self.RunHook(committing=False,
2133 may_prompt=not options.force,
2134 verbose=options.verbose,
2135 parallel=options.parallel,
2136 upstream=base_branch,
2137 description=change_desc.description,
2138 all_files=False)
2139 self.ExtendCC(hook_results['more_cc'])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002140
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002141 print_stats(git_diff_args)
2142 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base,
2143 change_desc)
2144 if not ret:
2145 if self.GetBranch() is not None:
2146 self._GitSetBranchConfigValue(
2147 LAST_UPLOAD_HASH_CONFIG_KEY,
2148 scm.GIT.ResolveCommit(settings.GetRoot(), 'HEAD'))
2149 # Run post upload hooks, if specified.
2150 if settings.GetRunPostUploadHook():
2151 self.RunPostUploadHook(options.verbose, base_branch,
2152 change_desc.description)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002153
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002154 # Upload all dependencies if specified.
2155 if options.dependencies:
2156 print()
2157 print('--dependencies has been specified.')
2158 print('All dependent local branches will be re-uploaded.')
2159 print()
2160 # Remove the dependencies flag from args so that we do not end
2161 # up in a loop.
2162 orig_args.remove('--dependencies')
2163 ret = upload_branch_deps(self, orig_args, options.force)
2164 return ret
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002165
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002166 def SetCQState(self, new_state):
2167 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002168
Struan Shrimpton8b2072b2023-07-31 21:01:26 +00002169 Issue must have been already uploaded and known.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002170 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002171 assert new_state in _CQState.ALL_STATES
2172 assert self.GetIssue()
2173 try:
2174 vote_map = {
2175 _CQState.NONE: 0,
2176 _CQState.DRY_RUN: 1,
2177 _CQState.COMMIT: 2,
2178 }
2179 labels = {'Commit-Queue': vote_map[new_state]}
2180 notify = False if new_state == _CQState.DRY_RUN else None
2181 gerrit_util.SetReview(self.GetGerritHost(),
2182 self._GerritChangeIdentifier(),
2183 labels=labels,
2184 notify=notify)
2185 return 0
2186 except KeyboardInterrupt:
2187 raise
2188 except:
2189 print(
2190 'WARNING: Failed to %s.\n'
2191 'Either:\n'
2192 ' * Your project has no CQ,\n'
2193 ' * You don\'t have permission to change the CQ state,\n'
2194 ' * There\'s a bug in this code (see stack trace below).\n'
2195 'Consider specifying which bots to trigger manually or asking your '
2196 'project owners for permissions or contacting Chrome Infra at:\n'
2197 'https://www.chromium.org/infra\n\n' %
2198 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
2199 # Still raise exception so that stack trace is printed.
2200 raise
qyearsley1fdfcb62016-10-24 13:22:03 -07002201
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002202 def GetGerritHost(self):
2203 # Lazy load of configs.
2204 self.GetCodereviewServer()
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002205
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002206 if self._gerrit_host and '.' not in self._gerrit_host:
2207 # Abbreviated domain like "chromium" instead of
2208 # chromium.googlesource.com.
2209 parsed = urllib.parse.urlparse(self.GetRemoteUrl())
2210 if parsed.scheme == 'sso':
2211 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2212 self._gerrit_server = 'https://%s' % self._gerrit_host
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002213
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002214 return self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002215
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002216 def _GetGitHost(self):
2217 """Returns git host to be used when uploading change to Gerrit."""
2218 remote_url = self.GetRemoteUrl()
2219 if not remote_url:
2220 return None
2221 return urllib.parse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002222
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002223 def GetCodereviewServer(self):
2224 if not self._gerrit_server:
2225 # If we're on a branch then get the server potentially associated
2226 # with that branch.
2227 if self.GetIssue() and self.GetBranch():
2228 self._gerrit_server = self._GitGetBranchConfigValue(
2229 CODEREVIEW_SERVER_CONFIG_KEY)
2230 if self._gerrit_server:
2231 self._gerrit_host = urllib.parse.urlparse(
2232 self._gerrit_server).netloc
2233 if not self._gerrit_server:
2234 url = urllib.parse.urlparse(self.GetRemoteUrl())
2235 parts = url.netloc.split('.')
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002236
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002237 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2238 # has "-review" suffix for lowest level subdomain.
2239 parts[0] = parts[0] + '-review'
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002240
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002241 if url.scheme == 'sso' and len(parts) == 1:
2242 # sso:// uses abbreivated hosts, eg. sso://chromium instead
2243 # of chromium.googlesource.com. Hence, for code review
2244 # server, they need to be expanded.
2245 parts[0] += '.googlesource.com'
Aravind Vasudevan0e3589e2023-05-25 23:00:30 +00002246
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002247 self._gerrit_host = '.'.join(parts)
2248 self._gerrit_server = 'https://%s' % self._gerrit_host
2249 return self._gerrit_server
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002250
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002251 def GetGerritProject(self):
2252 """Returns Gerrit project name based on remote git URL."""
2253 remote_url = self.GetRemoteUrl()
2254 if remote_url is None:
2255 logging.warning('can\'t detect Gerrit project.')
2256 return None
2257 project = urllib.parse.urlparse(remote_url).path.strip('/')
2258 if project.endswith('.git'):
2259 project = project[:-len('.git')]
2260 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start
2261 # with 'a/' prefix, because 'a/' prefix is used to force authentication
2262 # in gitiles/git-over-https protocol. E.g.,
2263 # https://chromium.googlesource.com/a/v8/v8 refers to the same
2264 # repo/project as https://chromium.googlesource.com/v8/v8
2265 if project.startswith('a/'):
2266 project = project[len('a/'):]
2267 return project
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002268
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002269 def _GerritChangeIdentifier(self):
2270 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002271
2272 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002273 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002274 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002275 project = self.GetGerritProject()
2276 if project:
2277 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
2278 # Fall back on still unique, but less efficient change number.
2279 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002280
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002281 def EnsureAuthenticated(self, force, refresh=None):
2282 """Best effort check that user is authenticated with Gerrit server."""
2283 if settings.GetGerritSkipEnsureAuthenticated():
2284 # For projects with unusual authentication schemes.
2285 # See http://crbug.com/603378.
2286 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002287
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002288 # Check presence of cookies only if using cookies-based auth method.
2289 cookie_auth = gerrit_util.Authenticator.get()
2290 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2291 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002292
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002293 remote_url = self.GetRemoteUrl()
2294 if remote_url is None:
2295 logging.warning('invalid remote')
2296 return
2297 if urllib.parse.urlparse(remote_url).scheme not in ['https', 'sso']:
2298 logging.warning(
2299 'Ignoring branch %(branch)s with non-https/sso remote '
2300 '%(remote)s', {
2301 'branch': self.branch,
2302 'remote': self.GetRemoteUrl()
2303 })
2304 return
Daniel Chengcf6269b2019-05-18 01:02:12 +00002305
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002306 # Lazy-loader to identify Gerrit and Git hosts.
2307 self.GetCodereviewServer()
2308 git_host = self._GetGitHost()
2309 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002310
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002311 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2312 git_auth = cookie_auth.get_auth_header(git_host)
2313 if gerrit_auth and git_auth:
2314 if gerrit_auth == git_auth:
2315 return
2316 all_gsrc = cookie_auth.get_auth_header(
2317 'd0esN0tEx1st.googlesource.com')
2318 print(
2319 'WARNING: You have different credentials for Gerrit and git hosts:\n'
2320 ' %s\n'
2321 ' %s\n'
2322 ' Consider running the following command:\n'
2323 ' git cl creds-check\n'
2324 ' %s\n'
2325 ' %s' %
2326 (git_host, self._gerrit_host,
2327 ('Hint: delete creds for .googlesource.com' if all_gsrc else
2328 ''), cookie_auth.get_new_password_message(git_host)))
2329 if not force:
2330 confirm_or_exit('If you know what you are doing',
2331 action='continue')
2332 return
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002333
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002334 missing = (([] if gerrit_auth else [self._gerrit_host]) +
2335 ([] if git_auth else [git_host]))
2336 DieWithError('Credentials for the following hosts are required:\n'
2337 ' %s\n'
2338 'These are read from %s (or legacy %s)\n'
2339 '%s' %
2340 ('\n '.join(missing), cookie_auth.get_gitcookies_path(),
2341 cookie_auth.get_netrc_path(),
2342 cookie_auth.get_new_password_message(git_host)))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002343
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002344 def EnsureCanUploadPatchset(self, force):
2345 if not self.GetIssue():
2346 return
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002347
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002348 status = self._GetChangeDetail()['status']
2349 if status == 'ABANDONED':
2350 DieWithError(
2351 'Change %s has been abandoned, new uploads are not allowed' %
2352 (self.GetIssueURL()))
2353 if status == 'MERGED':
2354 answer = gclient_utils.AskForData(
2355 'Change %s has been submitted, new uploads are not allowed. '
2356 'Would you like to start a new change (Y/n)?' %
2357 self.GetIssueURL()).lower()
2358 if answer not in ('y', ''):
2359 DieWithError('New uploads are not allowed.')
2360 self.SetIssue()
2361 return
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002362
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002363 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2364 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2365 # Apparently this check is not very important? Otherwise get_auth_email
2366 # could have been added to other implementations of Authenticator.
2367 cookies_auth = gerrit_util.Authenticator.get()
2368 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
2369 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002370
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002371 cookies_user = cookies_auth.get_auth_email(self.GetGerritHost())
2372 if self.GetIssueOwner() == cookies_user:
2373 return
2374 logging.debug('change %s owner is %s, cookies user is %s',
2375 self.GetIssue(), self.GetIssueOwner(), cookies_user)
2376 # Maybe user has linked accounts or something like that,
2377 # so ask what Gerrit thinks of this user.
2378 details = gerrit_util.GetAccountDetails(self.GetGerritHost(), 'self')
2379 if details['email'] == self.GetIssueOwner():
2380 return
2381 if not force:
2382 print(
2383 'WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
2384 'as %s.\n'
2385 'Uploading may fail due to lack of permissions.' %
2386 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2387 confirm_or_exit(action='upload')
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002388
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002389 def GetStatus(self):
2390 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002391 or CQ status, assuming adherence to a common workflow.
2392
2393 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002394 * 'error' - error from review tool (including deleted issues)
2395 * 'unsent' - no reviewers added
2396 * 'waiting' - waiting for review
2397 * 'reply' - waiting for uploader to reply to review
2398 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002399 * 'dry-run' - dry-running in the CQ
2400 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07002401 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002402 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002403 if not self.GetIssue():
2404 return None
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002405
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002406 try:
2407 data = self._GetChangeDetail(
2408 ['DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
2409 except GerritChangeNotExists:
2410 return 'error'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002411
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002412 if data['status'] in ('ABANDONED', 'MERGED'):
2413 return 'closed'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002414
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002415 cq_label = data['labels'].get('Commit-Queue', {})
2416 max_cq_vote = 0
2417 for vote in cq_label.get('all', []):
2418 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
2419 if max_cq_vote == 2:
2420 return 'commit'
2421 if max_cq_vote == 1:
2422 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002423
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002424 if data['labels'].get('Code-Review', {}).get('approved'):
2425 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002426
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002427 if not data.get('reviewers', {}).get('REVIEWER', []):
2428 return 'unsent'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002429
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002430 owner = data['owner'].get('_account_id')
2431 messages = sorted(data.get('messages', []), key=lambda m: m.get('date'))
2432 while messages:
2433 m = messages.pop()
2434 if (m.get('tag', '').startswith('autogenerated:cq')
2435 or m.get('tag', '').startswith('autogenerated:cv')):
2436 # Ignore replies from LUCI CV/CQ.
2437 continue
2438 if m.get('author', {}).get('_account_id') == owner:
2439 # Most recent message was by owner.
2440 return 'waiting'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00002441
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002442 # Some reply from non-owner.
2443 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002444
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002445 # Somehow there are no messages even though there are reviewers.
2446 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002447
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002448 def GetMostRecentPatchset(self, update=True):
2449 if not self.GetIssue():
2450 return None
Edward Lemur6c6827c2020-02-06 21:15:18 +00002451
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002452 data = self._GetChangeDetail(['CURRENT_REVISION'])
2453 patchset = data['revisions'][data['current_revision']]['_number']
2454 if update:
2455 self.SetPatchset(patchset)
2456 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002457
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002458 def _IsPatchsetRangeSignificant(self, lower, upper):
2459 """Returns True if the inclusive range of patchsets contains any reworks or
Gavin Makf35a9eb2022-11-17 18:34:36 +00002460 rebases."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002461 if not self.GetIssue():
2462 return False
Gavin Makf35a9eb2022-11-17 18:34:36 +00002463
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002464 data = self._GetChangeDetail(['ALL_REVISIONS'])
2465 ps_kind = {}
2466 for rev_info in data.get('revisions', {}).values():
2467 ps_kind[rev_info['_number']] = rev_info.get('kind', '')
Gavin Makf35a9eb2022-11-17 18:34:36 +00002468
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002469 for ps in range(lower, upper + 1):
2470 assert ps in ps_kind, 'expected patchset %d in change detail' % ps
2471 if ps_kind[ps] not in ('NO_CHANGE', 'NO_CODE_CHANGE'):
2472 return True
2473 return False
Gavin Makf35a9eb2022-11-17 18:34:36 +00002474
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002475 def GetMostRecentDryRunPatchset(self):
2476 """Get patchsets equivalent to the most recent patchset and return
Gavin Make61ccc52020-11-13 00:12:57 +00002477 the patchset with the latest dry run. If none have been dry run, return
2478 the latest patchset."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002479 if not self.GetIssue():
2480 return None
Gavin Make61ccc52020-11-13 00:12:57 +00002481
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002482 data = self._GetChangeDetail(['ALL_REVISIONS'])
2483 patchset = data['revisions'][data['current_revision']]['_number']
2484 dry_run = {
2485 int(m['_revision_number'])
2486 for m in data.get('messages', [])
2487 if m.get('tag', '').endswith('dry-run')
2488 }
Gavin Make61ccc52020-11-13 00:12:57 +00002489
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002490 for revision_info in sorted(data.get('revisions', {}).values(),
2491 key=lambda c: c['_number'],
2492 reverse=True):
2493 if revision_info['_number'] in dry_run:
2494 patchset = revision_info['_number']
2495 break
2496 if revision_info.get('kind', '') not in \
2497 ('NO_CHANGE', 'NO_CODE_CHANGE', 'TRIVIAL_REBASE'):
2498 break
2499 self.SetPatchset(patchset)
2500 return patchset
Gavin Make61ccc52020-11-13 00:12:57 +00002501
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002502 def AddComment(self, message, publish=None):
2503 gerrit_util.SetReview(self.GetGerritHost(),
2504 self._GerritChangeIdentifier(),
2505 msg=message,
2506 ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002507
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002508 def GetCommentsSummary(self, readable=True):
2509 # DETAILED_ACCOUNTS is to get emails in accounts.
2510 # CURRENT_REVISION is included to get the latest patchset so that
2511 # only the robot comments from the latest patchset can be shown.
2512 messages = self._GetChangeDetail(
2513 options=['MESSAGES', 'DETAILED_ACCOUNTS', 'CURRENT_REVISION']).get(
2514 'messages', [])
2515 file_comments = gerrit_util.GetChangeComments(
2516 self.GetGerritHost(), self._GerritChangeIdentifier())
2517 robot_file_comments = gerrit_util.GetChangeRobotComments(
2518 self.GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002519
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002520 # Add the robot comments onto the list of comments, but only
2521 # keep those that are from the latest patchset.
2522 latest_patch_set = self.GetMostRecentPatchset()
2523 for path, robot_comments in robot_file_comments.items():
2524 line_comments = file_comments.setdefault(path, [])
2525 line_comments.extend([
2526 c for c in robot_comments if c['patch_set'] == latest_patch_set
2527 ])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002528
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002529 # Build dictionary of file comments for easy access and sorting later.
2530 # {author+date: {path: {patchset: {line: url+message}}}}
2531 comments = collections.defaultdict(lambda: collections.defaultdict(
2532 lambda: collections.defaultdict(dict)))
Andrii Shyshkalova3762a92020-11-25 10:20:42 +00002533
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002534 server = self.GetCodereviewServer()
2535 if server in _KNOWN_GERRIT_TO_SHORT_URLS:
2536 # /c/ is automatically added by short URL server.
2537 url_prefix = '%s/%s' % (_KNOWN_GERRIT_TO_SHORT_URLS[server],
2538 self.GetIssue())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002539 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002540 url_prefix = '%s/c/%s' % (server, self.GetIssue())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002541
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002542 for path, line_comments in file_comments.items():
2543 for comment in line_comments:
2544 tag = comment.get('tag', '')
2545 if tag.startswith(
2546 'autogenerated') and 'robot_id' not in comment:
2547 continue
2548 key = (comment['author']['email'], comment['updated'])
2549 if comment.get('side', 'REVISION') == 'PARENT':
2550 patchset = 'Base'
2551 else:
2552 patchset = 'PS%d' % comment['patch_set']
2553 line = comment.get('line', 0)
2554 url = ('%s/%s/%s#%s%s' %
2555 (url_prefix, comment['patch_set'],
2556 path, 'b' if comment.get('side') == 'PARENT' else '',
2557 str(line) if line else ''))
2558 comments[key][path][patchset][line] = (url, comment['message'])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002559
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002560 summaries = []
2561 for msg in messages:
2562 summary = self._BuildCommentSummary(msg, comments, readable)
2563 if summary:
2564 summaries.append(summary)
2565 return summaries
Josip Sokcevic266129c2021-11-09 00:22:00 +00002566
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002567 @staticmethod
2568 def _BuildCommentSummary(msg, comments, readable):
2569 if 'email' not in msg['author']:
2570 # Some bot accounts may not have an email associated.
2571 return None
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002572
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002573 key = (msg['author']['email'], msg['date'])
2574 # Don't bother showing autogenerated messages that don't have associated
2575 # file or line comments. this will filter out most autogenerated
2576 # messages, but will keep robot comments like those from Tricium.
2577 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
2578 if is_autogenerated and not comments.get(key):
2579 return None
2580 message = msg['message']
2581 # Gerrit spits out nanoseconds.
2582 assert len(msg['date'].split('.')[-1]) == 9
2583 date = datetime.datetime.strptime(msg['date'][:-3],
2584 '%Y-%m-%d %H:%M:%S.%f')
2585 if key in comments:
2586 message += '\n'
2587 for path, patchsets in sorted(comments.get(key, {}).items()):
2588 if readable:
2589 message += '\n%s' % path
2590 for patchset, lines in sorted(patchsets.items()):
2591 for line, (url, content) in sorted(lines.items()):
2592 if line:
2593 line_str = 'Line %d' % line
2594 path_str = '%s:%d:' % (path, line)
2595 else:
2596 line_str = 'File comment'
2597 path_str = '%s:0:' % path
2598 if readable:
2599 message += '\n %s, %s: %s' % (patchset, line_str, url)
2600 message += '\n %s\n' % content
2601 else:
2602 message += '\n%s ' % path_str
2603 message += '\n%s\n' % content
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002604
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002605 return _CommentSummary(
2606 date=date,
2607 message=message,
2608 sender=msg['author']['email'],
2609 autogenerated=is_autogenerated,
2610 # These could be inferred from the text messages and correlated with
2611 # Code-Review label maximum, however this is not reliable.
2612 # Leaving as is until the need arises.
2613 approval=False,
2614 disapproval=False,
2615 )
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002616
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002617 def CloseIssue(self):
2618 gerrit_util.AbandonChange(self.GetGerritHost(),
2619 self._GerritChangeIdentifier(),
2620 msg='')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002621
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002622 def SubmitIssue(self):
2623 gerrit_util.SubmitChange(self.GetGerritHost(),
2624 self._GerritChangeIdentifier())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002625
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002626 def _GetChangeDetail(self, options=None):
2627 """Returns details of associated Gerrit change and caching results."""
2628 options = options or []
2629 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002630
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002631 # Optimization to avoid multiple RPCs:
2632 if 'CURRENT_REVISION' in options or 'ALL_REVISIONS' in options:
2633 options.append('CURRENT_COMMIT')
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002634
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002635 # Normalize issue and options for consistent keys in cache.
2636 cache_key = str(self.GetIssue())
2637 options_set = frozenset(o.upper() for o in options)
2638
2639 for cached_options_set, data in self._detail_cache.get(cache_key, []):
2640 # Assumption: data fetched before with extra options is suitable
2641 # for return for a smaller set of options.
2642 # For example, if we cached data for
2643 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2644 # and request is for options=[CURRENT_REVISION],
2645 # THEN we can return prior cached data.
2646 if options_set.issubset(cached_options_set):
2647 return data
2648
2649 try:
2650 data = gerrit_util.GetChangeDetail(self.GetGerritHost(),
2651 self._GerritChangeIdentifier(),
2652 options_set)
2653 except gerrit_util.GerritError as e:
2654 if e.http_status == 404:
2655 raise GerritChangeNotExists(self.GetIssue(),
2656 self.GetCodereviewServer())
2657 raise
2658
2659 self._detail_cache.setdefault(cache_key, []).append((options_set, data))
Edward Lesmes7677e5c2020-02-19 20:39:03 +00002660 return data
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002661
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002662 def _GetChangeCommit(self, revision='current'):
2663 assert self.GetIssue(), 'issue must be set to query Gerrit'
2664 try:
2665 data = gerrit_util.GetChangeCommit(self.GetGerritHost(),
2666 self._GerritChangeIdentifier(),
2667 revision)
2668 except gerrit_util.GerritError as e:
2669 if e.http_status == 404:
2670 raise GerritChangeNotExists(self.GetIssue(),
2671 self.GetCodereviewServer())
2672 raise
2673 return data
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002674
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002675 def _IsCqConfigured(self):
2676 detail = self._GetChangeDetail(['LABELS'])
2677 return u'Commit-Queue' in detail.get('labels', {})
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002678
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002679 def CMDLand(self, force, bypass_hooks, verbose, parallel, resultdb, realm):
2680 if git_common.is_dirty_git_tree('land'):
2681 return 1
agable32978d92016-11-01 12:55:02 -07002682
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002683 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2684 if not force and self._IsCqConfigured():
2685 confirm_or_exit(
2686 '\nIt seems this repository has a CQ, '
2687 'which can test and land changes for you. '
2688 'Are you sure you wish to bypass it?\n',
2689 action='bypass CQ')
2690 differs = True
2691 last_upload = self._GitGetBranchConfigValue(
Gavin Mak4e5e3992022-11-14 22:40:12 +00002692 GERRIT_SQUASH_HASH_CONFIG_KEY)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002693 # Note: git diff outputs nothing if there is no diff.
2694 if not last_upload or RunGit(['diff', last_upload]).strip():
2695 print(
2696 'WARNING: Some changes from local branch haven\'t been uploaded.'
2697 )
Edward Lemur5a644f82020-03-18 16:44:57 +00002698 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002699 if detail['current_revision'] == last_upload:
2700 differs = False
2701 else:
2702 print(
2703 'WARNING: Local branch contents differ from latest uploaded '
2704 'patchset.')
2705 if differs:
2706 if not force:
2707 confirm_or_exit(
2708 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2709 action='submit')
2710 print(
2711 'WARNING: Bypassing hooks and submitting latest uploaded patchset.'
2712 )
2713 elif not bypass_hooks:
2714 upstream = self.GetCommonAncestorWithUpstream()
2715 if self.GetIssue():
2716 description = self.FetchDescription()
2717 else:
2718 description = _create_description_from_log([upstream])
2719 self.RunHook(committing=True,
2720 may_prompt=not force,
2721 verbose=verbose,
2722 parallel=parallel,
2723 upstream=upstream,
2724 description=description,
2725 all_files=False,
2726 resultdb=resultdb,
2727 realm=realm)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002728
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002729 self.SubmitIssue()
2730 print('Issue %s has been submitted.' % self.GetIssueURL())
2731 links = self._GetChangeCommit().get('web_links', [])
2732 for link in links:
2733 if link.get('name') in ['gitiles', 'browse'] and link.get('url'):
2734 print('Landed as: %s' % link.get('url'))
2735 break
2736 return 0
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002737
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002738 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force,
2739 newbranch):
2740 assert parsed_issue_arg.valid
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002741
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002742 self.issue = parsed_issue_arg.issue
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002743
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002744 if parsed_issue_arg.hostname:
2745 self._gerrit_host = parsed_issue_arg.hostname
2746 self._gerrit_server = 'https://%s' % self._gerrit_host
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002747
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002748 try:
2749 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2750 except GerritChangeNotExists as e:
2751 DieWithError(str(e))
agablec6787972016-09-09 16:13:34 -07002752
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002753 if not parsed_issue_arg.patchset:
2754 # Use current revision by default.
2755 revision_info = detail['revisions'][detail['current_revision']]
2756 patchset = int(revision_info['_number'])
2757 else:
2758 patchset = parsed_issue_arg.patchset
2759 for revision_info in detail['revisions'].values():
2760 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2761 break
2762 else:
2763 DieWithError('Couldn\'t find patchset %i in change %i' %
2764 (parsed_issue_arg.patchset, self.GetIssue()))
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002765
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002766 remote_url = self.GetRemoteUrl()
2767 if remote_url.endswith('.git'):
2768 remote_url = remote_url[:-len('.git')]
2769 remote_url = remote_url.rstrip('/')
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002770
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002771 fetch_info = revision_info['fetch']['http']
2772 fetch_info['url'] = fetch_info['url'].rstrip('/')
Josip Sokcevicf2cfd3d2021-03-30 18:39:18 +00002773
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002774 if remote_url != fetch_info['url']:
2775 DieWithError(
2776 'Trying to patch a change from %s but this repo appears '
2777 'to be %s.' % (fetch_info['url'], remote_url))
Gavin Mak4e5e3992022-11-14 22:40:12 +00002778
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002779 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002780
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002781 # Set issue immediately in case the cherry-pick fails, which happens
2782 # when resolving conflicts.
2783 if self.GetBranch():
2784 self.SetIssue(parsed_issue_arg.issue)
tandrii88189772016-09-29 04:29:57 -07002785
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002786 if force:
2787 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2788 print('Checked out commit for change %i patchset %i locally' %
2789 (parsed_issue_arg.issue, patchset))
2790 elif nocommit:
2791 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2792 print('Patch applied to index.')
2793 else:
2794 RunGit(['cherry-pick', 'FETCH_HEAD'])
2795 print('Committed patch for change %i patchset %i locally.' %
2796 (parsed_issue_arg.issue, patchset))
2797 print(
2798 'Note: this created a local commit which does not have '
2799 'the same hash as the one uploaded for review. This will make '
2800 'uploading changes based on top of this branch difficult.\n'
2801 'If you want to do that, use "git cl patch --force" instead.')
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002802
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002803 if self.GetBranch():
2804 self.SetPatchset(patchset)
2805 fetched_hash = scm.GIT.ResolveCommit(settings.GetRoot(),
2806 'FETCH_HEAD')
2807 self._GitSetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY,
2808 fetched_hash)
2809 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY,
2810 fetched_hash)
2811 else:
2812 print(
2813 'WARNING: You are in detached HEAD state.\n'
2814 'The patch has been applied to your checkout, but you will not be '
2815 'able to upload a new patch set to the gerrit issue.\n'
2816 'Try using the \'-b\' option if you would like to work on a '
2817 'branch and/or upload a new patch set.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002818
Mike Frysinger124bb8e2023-09-06 05:48:55 +00002819 return 0
2820
2821 @staticmethod
2822 def _GerritCommitMsgHookCheck(offer_removal):
2823 # type: (bool) -> None
2824 """Checks for the gerrit's commit-msg hook and removes it if necessary."""
2825 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2826 if not os.path.exists(hook):
2827 return
2828 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
2829 # custom developer-made one.
2830 data = gclient_utils.FileRead(hook)
2831 if not ('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2832 return
2833 print('WARNING: You have Gerrit commit-msg hook installed.\n'
2834 'It is not necessary for uploading with git cl in squash mode, '
2835 'and may interfere with it in subtle ways.\n'
2836 'We recommend you remove the commit-msg hook.')
2837 if offer_removal:
2838 if ask_for_explicit_yes('Do you want to remove it now?'):
2839 gclient_utils.rm_file_or_tree(hook)
2840 print('Gerrit commit-msg hook removed.')
2841 else:
2842 print('OK, will keep Gerrit commit-msg hook in place.')
2843
2844 def _CleanUpOldTraces(self):
2845 """Keep only the last |MAX_TRACES| traces."""
2846 try:
2847 traces = sorted([
2848 os.path.join(TRACES_DIR, f) for f in os.listdir(TRACES_DIR)
2849 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2850 and not f.startswith('tmp'))
2851 ])
2852 traces_to_delete = traces[:-MAX_TRACES]
2853 for trace in traces_to_delete:
2854 os.remove(trace)
2855 except OSError:
2856 print('WARNING: Failed to remove old git traces from\n'
2857 ' %s'
2858 'Consider removing them manually.' % TRACES_DIR)
2859
2860 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
2861 """Zip and write the git push traces stored in traces_dir."""
2862 gclient_utils.safe_makedirs(TRACES_DIR)
2863 traces_zip = trace_name + '-traces'
2864 traces_readme = trace_name + '-README'
2865 # Create a temporary dir to store git config and gitcookies in. It will
2866 # be compressed and stored next to the traces.
2867 git_info_dir = tempfile.mkdtemp()
2868 git_info_zip = trace_name + '-git-info'
2869
2870 git_push_metadata['now'] = datetime_now().strftime(
2871 '%Y-%m-%dT%H:%M:%S.%f')
2872
2873 git_push_metadata['trace_name'] = trace_name
2874 gclient_utils.FileWrite(traces_readme,
2875 TRACES_README_FORMAT % git_push_metadata)
2876
2877 # Keep only the first 6 characters of the git hashes on the packet
2878 # trace. This greatly decreases size after compression.
2879 packet_traces = os.path.join(traces_dir, 'trace-packet')
2880 if os.path.isfile(packet_traces):
2881 contents = gclient_utils.FileRead(packet_traces)
2882 gclient_utils.FileWrite(packet_traces,
2883 GIT_HASH_RE.sub(r'\1', contents))
2884 shutil.make_archive(traces_zip, 'zip', traces_dir)
2885
2886 # Collect and compress the git config and gitcookies.
2887 git_config = RunGit(['config', '-l'])
2888 gclient_utils.FileWrite(os.path.join(git_info_dir, 'git-config'),
2889 git_config)
2890
2891 cookie_auth = gerrit_util.Authenticator.get()
2892 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2893 gitcookies_path = cookie_auth.get_gitcookies_path()
2894 if os.path.isfile(gitcookies_path):
2895 gitcookies = gclient_utils.FileRead(gitcookies_path)
2896 gclient_utils.FileWrite(
2897 os.path.join(git_info_dir, 'gitcookies'),
2898 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2899 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2900
2901 gclient_utils.rmtree(git_info_dir)
2902
2903 def _RunGitPushWithTraces(self,
2904 refspec,
2905 refspec_opts,
2906 git_push_metadata,
2907 git_push_options=None):
2908 """Run git push and collect the traces resulting from the execution."""
2909 # Create a temporary directory to store traces in. Traces will be
2910 # compressed and stored in a 'traces' dir inside depot_tools.
2911 traces_dir = tempfile.mkdtemp()
2912 trace_name = os.path.join(TRACES_DIR,
2913 datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
2914
2915 env = os.environ.copy()
2916 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2917 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
2918 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
2919 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2920 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2921 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2922
2923 push_returncode = 0
2924 before_push = time_time()
2925 try:
2926 remote_url = self.GetRemoteUrl()
2927 push_cmd = ['git', 'push', remote_url, refspec]
2928 if git_push_options:
2929 for opt in git_push_options:
2930 push_cmd.extend(['-o', opt])
2931
2932 push_stdout = gclient_utils.CheckCallAndFilter(
2933 push_cmd,
2934 env=env,
2935 print_stdout=True,
2936 # Flush after every line: useful for seeing progress when
2937 # running as recipe.
2938 filter_fn=lambda _: sys.stdout.flush())
2939 push_stdout = push_stdout.decode('utf-8', 'replace')
2940 except subprocess2.CalledProcessError as e:
2941 push_returncode = e.returncode
2942 if 'blocked keyword' in str(e.stdout) or 'banned word' in str(
2943 e.stdout):
2944 raise GitPushError(
2945 'Failed to create a change, very likely due to blocked keyword. '
2946 'Please examine output above for the reason of the failure.\n'
2947 'If this is a false positive, you can try to bypass blocked '
2948 'keyword by using push option '
2949 '-o banned-words~skip, e.g.:\n'
2950 'git cl upload -o banned-words~skip\n\n'
2951 'If git-cl is not working correctly, file a bug under the '
2952 'Infra>SDK component.')
2953 if 'git push -o nokeycheck' in str(e.stdout):
2954 raise GitPushError(
2955 'Failed to create a change, very likely due to a private key being '
2956 'detected. Please examine output above for the reason of the '
2957 'failure.\n'
2958 'If this is a false positive, you can try to bypass private key '
2959 'detection by using push option '
2960 '-o nokeycheck, e.g.:\n'
2961 'git cl upload -o nokeycheck\n\n'
2962 'If git-cl is not working correctly, file a bug under the '
2963 'Infra>SDK component.')
2964
2965 raise GitPushError(
2966 'Failed to create a change. Please examine output above for the '
2967 'reason of the failure.\n'
2968 'For emergencies, Googlers can escalate to '
2969 'go/gob-support or go/notify#gob\n'
2970 'Hint: run command below to diagnose common Git/Gerrit '
2971 'credential problems:\n'
2972 ' git cl creds-check\n'
2973 '\n'
2974 'If git-cl is not working correctly, file a bug under the Infra>SDK '
2975 'component including the files below.\n'
2976 'Review the files before upload, since they might contain sensitive '
2977 'information.\n'
2978 'Set the Restrict-View-Google label so that they are not publicly '
2979 'accessible.\n' + TRACES_MESSAGE % {'trace_name': trace_name})
2980 finally:
2981 execution_time = time_time() - before_push
2982 metrics.collector.add_repeated(
2983 'sub_commands', {
2984 'command':
2985 'git push',
2986 'execution_time':
2987 execution_time,
2988 'exit_code':
2989 push_returncode,
2990 'arguments':
2991 metrics_utils.extract_known_subcommand_args(refspec_opts),
2992 })
2993
2994 git_push_metadata['execution_time'] = execution_time
2995 git_push_metadata['exit_code'] = push_returncode
2996 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
2997
2998 self._CleanUpOldTraces()
2999 gclient_utils.rmtree(traces_dir)
3000
3001 return push_stdout
3002
3003 def CMDUploadChange(self, options, git_diff_args, custom_cl_base,
3004 change_desc):
3005 """Upload the current branch to Gerrit, retry if new remote HEAD is
3006 found. options and change_desc may be mutated."""
3007 remote, remote_branch = self.GetRemoteBranch()
3008 branch = GetTargetRef(remote, remote_branch, options.target_branch)
3009
3010 try:
3011 return self._CMDUploadChange(options, git_diff_args, custom_cl_base,
3012 change_desc, branch)
3013 except GitPushError as e:
3014 # Repository might be in the middle of transition to main branch as
3015 # default, and uploads to old default might be blocked.
3016 if remote_branch not in [DEFAULT_OLD_BRANCH, DEFAULT_NEW_BRANCH]:
3017 DieWithError(str(e), change_desc)
3018
3019 project_head = gerrit_util.GetProjectHead(self._gerrit_host,
3020 self.GetGerritProject())
3021 if project_head == branch:
3022 DieWithError(str(e), change_desc)
3023 branch = project_head
3024
3025 print("WARNING: Fetching remote state and retrying upload to default "
3026 "branch...")
3027 RunGit(['fetch', '--prune', remote])
3028 options.edit_description = False
3029 options.force = True
3030 try:
3031 self._CMDUploadChange(options, git_diff_args, custom_cl_base,
3032 change_desc, branch)
3033 except GitPushError as e:
3034 DieWithError(str(e), change_desc)
3035
3036 def _CMDUploadChange(self, options, git_diff_args, custom_cl_base,
3037 change_desc, branch):
3038 """Upload the current branch to Gerrit."""
3039 if options.squash:
3040 Changelist._GerritCommitMsgHookCheck(
3041 offer_removal=not options.force)
3042 external_parent = None
3043 if self.GetIssue():
3044 # User requested to change description
3045 if options.edit_description:
3046 change_desc.prompt()
3047 change_detail = self._GetChangeDetail(['CURRENT_REVISION'])
3048 change_id = change_detail['change_id']
3049 change_desc.ensure_change_id(change_id)
3050
3051 # Check if changes outside of this workspace have been uploaded.
3052 current_rev = change_detail['current_revision']
3053 last_uploaded_rev = self._GitGetBranchConfigValue(
3054 GERRIT_SQUASH_HASH_CONFIG_KEY)
3055 if last_uploaded_rev and current_rev != last_uploaded_rev:
3056 external_parent = self._UpdateWithExternalChanges()
3057 else: # if not self.GetIssue()
3058 if not options.force and not options.message_file:
3059 change_desc.prompt()
3060 change_ids = git_footers.get_footer_change_id(
3061 change_desc.description)
3062 if len(change_ids) == 1:
3063 change_id = change_ids[0]
3064 else:
3065 change_id = GenerateGerritChangeId(change_desc.description)
3066 change_desc.ensure_change_id(change_id)
3067
3068 if options.preserve_tryjobs:
3069 change_desc.set_preserve_tryjobs()
3070
3071 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
3072 parent = external_parent or self._ComputeParent(
3073 remote, upstream_branch, custom_cl_base, options.force,
3074 change_desc)
3075 tree = RunGit(['rev-parse', 'HEAD:']).strip()
3076 with gclient_utils.temporary_file() as desc_tempfile:
3077 gclient_utils.FileWrite(desc_tempfile, change_desc.description)
3078 ref_to_push = RunGit(
3079 ['commit-tree', tree, '-p', parent, '-F',
3080 desc_tempfile]).strip()
3081 else: # if not options.squash
3082 if options.no_add_changeid:
3083 pass
3084 else: # adding Change-Ids is okay.
3085 if not git_footers.get_footer_change_id(
3086 change_desc.description):
3087 DownloadGerritHook(False)
3088 change_desc.set_description(
3089 self._AddChangeIdToCommitMessage(
3090 change_desc.description, git_diff_args))
3091 ref_to_push = 'HEAD'
3092 # For no-squash mode, we assume the remote called "origin" is the
3093 # one we want. It is not worthwhile to support different workflows
3094 # for no-squash mode.
3095 parent = 'origin/%s' % branch
3096 # attempt to extract the changeid from the current description
3097 # fail informatively if not possible.
3098 change_id_candidates = git_footers.get_footer_change_id(
3099 change_desc.description)
3100 if not change_id_candidates:
3101 DieWithError("Unable to extract change-id from message.")
3102 change_id = change_id_candidates[0]
3103
3104 SaveDescriptionBackup(change_desc)
3105 commits = RunGitSilent(['rev-list',
3106 '%s..%s' % (parent, ref_to_push)]).splitlines()
3107 if len(commits) > 1:
3108 print(
3109 'WARNING: This will upload %d commits. Run the following command '
3110 'to see which commits will be uploaded: ' % len(commits))
3111 print('git log %s..%s' % (parent, ref_to_push))
3112 print('You can also use `git squash-branch` to squash these into a '
3113 'single commit.')
3114 confirm_or_exit(action='upload')
3115
3116 reviewers = sorted(change_desc.get_reviewers())
3117 cc = []
3118 # Add default, watchlist, presubmit ccs if this is the initial upload
3119 # and CL is not private and auto-ccing has not been disabled.
3120 if not options.private and not options.no_autocc and not self.GetIssue(
3121 ):
3122 cc = self.GetCCList().split(',')
3123 if len(cc) > 100:
3124 lsc = ('https://chromium.googlesource.com/chromium/src/+/HEAD/docs/'
3125 'process/lsc/lsc_workflow.md')
3126 print('WARNING: This will auto-CC %s users.' % len(cc))
3127 print('LSC may be more appropriate: %s' % lsc)
3128 print('You can also use the --no-autocc flag to disable auto-CC.')
3129 confirm_or_exit(action='continue')
3130 # Add cc's from the --cc flag.
3131 if options.cc:
3132 cc.extend(options.cc)
3133 cc = [email.strip() for email in cc if email.strip()]
3134 if change_desc.get_cced():
3135 cc.extend(change_desc.get_cced())
3136 if self.GetGerritHost() == 'chromium-review.googlesource.com':
3137 valid_accounts = set(reviewers + cc)
3138 # TODO(crbug/877717): relax this for all hosts.
3139 else:
3140 valid_accounts = gerrit_util.ValidAccounts(self.GetGerritHost(),
3141 reviewers + cc)
3142 logging.info('accounts %s are recognized, %s invalid',
3143 sorted(valid_accounts),
3144 set(reviewers + cc).difference(set(valid_accounts)))
3145
3146 # Extra options that can be specified at push time. Doc:
3147 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
3148 refspec_opts = self._GetRefSpecOptions(options, change_desc)
3149
3150 for r in sorted(reviewers):
3151 if r in valid_accounts:
3152 refspec_opts.append('r=%s' % r)
3153 reviewers.remove(r)
3154 else:
3155 # TODO(tandrii): this should probably be a hard failure.
3156 print(
3157 'WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
3158 % r)
3159 for c in sorted(cc):
3160 # refspec option will be rejected if cc doesn't correspond to an
3161 # account, even though REST call to add such arbitrary cc may
3162 # succeed.
3163 if c in valid_accounts:
3164 refspec_opts.append('cc=%s' % c)
3165 cc.remove(c)
3166
3167 refspec_suffix = ''
3168 if refspec_opts:
3169 refspec_suffix = '%' + ','.join(refspec_opts)
3170 assert ' ' not in refspec_suffix, (
3171 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3172 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3173
3174 git_push_metadata = {
3175 'gerrit_host': self.GetGerritHost(),
3176 'title': options.title or '<untitled>',
3177 'change_id': change_id,
3178 'description': change_desc.description,
3179 }
3180
3181 # Gerrit may or may not update fast enough to return the correct
3182 # patchset number after we push. Get the pre-upload patchset and
3183 # increment later.
3184 latest_ps = self.GetMostRecentPatchset(update=False) or 0
3185
3186 push_stdout = self._RunGitPushWithTraces(refspec, refspec_opts,
3187 git_push_metadata,
3188 options.push_options)
3189
3190 if options.squash:
3191 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
3192 change_numbers = [
3193 m.group(1) for m in map(regex.match, push_stdout.splitlines())
3194 if m
3195 ]
3196 if len(change_numbers) != 1:
3197 DieWithError((
3198 'Created|Updated %d issues on Gerrit, but only 1 expected.\n'
3199 'Change-Id: %s') % (len(change_numbers), change_id),
3200 change_desc)
3201 self.SetIssue(change_numbers[0])
3202 self.SetPatchset(latest_ps + 1)
3203 self._GitSetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY,
3204 ref_to_push)
3205
3206 if self.GetIssue() and (reviewers or cc):
3207 # GetIssue() is not set in case of non-squash uploads according to
3208 # tests. TODO(crbug.com/751901): non-squash uploads in git cl should
3209 # be removed.
3210 gerrit_util.AddReviewers(self.GetGerritHost(),
3211 self._GerritChangeIdentifier(),
3212 reviewers,
3213 cc,
3214 notify=bool(options.send_mail))
3215
3216 return 0
3217
3218 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3219 change_desc):
3220 """Computes parent of the generated commit to be uploaded to Gerrit.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003221
3222 Returns revision or a ref name.
3223 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003224 if custom_cl_base:
3225 # Try to avoid creating additional unintended CLs when uploading,
3226 # unless user wants to take this risk.
3227 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3228 code, _ = RunGitWithCode([
3229 'merge-base', '--is-ancestor', custom_cl_base,
3230 local_ref_of_target_remote
3231 ])
3232 if code == 1:
3233 print(
3234 '\nWARNING: Manually specified base of this CL `%s` '
3235 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3236 'If you proceed with upload, more than 1 CL may be created by '
3237 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3238 'If you are certain that specified base `%s` has already been '
3239 'uploaded to Gerrit as another CL, you may proceed.\n' %
3240 (custom_cl_base, local_ref_of_target_remote,
3241 custom_cl_base))
3242 if not force:
3243 confirm_or_exit(
3244 'Do you take responsibility for cleaning up potential mess '
3245 'resulting from proceeding with upload?',
3246 action='upload')
3247 return custom_cl_base
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003248
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003249 if remote != '.':
3250 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003251
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003252 # If our upstream branch is local, we base our squashed commit on its
3253 # squashed version.
3254 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
Aaron Gablef97e33d2017-03-30 15:44:27 -07003255
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003256 if upstream_branch_name == 'master':
3257 return self.GetCommonAncestorWithUpstream()
3258 if upstream_branch_name == 'main':
3259 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003260
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003261 # Check the squashed hash of the parent.
3262 # TODO(tandrii): consider checking parent change in Gerrit and using its
3263 # hash if tree hash of latest parent revision (patchset) in Gerrit
3264 # matches the tree hash of the parent branch. The upside is less likely
3265 # bogus requests to reupload parent change just because it's uploadhash
3266 # is missing, yet the downside likely exists, too (albeit unknown to me
3267 # yet).
3268 parent = scm.GIT.GetBranchConfig(settings.GetRoot(),
3269 upstream_branch_name,
3270 GERRIT_SQUASH_HASH_CONFIG_KEY)
3271 # Verify that the upstream branch has been uploaded too, otherwise
3272 # Gerrit will create additional CLs when uploading.
3273 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3274 RunGitSilent(['rev-parse', parent + ':'])):
3275 DieWithError(
3276 '\nUpload upstream branch %s first.\n'
3277 'It is likely that this branch has been rebased since its last '
3278 'upload, so you just need to upload it again.\n'
3279 '(If you uploaded it with --no-squash, then branch dependencies '
3280 'are not supported, and you should reupload with --squash.)' %
3281 upstream_branch_name, change_desc)
3282 return parent
Aaron Gablef97e33d2017-03-30 15:44:27 -07003283
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003284 def _UpdateWithExternalChanges(self):
3285 """Updates workspace with external changes.
Gavin Mak4e5e3992022-11-14 22:40:12 +00003286
3287 Returns the commit hash that should be used as the merge base on upload.
3288 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003289 local_ps = self.GetPatchset()
3290 if local_ps is None:
3291 return
Gavin Mak4e5e3992022-11-14 22:40:12 +00003292
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003293 external_ps = self.GetMostRecentPatchset(update=False)
3294 if external_ps is None or local_ps == external_ps or \
3295 not self._IsPatchsetRangeSignificant(local_ps + 1, external_ps):
3296 return
Gavin Mak4e5e3992022-11-14 22:40:12 +00003297
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003298 num_changes = external_ps - local_ps
3299 if num_changes > 1:
3300 change_words = 'changes were'
3301 else:
3302 change_words = 'change was'
3303 print('\n%d external %s published to %s:\n' %
3304 (num_changes, change_words, self.GetIssueURL(short=True)))
Gavin Mak6f905472023-01-06 21:01:36 +00003305
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003306 # Print an overview of external changes.
3307 ps_to_commit = {}
3308 ps_to_info = {}
3309 revisions = self._GetChangeDetail(['ALL_REVISIONS'])
3310 for commit_id, revision_info in revisions.get('revisions', {}).items():
3311 ps_num = revision_info['_number']
3312 ps_to_commit[ps_num] = commit_id
3313 ps_to_info[ps_num] = revision_info
Gavin Mak6f905472023-01-06 21:01:36 +00003314
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003315 for ps in range(external_ps, local_ps, -1):
3316 commit = ps_to_commit[ps][:8]
3317 desc = ps_to_info[ps].get('description', '')
3318 print('Patchset %d [%s] %s' % (ps, commit, desc))
Gavin Mak6f905472023-01-06 21:01:36 +00003319
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003320 print('\nSee diff at: %s/%d..%d' %
3321 (self.GetIssueURL(short=True), local_ps, external_ps))
3322 print('\nUploading without applying patches will override them.')
Josip Sokcevic43ceaf02023-05-25 15:56:00 +00003323
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003324 if not ask_for_explicit_yes('Get the latest changes and apply on top?'):
3325 return
Gavin Mak4e5e3992022-11-14 22:40:12 +00003326
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003327 # Get latest Gerrit merge base. Use the first parent even if multiple
3328 # exist.
3329 external_parent = self._GetChangeCommit(
3330 revision=external_ps)['parents'][0]
3331 external_base = external_parent['commit']
Gavin Mak4e5e3992022-11-14 22:40:12 +00003332
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003333 branch = git_common.current_branch()
3334 local_base = self.GetCommonAncestorWithUpstream()
3335 if local_base != external_base:
3336 print('\nLocal merge base %s is different from Gerrit %s.\n' %
3337 (local_base, external_base))
3338 if git_common.upstream(branch):
3339 confirm_or_exit(
3340 'Can\'t apply the latest changes from Gerrit.\n'
3341 'Continue with upload and override the latest changes?')
3342 return
3343 print(
3344 'No upstream branch set. Continuing upload with Gerrit merge base.'
3345 )
Gavin Mak4e5e3992022-11-14 22:40:12 +00003346
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003347 external_parent_last_uploaded = self._GetChangeCommit(
3348 revision=local_ps)['parents'][0]
3349 external_base_last_uploaded = external_parent_last_uploaded['commit']
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003350
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003351 if external_base != external_base_last_uploaded:
3352 print('\nPatch set merge bases are different (%s, %s).\n' %
3353 (external_base_last_uploaded, external_base))
3354 confirm_or_exit(
3355 'Can\'t apply the latest changes from Gerrit.\n'
3356 'Continue with upload and override the latest changes?')
3357 return
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003358
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003359 # Fetch Gerrit's CL base if it doesn't exist locally.
3360 remote, _ = self.GetRemoteBranch()
3361 if not scm.GIT.IsValidRevision(settings.GetRoot(), external_base):
3362 RunGitSilent(['fetch', remote, external_base])
Gavin Mak4e5e3992022-11-14 22:40:12 +00003363
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003364 # Get the diff between local_ps and external_ps.
3365 print('Fetching changes...')
3366 issue = self.GetIssue()
3367 changes_ref = 'refs/changes/%02d/%d/' % (issue % 100, issue)
3368 RunGitSilent(['fetch', remote, changes_ref + str(local_ps)])
3369 last_uploaded = RunGitSilent(['rev-parse', 'FETCH_HEAD']).strip()
3370 RunGitSilent(['fetch', remote, changes_ref + str(external_ps)])
3371 latest_external = RunGitSilent(['rev-parse', 'FETCH_HEAD']).strip()
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003372
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003373 # If the commit parents are different, don't apply the diff as it very
3374 # likely contains many more changes not relevant to this CL.
3375 parents = RunGitSilent(
3376 ['rev-parse',
3377 '%s~1' % (last_uploaded),
3378 '%s~1' % (latest_external)]).strip().split()
3379 assert len(parents) == 2, 'Expected two parents.'
3380 if parents[0] != parents[1]:
3381 confirm_or_exit(
3382 'Can\'t apply the latest changes from Gerrit (parent mismatch '
3383 'between PS).\n'
3384 'Continue with upload and override the latest changes?')
3385 return
Josip Sokcevic2568d4c2023-05-24 03:37:42 +00003386
Joanna Wangbcba1782023-09-12 22:48:05 +00003387 diff = RunGitSilent([
3388 'diff', '--no-ext-diff',
3389 '%s..%s' % (last_uploaded, latest_external)
3390 ])
Gavin Mak4e5e3992022-11-14 22:40:12 +00003391
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003392 # Diff can be empty in the case of trivial rebases.
3393 if not diff:
3394 return external_base
Gavin Mak4e5e3992022-11-14 22:40:12 +00003395
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003396 # Apply the diff.
3397 with gclient_utils.temporary_file() as diff_tempfile:
3398 gclient_utils.FileWrite(diff_tempfile, diff)
3399 clean_patch = RunGitWithCode(['apply', '--check',
3400 diff_tempfile])[0] == 0
3401 RunGitSilent(['apply', '-3', '--intent-to-add', diff_tempfile])
3402 if not clean_patch:
3403 # Normally patchset is set after upload. But because we exit,
3404 # that never happens. Updating here makes sure that subsequent
3405 # uploads don't need to fetch/apply the same diff again.
3406 self.SetPatchset(external_ps)
3407 DieWithError(
3408 '\nPatch did not apply cleanly. Please resolve any '
3409 'conflicts and reupload.')
Gavin Mak4e5e3992022-11-14 22:40:12 +00003410
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003411 message = 'Incorporate external changes from '
3412 if num_changes == 1:
3413 message += 'patchset %d' % external_ps
3414 else:
3415 message += 'patchsets %d to %d' % (local_ps + 1, external_ps)
3416 RunGitSilent(['commit', '-am', message])
3417 # TODO(crbug.com/1382528): Use the previous commit's message as a
3418 # default patchset title instead of this 'Incorporate' message.
3419 return external_base
Gavin Mak4e5e3992022-11-14 22:40:12 +00003420
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003421 def _AddChangeIdToCommitMessage(self, log_desc, args):
3422 """Re-commits using the current message, assumes the commit hook is in
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003423 place.
3424 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003425 RunGit(['commit', '--amend', '-m', log_desc])
3426 new_log_desc = _create_description_from_log(args)
3427 if git_footers.get_footer_change_id(new_log_desc):
3428 print('git-cl: Added Change-Id to commit message.')
3429 return new_log_desc
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003430
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003431 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003432
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003433 def CannotTriggerTryJobReason(self):
3434 try:
3435 data = self._GetChangeDetail()
3436 except GerritChangeNotExists:
3437 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003438
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003439 if data['status'] in ('ABANDONED', 'MERGED'):
3440 return 'CL %s is closed' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003441
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003442 def GetGerritChange(self, patchset=None):
3443 """Returns a buildbucket.v2.GerritChange message for the current issue."""
3444 host = urllib.parse.urlparse(self.GetCodereviewServer()).hostname
3445 issue = self.GetIssue()
3446 patchset = int(patchset or self.GetPatchset())
3447 data = self._GetChangeDetail(['ALL_REVISIONS'])
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003448
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003449 assert host and issue and patchset, 'CL must be uploaded first'
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003450
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003451 has_patchset = any(
3452 int(revision_data['_number']) == patchset
3453 for revision_data in data['revisions'].values())
3454 if not has_patchset:
3455 raise Exception('Patchset %d is not known in Gerrit change %d' %
3456 (patchset, self.GetIssue()))
Edward Lemurd4d1ba42019-09-20 21:46:37 +00003457
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003458 return {
3459 'host': host,
3460 'change': issue,
3461 'project': data['project'],
3462 'patchset': patchset,
3463 }
tandriie113dfd2016-10-11 10:20:12 -07003464
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003465 def GetIssueOwner(self):
3466 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003467
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003468 def GetReviewers(self):
3469 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3470 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01003471
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003472
Lei Zhang8a0efc12020-08-05 19:58:45 +00003473def _get_bug_line_values(default_project_prefix, bugs):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003474 """Given default_project_prefix and comma separated list of bugs, yields bug
Lei Zhang8a0efc12020-08-05 19:58:45 +00003475 line values.
tandriif9aefb72016-07-01 09:06:51 -07003476
3477 Each bug can be either:
Lei Zhang8a0efc12020-08-05 19:58:45 +00003478 * a number, which is combined with default_project_prefix
tandriif9aefb72016-07-01 09:06:51 -07003479 * string, which is left as is.
3480
3481 This function may produce more than one line, because bugdroid expects one
3482 project per line.
3483
Lei Zhang8a0efc12020-08-05 19:58:45 +00003484 >>> list(_get_bug_line_values('v8:', '123,chromium:789'))
tandriif9aefb72016-07-01 09:06:51 -07003485 ['v8:123', 'chromium:789']
3486 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003487 default_bugs = []
3488 others = []
3489 for bug in bugs.split(','):
3490 bug = bug.strip()
3491 if bug:
3492 try:
3493 default_bugs.append(int(bug))
3494 except ValueError:
3495 others.append(bug)
tandriif9aefb72016-07-01 09:06:51 -07003496
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003497 if default_bugs:
3498 default_bugs = ','.join(map(str, default_bugs))
3499 if default_project_prefix:
3500 if not default_project_prefix.endswith(':'):
3501 default_project_prefix += ':'
3502 yield '%s%s' % (default_project_prefix, default_bugs)
3503 else:
3504 yield default_bugs
3505 for other in sorted(others):
3506 # Don't bother finding common prefixes, CLs with >2 bugs are very very
3507 # rare.
3508 yield other
tandriif9aefb72016-07-01 09:06:51 -07003509
3510
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003511def FindCodereviewSettingsFile(filename='codereview.settings'):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003512 """Finds the given file starting in the cwd and going up.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003513
3514 Only looks up to the top of the repository unless an
3515 'inherit-review-settings-ok' file exists in the root of the repository.
3516 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003517 inherit_ok_file = 'inherit-review-settings-ok'
3518 cwd = os.getcwd()
3519 root = settings.GetRoot()
3520 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3521 root = None
3522 while True:
3523 if os.path.isfile(os.path.join(cwd, filename)):
3524 return open(os.path.join(cwd, filename))
3525 if cwd == root:
3526 break
3527 parent_dir = os.path.dirname(cwd)
3528 if parent_dir == cwd:
3529 # We hit the system root directory.
3530 break
3531 cwd = parent_dir
3532 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003533
3534
3535def LoadCodereviewSettingsFromFile(fileobj):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003536 """Parses a codereview.settings file and updates hooks."""
3537 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003538
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003539 def SetProperty(name, setting, unset_error_ok=False):
3540 fullname = 'rietveld.' + name
3541 if setting in keyvals:
3542 RunGit(['config', fullname, keyvals[setting]])
3543 else:
3544 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003545
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003546 if not keyvals.get('GERRIT_HOST', False):
3547 SetProperty('server', 'CODE_REVIEW_SERVER')
3548 # Only server setting is required. Other settings can be absent.
3549 # In that case, we ignore errors raised during option deletion attempt.
3550 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3551 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3552 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
3553 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
3554 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3555 SetProperty('cpplint-ignore-regex',
3556 'LINT_IGNORE_REGEX',
3557 unset_error_ok=True)
3558 SetProperty('run-post-upload-hook',
3559 'RUN_POST_UPLOAD_HOOK',
3560 unset_error_ok=True)
3561 SetProperty('format-full-by-default',
3562 'FORMAT_FULL_BY_DEFAULT',
3563 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003564
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003565 if 'GERRIT_HOST' in keyvals:
3566 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003567
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003568 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
3569 RunGit([
3570 'config', 'gerrit.squash-uploads', keyvals['GERRIT_SQUASH_UPLOADS']
3571 ])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003572
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003573 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
3574 RunGit([
3575 'config', 'gerrit.skip-ensure-authenticated',
3576 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']
3577 ])
tandrii@chromium.org28253532016-04-14 13:46:56 +00003578
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003579 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3580 # should be of the form
3581 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3582 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
3583 RunGit([
3584 'config', keyvals['PUSH_URL_CONFIG'], keyvals['ORIGIN_URL_CONFIG']
3585 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003586
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003587
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003588def urlretrieve(source, destination):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003589 """Downloads a network object to a local file, like urllib.urlretrieve.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003590
3591 This is necessary because urllib is broken for SSL connections via a proxy.
3592 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003593 with open(destination, 'wb') as f:
3594 f.write(urllib.request.urlopen(source).read())
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003595
3596
ukai@chromium.org712d6102013-11-27 00:52:58 +00003597def hasSheBang(fname):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003598 """Checks fname is a #! script."""
3599 with open(fname) as f:
3600 return f.read(2).startswith('#!')
ukai@chromium.org712d6102013-11-27 00:52:58 +00003601
3602
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003603def DownloadGerritHook(force):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003604 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003605
3606 Args:
3607 force: True to update hooks. False to install hooks if not present.
3608 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003609 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
3610 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3611 if not os.access(dst, os.X_OK):
3612 if os.path.exists(dst):
3613 if not force:
3614 return
3615 try:
3616 urlretrieve(src, dst)
3617 if not hasSheBang(dst):
3618 DieWithError('Not a script: %s\n'
3619 'You need to download from\n%s\n'
3620 'into .git/hooks/commit-msg and '
3621 'chmod +x .git/hooks/commit-msg' % (dst, src))
3622 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3623 except Exception:
3624 if os.path.exists(dst):
3625 os.remove(dst)
3626 DieWithError('\nFailed to download hooks.\n'
3627 'You need to download from\n%s\n'
3628 'into .git/hooks/commit-msg and '
3629 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003630
3631
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003632class _GitCookiesChecker(object):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003633 """Provides facilities for validating and suggesting fixes to .gitcookies."""
3634 def __init__(self):
3635 # Cached list of [host, identity, source], where source is either
3636 # .gitcookies or .netrc.
3637 self._all_hosts = None
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003638
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003639 def ensure_configured_gitcookies(self):
3640 """Runs checks and suggests fixes to make git use .gitcookies from default
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003641 path."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003642 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3643 configured_path = RunGitSilent(
3644 ['config', '--global', 'http.cookiefile']).strip()
3645 configured_path = os.path.expanduser(configured_path)
3646 if configured_path:
3647 self._ensure_default_gitcookies_path(configured_path, default)
3648 else:
3649 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003650
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003651 @staticmethod
3652 def _ensure_default_gitcookies_path(configured_path, default_path):
3653 assert configured_path
3654 if configured_path == default_path:
3655 print('git is already configured to use your .gitcookies from %s' %
3656 configured_path)
3657 return
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003658
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003659 print('WARNING: You have configured custom path to .gitcookies: %s\n'
3660 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3661 (configured_path, default_path))
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003662
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003663 if not os.path.exists(configured_path):
3664 print('However, your configured .gitcookies file is missing.')
3665 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3666 action='reconfigure')
3667 RunGit(['config', '--global', 'http.cookiefile', default_path])
3668 return
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003669
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003670 if os.path.exists(default_path):
3671 print('WARNING: default .gitcookies file already exists %s' %
3672 default_path)
3673 DieWithError(
3674 'Please delete %s manually and re-run git cl creds-check' %
3675 default_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003676
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003677 confirm_or_exit('Move existing .gitcookies to default location?',
3678 action='move')
3679 shutil.move(configured_path, default_path)
3680 RunGit(['config', '--global', 'http.cookiefile', default_path])
3681 print('Moved and reconfigured git to use .gitcookies from %s' %
3682 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003683
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003684 @staticmethod
3685 def _configure_gitcookies_path(default_path):
3686 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3687 if os.path.exists(netrc_path):
3688 print(
3689 'You seem to be using outdated .netrc for git credentials: %s' %
3690 netrc_path)
3691 print(
3692 'This tool will guide you through setting up recommended '
3693 '.gitcookies store for git credentials.\n'
3694 '\n'
3695 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3696 ' git config --global --unset http.cookiefile\n'
3697 ' mv %s %s.backup\n\n' % (default_path, default_path))
3698 confirm_or_exit(action='setup .gitcookies')
3699 RunGit(['config', '--global', 'http.cookiefile', default_path])
3700 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003701
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003702 def get_hosts_with_creds(self, include_netrc=False):
3703 if self._all_hosts is None:
3704 a = gerrit_util.CookiesAuthenticator()
3705 self._all_hosts = [(h, u, s) for h, u, s in itertools.chain((
3706 (h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.items()), (
3707 (h, u, '.gitcookies')
3708 for h, (u, _) in a.gitcookies.items()))
3709 if h.endswith(_GOOGLESOURCE)]
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003710
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003711 if include_netrc:
3712 return self._all_hosts
3713 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003714
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003715 def print_current_creds(self, include_netrc=False):
3716 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3717 if not hosts:
3718 print('No Git/Gerrit credentials found')
3719 return
3720 lengths = [max(map(len, (row[i] for row in hosts))) for i in range(3)]
3721 header = [('Host', 'User', 'Which file'), ['=' * l for l in lengths]]
3722 for row in (header + hosts):
3723 print('\t'.join((('%%+%ds' % l) % s) for l, s in zip(lengths, row)))
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003724
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003725 @staticmethod
3726 def _parse_identity(identity):
3727 """Parses identity "git-<username>.domain" into <username> and domain."""
3728 # Special case: usernames that contain ".", which are generally not
3729 # distinguishable from sub-domains. But we do know typical domains:
3730 if identity.endswith('.chromium.org'):
3731 domain = 'chromium.org'
3732 username = identity[:-len('.chromium.org')]
3733 else:
3734 username, domain = identity.split('.', 1)
3735 if username.startswith('git-'):
3736 username = username[len('git-'):]
3737 return username, domain
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003738
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003739 def has_generic_host(self):
3740 """Returns whether generic .googlesource.com has been configured.
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003741
3742 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3743 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003744 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3745 if host == '.' + _GOOGLESOURCE:
3746 return True
3747 return False
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003748
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003749 def _get_git_gerrit_identity_pairs(self):
3750 """Returns map from canonic host to pair of identities (Git, Gerrit).
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003751
3752 One of identities might be None, meaning not configured.
3753 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003754 host_to_identity_pairs = {}
3755 for host, identity, _ in self.get_hosts_with_creds():
3756 canonical = _canonical_git_googlesource_host(host)
3757 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3758 idx = 0 if canonical == host else 1
3759 pair[idx] = identity
3760 return host_to_identity_pairs
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003761
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003762 def get_partially_configured_hosts(self):
3763 return set(
3764 (host if i1 else _canonical_gerrit_googlesource_host(host))
3765 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
3766 if None in (i1, i2) and host != '.' + _GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003767
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003768 def get_conflicting_hosts(self):
3769 return set(
3770 host
3771 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().items()
3772 if None not in (i1, i2) and i1 != i2)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003773
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003774 def get_duplicated_hosts(self):
3775 counters = collections.Counter(
3776 h for h, _, _ in self.get_hosts_with_creds())
3777 return set(host for host, count in counters.items() if count > 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003778
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003779 @staticmethod
3780 def _format_hosts(hosts, extra_column_func=None):
3781 hosts = sorted(hosts)
3782 assert hosts
3783 if extra_column_func is None:
3784 extras = [''] * len(hosts)
3785 else:
3786 extras = [extra_column_func(host) for host in hosts]
3787 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len,
3788 extras)))
3789 lines = []
3790 for he in zip(hosts, extras):
3791 lines.append(tmpl % he)
3792 return lines
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003793
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003794 def _find_problems(self):
3795 if self.has_generic_host():
3796 yield ('.googlesource.com wildcard record detected', [
3797 'Chrome Infrastructure team recommends to list full host names '
3798 'explicitly.'
3799 ], None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003800
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003801 dups = self.get_duplicated_hosts()
3802 if dups:
3803 yield ('The following hosts were defined twice',
3804 self._format_hosts(dups), None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003805
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003806 partial = self.get_partially_configured_hosts()
3807 if partial:
3808 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3809 'These hosts are missing',
3810 self._format_hosts(
3811 partial, lambda host: 'but %s defined' %
3812 _get_counterpart_host(host)), partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003813
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003814 conflicting = self.get_conflicting_hosts()
3815 if conflicting:
3816 yield (
3817 'The following Git hosts have differing credentials from their '
3818 'Gerrit counterparts',
3819 self._format_hosts(
3820 conflicting, lambda host: '%s vs %s' % tuple(
3821 self._get_git_gerrit_identity_pairs()[host])),
3822 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003823
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003824 def find_and_report_problems(self):
3825 """Returns True if there was at least one problem, else False."""
3826 found = False
3827 bad_hosts = set()
3828 for title, sublines, hosts in self._find_problems():
3829 if not found:
3830 found = True
3831 print('\n\n.gitcookies problem report:\n')
3832 bad_hosts.update(hosts or [])
3833 print(' %s%s' % (title, (':' if sublines else '')))
3834 if sublines:
3835 print()
3836 print(' %s' % '\n '.join(sublines))
3837 print()
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003838
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003839 if bad_hosts:
3840 assert found
3841 print(
3842 ' You can manually remove corresponding lines in your %s file and '
3843 'visit the following URLs with correct account to generate '
3844 'correct credential lines:\n' %
3845 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3846 print(' %s' % '\n '.join(
3847 sorted(
3848 set(gerrit_util.CookiesAuthenticator().get_new_password_url(
3849 _canonical_git_googlesource_host(host))
3850 for host in bad_hosts))))
3851 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003852
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003853
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003854@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003855def CMDcreds_check(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003856 """Checks credentials and suggests changes."""
3857 _, _ = parser.parse_args(args)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003858
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003859 # Code below checks .gitcookies. Abort if using something else.
3860 authn = gerrit_util.Authenticator.get()
3861 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3862 message = (
3863 'This command is not designed for bot environment. It checks '
3864 '~/.gitcookies file not generally used on bots.')
3865 # TODO(crbug.com/1059384): Automatically detect when running on
3866 # cloudtop.
3867 if isinstance(authn, gerrit_util.GceAuthenticator):
3868 message += (
3869 '\n'
3870 'If you need to run this on GCE or a cloudtop instance, '
3871 'export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
3872 DieWithError(message)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003873
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003874 checker = _GitCookiesChecker()
3875 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003876
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003877 print('Your .netrc and .gitcookies have credentials for these hosts:')
3878 checker.print_current_creds(include_netrc=True)
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003879
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003880 if not checker.find_and_report_problems():
3881 print('\nNo problems detected in your .gitcookies file.')
3882 return 0
3883 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003884
3885
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003886@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003887def CMDbaseurl(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003888 """Gets or sets base-url for this branch."""
3889 _, args = parser.parse_args(args)
3890 branchref = scm.GIT.GetBranchRef(settings.GetRoot())
3891 branch = scm.GIT.ShortBranchName(branchref)
3892 if not args:
3893 print('Current base-url:')
3894 return RunGit(['config', 'branch.%s.base-url' % branch],
3895 error_ok=False).strip()
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00003896
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003897 print('Setting base-url to %s' % args[0])
3898 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3899 error_ok=False).strip()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003900
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003901
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003902def color_for_status(status):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003903 """Maps a Changelist status to color, for CMDstatus and other tools."""
3904 BOLD = '\033[1m'
3905 return {
3906 'unsent': BOLD + Fore.YELLOW,
3907 'waiting': BOLD + Fore.RED,
3908 'reply': BOLD + Fore.YELLOW,
3909 'not lgtm': BOLD + Fore.RED,
3910 'lgtm': BOLD + Fore.GREEN,
3911 'commit': BOLD + Fore.MAGENTA,
3912 'closed': BOLD + Fore.CYAN,
3913 'error': BOLD + Fore.WHITE,
3914 }.get(status, Fore.WHITE)
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003915
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003916
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003917def get_cl_statuses(changes, fine_grained, max_processes=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003918 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003919
3920 If fine_grained is true, this will fetch CL statuses from the server.
3921 Otherwise, simply indicate if there's a matching url for the given branches.
3922
3923 If max_processes is specified, it is used as the maximum number of processes
3924 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3925 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003926
3927 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003928 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003929 if not changes:
3930 return
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003931
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003932 if not fine_grained:
3933 # Fast path which doesn't involve querying codereview servers.
3934 # Do not use get_approving_reviewers(), since it requires an HTTP
3935 # request.
3936 for cl in changes:
3937 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
3938 return
3939
3940 # First, sort out authentication issues.
3941 logging.debug('ensuring credentials exist')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003942 for cl in changes:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003943 cl.EnsureAuthenticated(force=False, refresh=True)
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003944
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003945 def fetch(cl):
3946 try:
3947 return (cl, cl.GetStatus())
3948 except:
3949 # See http://crbug.com/629863.
3950 logging.exception('failed to fetch status for cl %s:',
3951 cl.GetIssue())
3952 raise
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003953
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003954 threads_count = len(changes)
3955 if max_processes:
3956 threads_count = max(1, min(threads_count, max_processes))
3957 logging.debug('querying %d CLs using %d threads', len(changes),
3958 threads_count)
3959
3960 pool = multiprocessing.pool.ThreadPool(threads_count)
3961 fetched_cls = set()
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003962 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003963 it = pool.imap_unordered(fetch, changes).__iter__()
3964 while True:
3965 try:
3966 cl, status = it.next(timeout=5)
3967 except (multiprocessing.TimeoutError, StopIteration):
3968 break
3969 fetched_cls.add(cl)
3970 yield cl, status
3971 finally:
3972 pool.close()
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003973
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003974 # Add any branches that failed to fetch.
3975 for cl in set(changes) - fetched_cls:
3976 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003977
rmistry@google.com2dd99862015-06-22 12:22:18 +00003978
Jose Lopes3863fc52020-04-07 17:00:25 +00003979def upload_branch_deps(cl, args, force=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003980 """Uploads CLs of local branches that are dependents of the current branch.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003981
3982 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003983
3984 test1 -> test2.1 -> test3.1
3985 -> test3.2
3986 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003987
3988 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3989 run on the dependent branches in this order:
3990 test2.1, test3.1, test3.2, test2.2, test3.3
3991
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003992 Note: This function does not rebase your local dependent branches. Use it
3993 when you make a change to the parent branch that will not conflict
3994 with its dependent branches, and you would like their dependencies
3995 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003996 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00003997 if git_common.is_dirty_git_tree('upload-branch-deps'):
3998 return 1
rmistry@google.com2dd99862015-06-22 12:22:18 +00003999
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004000 root_branch = cl.GetBranch()
4001 if root_branch is None:
4002 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4003 'Get on a branch!')
4004 if not cl.GetIssue():
4005 DieWithError(
4006 'Current branch does not have an uploaded CL. We cannot set '
4007 'patchset dependencies without an uploaded CL.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004008
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004009 branches = RunGit([
4010 'for-each-ref', '--format=%(refname:short) %(upstream:short)',
4011 'refs/heads'
4012 ])
4013 if not branches:
4014 print('No local branches found.')
4015 return 0
rmistry@google.com2dd99862015-06-22 12:22:18 +00004016
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004017 # Create a dictionary of all local branches to the branches that are
4018 # dependent on it.
4019 tracked_to_dependents = collections.defaultdict(list)
4020 for b in branches.splitlines():
4021 tokens = b.split()
4022 if len(tokens) == 2:
4023 branch_name, tracked = tokens
4024 tracked_to_dependents[tracked].append(branch_name)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004025
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004026 print()
4027 print('The dependent local branches of %s are:' % root_branch)
4028 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004029
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004030 def traverse_dependents_preorder(branch, padding=''):
4031 dependents_to_process = tracked_to_dependents.get(branch, [])
4032 padding += ' '
4033 for dependent in dependents_to_process:
4034 print('%s%s' % (padding, dependent))
4035 dependents.append(dependent)
4036 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004037
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004038 traverse_dependents_preorder(root_branch)
4039 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004040
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004041 if not dependents:
4042 print('There are no dependent local branches for %s' % root_branch)
4043 return 0
rmistry@google.com2dd99862015-06-22 12:22:18 +00004044
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004045 # Record all dependents that failed to upload.
4046 failures = {}
4047 # Go through all dependents, checkout the branch and upload.
4048 try:
4049 for dependent_branch in dependents:
4050 print()
4051 print('--------------------------------------')
4052 print('Running "git cl upload" from %s:' % dependent_branch)
4053 RunGit(['checkout', '-q', dependent_branch])
4054 print()
4055 try:
4056 if CMDupload(OptionParser(), args) != 0:
4057 print('Upload failed for %s!' % dependent_branch)
4058 failures[dependent_branch] = 1
4059 except: # pylint: disable=bare-except
4060 failures[dependent_branch] = 1
4061 print()
4062 finally:
4063 # Swap back to the original root branch.
4064 RunGit(['checkout', '-q', root_branch])
4065
4066 print()
4067 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004068 for dependent_branch in dependents:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004069 upload_status = 'failed' if failures.get(
4070 dependent_branch) else 'succeeded'
4071 print(' %s : %s' % (dependent_branch, upload_status))
4072 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004073
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004074 return 0
rmistry@google.com2dd99862015-06-22 12:22:18 +00004075
4076
Tibor Goldschwendt7c5efb22020-03-25 01:23:54 +00004077def GetArchiveTagForBranch(issue_num, branch_name, existing_tags, pattern):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004078 """Given a proposed tag name, returns a tag name that is guaranteed to be
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00004079 unique. If 'foo' is proposed but already exists, then 'foo-2' is used,
4080 or 'foo-3', and so on."""
4081
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004082 proposed_tag = pattern.format(**{'issue': issue_num, 'branch': branch_name})
4083 for suffix_num in itertools.count(1):
4084 if suffix_num == 1:
4085 to_check = proposed_tag
4086 else:
4087 to_check = '%s-%d' % (proposed_tag, suffix_num)
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00004088
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004089 if to_check not in existing_tags:
4090 return to_check
Kevin Marshall0e60ecd2019-12-04 17:44:13 +00004091
4092
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004093@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004094def CMDarchive(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004095 """Archives and deletes branches associated with closed changelists."""
4096 parser.add_option(
4097 '-j',
4098 '--maxjobs',
4099 action='store',
4100 type=int,
4101 help='The maximum number of jobs to use when retrieving review status.')
4102 parser.add_option('-f',
4103 '--force',
4104 action='store_true',
4105 help='Bypasses the confirmation prompt.')
4106 parser.add_option('-d',
4107 '--dry-run',
4108 action='store_true',
4109 help='Skip the branch tagging and removal steps.')
4110 parser.add_option('-t',
4111 '--notags',
4112 action='store_true',
4113 help='Do not tag archived branches. '
4114 'Note: local commit history may be lost.')
4115 parser.add_option('-p',
4116 '--pattern',
4117 default='git-cl-archived-{issue}-{branch}',
4118 help='Format string for archive tags. '
4119 'E.g. \'archived-{issue}-{branch}\'.')
kmarshall3bff56b2016-06-06 18:31:47 -07004120
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004121 options, args = parser.parse_args(args)
4122 if args:
4123 parser.error('Unsupported args: %s' % ' '.join(args))
kmarshall3bff56b2016-06-06 18:31:47 -07004124
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004125 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4126 if not branches:
4127 return 0
4128
4129 tags = RunGit(['for-each-ref', '--format=%(refname)', 'refs/tags'
4130 ]).splitlines() or []
4131 tags = [t.split('/')[-1] for t in tags]
4132
4133 print('Finding all branches associated with closed issues...')
4134 changes = [Changelist(branchref=b) for b in branches.splitlines()]
4135 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4136 statuses = get_cl_statuses(changes,
4137 fine_grained=True,
4138 max_processes=options.maxjobs)
4139 proposal = [(cl.GetBranch(),
4140 GetArchiveTagForBranch(cl.GetIssue(), cl.GetBranch(), tags,
4141 options.pattern))
4142 for cl, status in statuses
4143 if status in ('closed', 'rietveld-not-supported')]
4144 proposal.sort()
4145
4146 if not proposal:
4147 print('No branches with closed codereview issues found.')
4148 return 0
4149
4150 current_branch = scm.GIT.GetBranch(settings.GetRoot())
4151
4152 print('\nBranches with closed issues that will be archived:\n')
4153 if options.notags:
4154 for next_item in proposal:
4155 print(' ' + next_item[0])
4156 else:
4157 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4158 for next_item in proposal:
4159 print('%*s %s' % (alignment, next_item[0], next_item[1]))
4160
4161 # Quit now on precondition failure or if instructed by the user, either
4162 # via an interactive prompt or by command line flags.
4163 if options.dry_run:
4164 print('\nNo changes were made (dry run).\n')
4165 return 0
4166
4167 if any(branch == current_branch for branch, _ in proposal):
4168 print('You are currently on a branch \'%s\' which is associated with a '
4169 'closed codereview issue, so archive cannot proceed. Please '
4170 'checkout another branch and run this command again.' %
4171 current_branch)
4172 return 1
4173
4174 if not options.force:
4175 answer = gclient_utils.AskForData(
4176 '\nProceed with deletion (Y/n)? ').lower()
4177 if answer not in ('y', ''):
4178 print('Aborted.')
4179 return 1
4180
4181 for branch, tagname in proposal:
4182 if not options.notags:
4183 RunGit(['tag', tagname, branch])
4184
4185 if RunGitWithCode(['branch', '-D', branch])[0] != 0:
4186 # Clean up the tag if we failed to delete the branch.
4187 RunGit(['tag', '-d', tagname])
4188
4189 print('\nJob\'s done!')
4190
kmarshall3bff56b2016-06-06 18:31:47 -07004191 return 0
4192
kmarshall3bff56b2016-06-06 18:31:47 -07004193
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004194@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004195def CMDstatus(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004196 """Show status of changelists.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004197
4198 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004199 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004200 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004201 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004202 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004203 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004204 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004205 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004206
4207 Also see 'git cl comments'.
4208 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004209 parser.add_option('--no-branch-color',
4210 action='store_true',
4211 help='Disable colorized branch names')
4212 parser.add_option(
4213 '--field', help='print only specific field (desc|id|patch|status|url)')
4214 parser.add_option('-f',
4215 '--fast',
4216 action='store_true',
4217 help='Do not retrieve review status')
4218 parser.add_option(
4219 '-j',
4220 '--maxjobs',
4221 action='store',
4222 type=int,
4223 help='The maximum number of jobs to use when retrieving review status')
4224 parser.add_option(
4225 '-i',
4226 '--issue',
4227 type=int,
4228 help='Operate on this issue instead of the current branch\'s implicit '
4229 'issue. Requires --field to be set.')
4230 parser.add_option('-d',
4231 '--date-order',
4232 action='store_true',
4233 help='Order branches by committer date.')
4234 options, args = parser.parse_args(args)
4235 if args:
4236 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004237
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004238 if options.issue is not None and not options.field:
4239 parser.error('--field must be given when --issue is set.')
iannucci3c972b92016-08-17 13:24:10 -07004240
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004241 if options.field:
4242 cl = Changelist(issue=options.issue)
4243 if options.field.startswith('desc'):
4244 if cl.GetIssue():
4245 print(cl.FetchDescription())
4246 elif options.field == 'id':
4247 issueid = cl.GetIssue()
4248 if issueid:
4249 print(issueid)
4250 elif options.field == 'patch':
4251 patchset = cl.GetMostRecentPatchset()
4252 if patchset:
4253 print(patchset)
4254 elif options.field == 'status':
4255 print(cl.GetStatus())
4256 elif options.field == 'url':
4257 url = cl.GetIssueURL()
4258 if url:
4259 print(url)
4260 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004261
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004262 branches = RunGit([
4263 'for-each-ref', '--format=%(refname) %(committerdate:unix)',
4264 'refs/heads'
4265 ])
4266 if not branches:
4267 print('No local branch found.')
4268 return 0
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004269
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004270 changes = [
4271 Changelist(branchref=b, commit_date=ct)
4272 for b, ct in map(lambda line: line.split(' '), branches.splitlines())
4273 ]
4274 print('Branches associated with reviews:')
4275 output = get_cl_statuses(changes,
4276 fine_grained=not options.fast,
4277 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004278
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004279 current_branch = scm.GIT.GetBranch(settings.GetRoot())
Daniel McArdlea23bf592019-02-12 00:25:12 +00004280
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004281 def FormatBranchName(branch, colorize=False):
4282 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
Daniel McArdlea23bf592019-02-12 00:25:12 +00004283 an asterisk when it is the current branch."""
4284
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004285 asterisk = ""
4286 color = Fore.RESET
4287 if branch == current_branch:
4288 asterisk = "* "
4289 color = Fore.GREEN
4290 branch_name = scm.GIT.ShortBranchName(branch)
Daniel McArdlea23bf592019-02-12 00:25:12 +00004291
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004292 if colorize:
4293 return asterisk + color + branch_name + Fore.RESET
4294 return asterisk + branch_name
Daniel McArdle452a49f2019-02-14 17:28:31 +00004295
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004296 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00004297
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004298 alignment = max(5,
4299 max(len(FormatBranchName(c.GetBranch())) for c in changes))
Sigurd Schneider1bfda8e2021-06-30 14:46:25 +00004300
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004301 if options.date_order or settings.IsStatusCommitOrderByDate():
4302 sorted_changes = sorted(changes,
4303 key=lambda c: c.GetCommitDate(),
4304 reverse=True)
4305 else:
4306 sorted_changes = sorted(changes, key=lambda c: c.GetBranch())
4307 for cl in sorted_changes:
4308 branch = cl.GetBranch()
4309 while branch not in branch_statuses:
4310 c, status = next(output)
4311 branch_statuses[c.GetBranch()] = status
4312 status = branch_statuses.pop(branch)
4313 url = cl.GetIssueURL(short=True)
4314 if url and (not status or status == 'error'):
4315 # The issue probably doesn't exist anymore.
4316 url += ' (broken)'
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004317
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004318 color = color_for_status(status)
4319 # Turn off bold as well as colors.
4320 END = '\033[0m'
4321 reset = Fore.RESET + END
4322 if not setup_color.IS_TTY:
4323 color = ''
4324 reset = ''
4325 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00004326
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004327 branch_display = FormatBranchName(branch)
4328 padding = ' ' * (alignment - len(branch_display))
4329 if not options.no_branch_color:
4330 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00004331
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004332 print(' %s : %s%s %s%s' %
4333 (padding + branch_display, color, url, status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004334
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004335 print()
4336 print('Current branch: %s' % current_branch)
4337 for cl in changes:
4338 if cl.GetBranch() == current_branch:
4339 break
4340 if not cl.GetIssue():
4341 print('No issue assigned.')
4342 return 0
4343 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4344 if not options.fast:
4345 print('Issue description:')
4346 print(cl.FetchDescription(pretty=True))
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004347 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004348
4349
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004350def colorize_CMDstatus_doc():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004351 """To be called once in main() to add colors to git cl status help."""
4352 colors = [i for i in dir(Fore) if i[0].isupper()]
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004353
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004354 def colorize_line(line):
4355 for color in colors:
4356 if color in line.upper():
4357 # Extract whitespace first and the leading '-'.
4358 indent = len(line) - len(line.lstrip(' ')) + 1
4359 return line[:indent] + getattr(
4360 Fore, color) + line[indent:] + Fore.RESET
4361 return line
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004362
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004363 lines = CMDstatus.__doc__.splitlines()
4364 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004365
4366
phajdan.jre328cf92016-08-22 04:12:17 -07004367def write_json(path, contents):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004368 if path == '-':
4369 json.dump(contents, sys.stdout)
4370 else:
4371 with open(path, 'w') as f:
4372 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004373
4374
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004375@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004376@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004377def CMDissue(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004378 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004379
4380 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004381 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004382 parser.add_option('-r',
4383 '--reverse',
4384 action='store_true',
4385 help='Lookup the branch(es) for the specified issues. If '
4386 'no issues are specified, all branches with mapped '
4387 'issues will be listed.')
4388 parser.add_option('--json',
4389 help='Path to JSON output file, or "-" for stdout.')
4390 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004391
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004392 if options.reverse:
4393 branches = RunGit(['for-each-ref', 'refs/heads',
4394 '--format=%(refname)']).splitlines()
4395 # Reverse issue lookup.
4396 issue_branch_map = {}
Arthur Milchior801a9752023-04-07 10:33:54 +00004397
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004398 git_config = {}
4399 for config in RunGit(['config', '--get-regexp',
4400 r'branch\..*issue']).splitlines():
4401 name, _space, val = config.partition(' ')
4402 git_config[name] = val
Arthur Milchior801a9752023-04-07 10:33:54 +00004403
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004404 for branch in branches:
4405 issue = git_config.get(
4406 'branch.%s.%s' %
4407 (scm.GIT.ShortBranchName(branch), ISSUE_CONFIG_KEY))
4408 if issue:
4409 issue_branch_map.setdefault(int(issue), []).append(branch)
4410 if not args:
4411 args = sorted(issue_branch_map.keys())
4412 result = {}
4413 for issue in args:
4414 try:
4415 issue_num = int(issue)
4416 except ValueError:
4417 print('ERROR cannot parse issue number: %s' % issue,
4418 file=sys.stderr)
4419 continue
4420 result[issue_num] = issue_branch_map.get(issue_num)
4421 print('Branch for issue number %s: %s' % (issue, ', '.join(
4422 issue_branch_map.get(issue_num) or ('None', ))))
4423 if options.json:
4424 write_json(options.json, result)
4425 return 0
4426
4427 if len(args) > 0:
4428 issue = ParseIssueNumberArgument(args[0])
4429 if not issue.valid:
4430 DieWithError(
4431 'Pass a url or number to set the issue, 0 to unset it, '
4432 'or no argument to list it.\n'
4433 'Maybe you want to run git cl status?')
4434 cl = Changelist()
4435 cl.SetIssue(issue.issue)
4436 else:
4437 cl = Changelist()
4438 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
Arthur Milchior801a9752023-04-07 10:33:54 +00004439 if options.json:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004440 write_json(
4441 options.json, {
4442 'gerrit_host': cl.GetGerritHost(),
4443 'gerrit_project': cl.GetGerritProject(),
4444 'issue_url': cl.GetIssueURL(),
4445 'issue': cl.GetIssue(),
4446 })
Arthur Milchior801a9752023-04-07 10:33:54 +00004447 return 0
Aaron Gable78753da2017-06-15 10:35:49 -07004448
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004449
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004450@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004451def CMDcomments(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004452 """Shows or posts review comments for any changelist."""
4453 parser.add_option('-a',
4454 '--add-comment',
4455 dest='comment',
4456 help='comment to add to an issue')
4457 parser.add_option('-p',
4458 '--publish',
4459 action='store_true',
4460 help='marks CL as ready and sends comment to reviewers')
4461 parser.add_option('-i',
4462 '--issue',
4463 dest='issue',
4464 help='review issue id (defaults to current issue).')
4465 parser.add_option('-m',
4466 '--machine-readable',
4467 dest='readable',
4468 action='store_false',
4469 default=True,
4470 help='output comments in a format compatible with '
4471 'editor parsing')
4472 parser.add_option('-j',
4473 '--json-file',
4474 help='File to write JSON summary to, or "-" for stdout')
4475 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004476
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004477 issue = None
4478 if options.issue:
4479 try:
4480 issue = int(options.issue)
4481 except ValueError:
4482 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004483
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004484 cl = Changelist(issue=issue)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004485
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004486 if options.comment:
4487 cl.AddComment(options.comment, options.publish)
4488 return 0
4489
4490 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4491 key=lambda c: c.date)
4492 for comment in summary:
4493 if comment.disapproval:
4494 color = Fore.RED
4495 elif comment.approval:
4496 color = Fore.GREEN
4497 elif comment.sender == cl.GetIssueOwner():
4498 color = Fore.MAGENTA
4499 elif comment.autogenerated:
4500 color = Fore.CYAN
4501 else:
4502 color = Fore.BLUE
4503 print('\n%s%s %s%s\n%s' %
4504 (color, comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4505 comment.sender, Fore.RESET, '\n'.join(
4506 ' ' + l for l in comment.message.strip().splitlines())))
4507
4508 if options.json_file:
4509
4510 def pre_serialize(c):
4511 dct = c._asdict().copy()
4512 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4513 return dct
4514
4515 write_json(options.json_file, [pre_serialize(x) for x in summary])
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004516 return 0
4517
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004518
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004519@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004520@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004521def CMDdescription(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004522 """Brings up the editor for the current CL's description."""
4523 parser.add_option(
4524 '-d',
4525 '--display',
4526 action='store_true',
4527 help='Display the description instead of opening an editor')
4528 parser.add_option(
4529 '-n',
4530 '--new-description',
4531 help='New description to set for this issue (- for stdin, '
4532 '+ to load from local commit HEAD)')
4533 parser.add_option('-f',
4534 '--force',
4535 action='store_true',
4536 help='Delete any unpublished Gerrit edits for this issue '
4537 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004538
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004539 options, args = parser.parse_args(args)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004540
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004541 target_issue_arg = None
4542 if len(args) > 0:
4543 target_issue_arg = ParseIssueNumberArgument(args[0])
4544 if not target_issue_arg.valid:
4545 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004546
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004547 kwargs = {}
4548 if target_issue_arg:
4549 kwargs['issue'] = target_issue_arg.issue
4550 kwargs['codereview_host'] = target_issue_arg.hostname
martiniss6eda05f2016-06-30 10:18:35 -07004551
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004552 cl = Changelist(**kwargs)
4553 if not cl.GetIssue():
4554 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004555
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004556 if args and not args[0].isdigit():
4557 logging.info('canonical issue/change URL: %s\n', cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004558
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004559 description = ChangeDescription(cl.FetchDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004560
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004561 if options.display:
4562 print(description.description)
4563 return 0
4564
4565 if options.new_description:
4566 text = options.new_description
4567 if text == '-':
4568 text = '\n'.join(l.rstrip() for l in sys.stdin)
4569 elif text == '+':
4570 base_branch = cl.GetCommonAncestorWithUpstream()
4571 text = _create_description_from_log([base_branch])
4572
4573 description.set_description(text)
4574 else:
4575 description.prompt()
4576 if cl.FetchDescription().strip() != description.description:
4577 cl.UpdateDescription(description.description, force=options.force)
smut@google.com34fb6b12015-07-13 20:03:26 +00004578 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004579
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004580
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004581@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004582def CMDlint(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004583 """Runs cpplint on the current changelist."""
4584 parser.add_option(
4585 '--filter',
4586 action='append',
4587 metavar='-x,+y',
4588 help='Comma-separated list of cpplint\'s category-filters')
4589 options, args = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004590
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004591 # Access to a protected member _XX of a client class
4592 # pylint: disable=protected-access
4593 try:
4594 import cpplint
4595 import cpplint_chromium
4596 except ImportError:
4597 print(
4598 'Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.'
4599 )
4600 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004601
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004602 # Change the current working directory before calling lint so that it
4603 # shows the correct base.
4604 previous_cwd = os.getcwd()
4605 os.chdir(settings.GetRoot())
4606 try:
4607 cl = Changelist()
4608 files = cl.GetAffectedFiles(cl.GetCommonAncestorWithUpstream())
4609 if not files:
4610 print('Cannot lint an empty CL')
4611 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004612
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004613 # Process cpplint arguments, if any.
4614 filters = presubmit_canned_checks.GetCppLintFilters(options.filter)
4615 command = ['--filter=' + ','.join(filters)]
4616 command.extend(args)
4617 command.extend(files)
4618 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004619
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004620 include_regex = re.compile(settings.GetLintRegex())
4621 ignore_regex = re.compile(settings.GetLintIgnoreRegex())
4622 extra_check_functions = [
4623 cpplint_chromium.CheckPointerDeclarationWhitespace
4624 ]
4625 for filename in filenames:
4626 if not include_regex.match(filename):
4627 print('Skipping file %s' % filename)
4628 continue
Lei Zhang379d1ad2020-07-15 19:40:06 +00004629
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004630 if ignore_regex.match(filename):
4631 print('Ignoring file %s' % filename)
4632 continue
Lei Zhang379d1ad2020-07-15 19:40:06 +00004633
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004634 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4635 extra_check_functions)
4636 finally:
4637 os.chdir(previous_cwd)
4638 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
4639 if cpplint._cpplint_state.error_count != 0:
4640 return 1
4641 return 0
thestig@chromium.org44202a22014-03-11 19:22:18 +00004642
4643
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004644@metrics.collector.collect_metrics('git cl presubmit')
mlcuic601e362023-08-14 23:39:46 +00004645@subcommand.usage('[base branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004646def CMDpresubmit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004647 """Runs presubmit tests on the current changelist."""
4648 parser.add_option('-u',
4649 '--upload',
4650 action='store_true',
4651 help='Run upload hook instead of the push hook')
4652 parser.add_option('-f',
4653 '--force',
4654 action='store_true',
4655 help='Run checks even if tree is dirty')
4656 parser.add_option(
4657 '--all',
4658 action='store_true',
4659 help='Run checks against all files, not just modified ones')
4660 parser.add_option('--files',
4661 nargs=1,
4662 help='Semicolon-separated list of files to be marked as '
4663 'modified when executing presubmit or post-upload hooks. '
4664 'fnmatch wildcards can also be used.')
4665 parser.add_option(
4666 '--parallel',
4667 action='store_true',
4668 help='Run all tests specified by input_api.RunTests in all '
4669 'PRESUBMIT files in parallel.')
4670 parser.add_option('--resultdb',
4671 action='store_true',
4672 help='Run presubmit checks in the ResultSink environment '
4673 'and send results to the ResultDB database.')
4674 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
4675 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004676
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004677 if not options.force and git_common.is_dirty_git_tree('presubmit'):
4678 print('use --force to check even if tree is dirty.')
4679 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004680
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004681 cl = Changelist()
4682 if args:
4683 base_branch = args[0]
Bruce Dawsoneb8426e2022-08-05 23:58:15 +00004684 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004685 # Default to diffing against the common ancestor of the upstream branch.
4686 base_branch = cl.GetCommonAncestorWithUpstream()
Aaron Gable8076c282017-11-29 14:39:41 -08004687
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004688 start = time.time()
4689 try:
4690 if not 'PRESUBMIT_SKIP_NETWORK' in os.environ and cl.GetIssue():
4691 description = cl.FetchDescription()
4692 else:
4693 description = _create_description_from_log([base_branch])
4694 except Exception as e:
4695 print('Failed to fetch CL description - %s' % str(e))
4696 description = _create_description_from_log([base_branch])
4697 elapsed = time.time() - start
4698 if elapsed > 5:
4699 print('%.1f s to get CL description.' % elapsed)
Bruce Dawson13acea32022-05-03 22:13:08 +00004700
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004701 if not base_branch:
4702 if not options.force:
4703 print('use --force to check even when not on a branch.')
4704 return 1
4705 base_branch = 'HEAD'
4706
4707 cl.RunHook(committing=not options.upload,
4708 may_prompt=False,
4709 verbose=options.verbose,
4710 parallel=options.parallel,
4711 upstream=base_branch,
4712 description=description,
4713 all_files=options.all,
4714 files=options.files,
4715 resultdb=options.resultdb,
4716 realm=options.realm)
4717 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004718
4719
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004720def GenerateGerritChangeId(message):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004721 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004722
4723 Works the same way as
4724 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4725 but can be called on demand on all platforms.
4726
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004727 The basic idea is to generate git hash of a state of the tree, original
4728 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004729 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004730 lines = []
4731 tree_hash = RunGitSilent(['write-tree'])
4732 lines.append('tree %s' % tree_hash.strip())
4733 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'],
4734 suppress_stderr=False)
4735 if code == 0:
4736 lines.append('parent %s' % parent.strip())
4737 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4738 lines.append('author %s' % author.strip())
4739 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4740 lines.append('committer %s' % committer.strip())
4741 lines.append('')
4742 # Note: Gerrit's commit-hook actually cleans message of some lines and
4743 # whitespace. This code is not doing this, but it clearly won't decrease
4744 # entropy.
4745 lines.append(message)
4746 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4747 stdin=('\n'.join(lines)).encode())
4748 return 'I%s' % change_hash.strip()
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004749
4750
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004751def GetTargetRef(remote, remote_branch, target_branch):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004752 """Computes the remote branch ref to use for the CL.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004753
4754 Args:
4755 remote (str): The git remote for the CL.
4756 remote_branch (str): The git remote branch for the CL.
4757 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004758 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004759 if not (remote and remote_branch):
4760 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004761
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004762 if target_branch:
4763 # Canonicalize branch references to the equivalent local full symbolic
4764 # refs, which are then translated into the remote full symbolic refs
4765 # below.
4766 if '/' not in target_branch:
4767 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4768 else:
4769 prefix_replacements = (
4770 ('^((refs/)?remotes/)?branch-heads/',
4771 'refs/remotes/branch-heads/'),
4772 ('^((refs/)?remotes/)?%s/' % remote,
4773 'refs/remotes/%s/' % remote),
4774 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4775 )
4776 match = None
4777 for regex, replacement in prefix_replacements:
4778 match = re.search(regex, target_branch)
4779 if match:
4780 remote_branch = target_branch.replace(
4781 match.group(0), replacement)
4782 break
4783 if not match:
4784 # This is a branch path but not one we recognize; use as-is.
4785 remote_branch = target_branch
4786 # pylint: disable=consider-using-get
4787 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4788 # pylint: enable=consider-using-get
4789 # Handle the refs that need to land in different refs.
4790 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004791
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004792 # Create the true path to the remote branch.
4793 # Does the following translation:
4794 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4795 # * refs/remotes/origin/main -> refs/heads/main
4796 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4797 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4798 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4799 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4800 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4801 'refs/heads/')
4802 elif remote_branch.startswith('refs/remotes/branch-heads'):
4803 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004804
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004805 return remote_branch
wittman@chromium.org455dc922015-01-26 20:15:50 +00004806
4807
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004808def cleanup_list(l):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004809 """Fixes a list so that comma separated items are put as individual items.
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004810
4811 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4812 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4813 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004814 items = sum((i.split(',') for i in l), [])
4815 stripped_items = (i.strip() for i in items)
4816 return sorted(filter(None, stripped_items))
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004817
4818
Aaron Gable4db38df2017-11-03 14:59:07 -07004819@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004820@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004821def CMDupload(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004822 """Uploads the current changelist to codereview.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004823
4824 Can skip dependency patchset uploads for a branch by running:
4825 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004826 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00004827 git config --unset branch.branch_name.skip-deps-uploads
4828 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004829
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004830 If the name of the checked out branch starts with "bug-" or "fix-" followed
4831 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02004832 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004833
4834 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004835 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004836 [git-cl] add support for hashtags
4837 Foo bar: implement foo
4838 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004839 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004840 parser.add_option('--bypass-hooks',
4841 action='store_true',
4842 dest='bypass_hooks',
4843 help='bypass upload presubmit hook')
4844 parser.add_option('--bypass-watchlists',
4845 action='store_true',
4846 dest='bypass_watchlists',
4847 help='bypass watchlists auto CC-ing reviewers')
4848 parser.add_option('-f',
4849 '--force',
4850 action='store_true',
4851 dest='force',
4852 help="force yes to questions (don't prompt)")
4853 parser.add_option('--message',
4854 '-m',
4855 dest='message',
4856 help='message for patchset')
4857 parser.add_option('-b',
4858 '--bug',
4859 help='pre-populate the bug number(s) for this issue. '
4860 'If several, separate with commas')
4861 parser.add_option('--message-file',
4862 dest='message_file',
4863 help='file which contains message for patchset')
4864 parser.add_option('--title', '-t', dest='title', help='title for patchset')
4865 parser.add_option('-T',
4866 '--skip-title',
4867 action='store_true',
4868 dest='skip_title',
4869 help='Use the most recent commit message as the title of '
4870 'the patchset')
4871 parser.add_option('-r',
4872 '--reviewers',
4873 action='append',
4874 default=[],
4875 help='reviewer email addresses')
4876 parser.add_option('--cc',
4877 action='append',
4878 default=[],
4879 help='cc email addresses')
4880 parser.add_option('--hashtag',
4881 dest='hashtags',
4882 action='append',
4883 default=[],
4884 help=('Gerrit hashtag for new CL; '
4885 'can be applied multiple times'))
4886 parser.add_option('-s',
4887 '--send-mail',
4888 '--send-email',
4889 dest='send_mail',
4890 action='store_true',
4891 help='send email to reviewer(s) and cc(s) immediately')
4892 parser.add_option('--target_branch',
4893 '--target-branch',
4894 metavar='TARGET',
4895 help='Apply CL to remote ref TARGET. ' +
4896 'Default: remote branch head, or main')
4897 parser.add_option('--squash',
4898 action='store_true',
4899 help='Squash multiple commits into one')
4900 parser.add_option('--no-squash',
4901 action='store_false',
4902 dest='squash',
4903 help='Don\'t squash multiple commits into one')
4904 parser.add_option('--topic',
4905 default=None,
4906 help='Topic to specify when uploading')
4907 parser.add_option('--r-owners',
4908 dest='add_owners_to',
4909 action='store_const',
4910 const='R',
4911 help='add a set of OWNERS to R')
4912 parser.add_option('-c',
4913 '--use-commit-queue',
4914 action='store_true',
4915 default=False,
4916 help='tell the CQ to commit this patchset; '
4917 'implies --send-mail')
4918 parser.add_option('-d',
4919 '--cq-dry-run',
4920 action='store_true',
4921 default=False,
4922 help='Send the patchset to do a CQ dry run right after '
4923 'upload.')
4924 parser.add_option('--set-bot-commit',
4925 action='store_true',
4926 help=optparse.SUPPRESS_HELP)
4927 parser.add_option('--preserve-tryjobs',
4928 action='store_true',
4929 help='instruct the CQ to let tryjobs running even after '
4930 'new patchsets are uploaded instead of canceling '
4931 'prior patchset\' tryjobs')
4932 parser.add_option(
4933 '--dependencies',
4934 action='store_true',
4935 help='Uploads CLs of all the local branches that depend on '
4936 'the current branch')
4937 parser.add_option(
4938 '-a',
4939 '--enable-auto-submit',
4940 action='store_true',
4941 help='Sends your change to the CQ after an approval. Only '
4942 'works on repos that have the Auto-Submit label '
4943 'enabled')
4944 parser.add_option(
4945 '--parallel',
4946 action='store_true',
4947 help='Run all tests specified by input_api.RunTests in all '
4948 'PRESUBMIT files in parallel.')
4949 parser.add_option('--no-autocc',
4950 action='store_true',
4951 help='Disables automatic addition of CC emails')
4952 parser.add_option('--private',
4953 action='store_true',
4954 help='Set the review private. This implies --no-autocc.')
4955 parser.add_option('-R',
4956 '--retry-failed',
4957 action='store_true',
4958 help='Retry failed tryjobs from old patchset immediately '
4959 'after uploading new patchset. Cannot be used with '
4960 '--use-commit-queue or --cq-dry-run.')
4961 parser.add_option('--fixed',
4962 '-x',
4963 help='List of bugs that will be commented on and marked '
4964 'fixed (pre-populates "Fixed:" tag). Same format as '
4965 '-b option / "Bug:" tag. If fixing several issues, '
4966 'separate with commas.')
4967 parser.add_option('--edit-description',
4968 action='store_true',
4969 default=False,
4970 help='Modify description before upload. Cannot be used '
4971 'with --force. It is a noop when --no-squash is set '
4972 'or a new commit is created.')
4973 parser.add_option('--git-completion-helper',
4974 action="store_true",
4975 help=optparse.SUPPRESS_HELP)
4976 parser.add_option('-o',
4977 '--push-options',
4978 action='append',
4979 default=[],
4980 help='Transmit the given string to the server when '
4981 'performing git push (pass-through). See git-push '
4982 'documentation for more details.')
4983 parser.add_option('--no-add-changeid',
4984 action='store_true',
4985 dest='no_add_changeid',
4986 help='Do not add change-ids to messages.')
4987 parser.add_option('--cherry-pick-stacked',
4988 '--cp',
4989 dest='cherry_pick_stacked',
4990 action='store_true',
4991 help='If parent branch has un-uploaded updates, '
4992 'automatically skip parent branches and just upload '
4993 'the current branch cherry-pick on its parent\'s last '
4994 'uploaded commit. Allows users to skip the potential '
4995 'interactive confirmation step.')
4996 # TODO(b/265929888): Add --wip option of --cl-status option.
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004997
Mike Frysinger124bb8e2023-09-06 05:48:55 +00004998 orig_args = args
4999 (options, args) = parser.parse_args(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005000
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005001 if options.git_completion_helper:
5002 print(' '.join(opt.get_opt_string() for opt in parser.option_list
5003 if opt.help != optparse.SUPPRESS_HELP))
5004 return
Ng Zhi Ancdaf0be2020-05-27 20:57:28 +00005005
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005006 # TODO(crbug.com/1475405): Warn users if the project uses submodules and
5007 # they have fsmonitor enabled.
5008 if os.path.isfile('.gitmodules'):
5009 git_common.warn_submodule()
Aravind Vasudevanb8164182023-08-25 21:49:12 +00005010
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005011 if git_common.is_dirty_git_tree('upload'):
5012 return 1
ukai@chromium.orge8077812012-02-03 03:41:46 +00005013
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005014 options.reviewers = cleanup_list(options.reviewers)
5015 options.cc = cleanup_list(options.cc)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005016
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005017 if options.edit_description and options.force:
5018 parser.error('Only one of --force and --edit-description allowed')
Josipe827b0f2020-01-30 00:07:20 +00005019
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005020 if options.message_file:
5021 if options.message:
5022 parser.error('Only one of --message and --message-file allowed.')
5023 options.message = gclient_utils.FileRead(options.message_file)
tandriib80458a2016-06-23 12:20:07 -07005024
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005025 if ([options.cq_dry_run, options.use_commit_queue, options.retry_failed
5026 ].count(True) > 1):
5027 parser.error('Only one of --use-commit-queue, --cq-dry-run or '
5028 '--retry-failed is allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07005029
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005030 if options.skip_title and options.title:
5031 parser.error('Only one of --title and --skip-title allowed.')
Mario Bianuccicebfb4e2020-07-22 23:08:16 +00005032
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005033 if options.use_commit_queue:
5034 options.send_mail = True
Aaron Gableedbc4132017-09-11 13:22:28 -07005035
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005036 if options.squash is None:
5037 # Load default for user, repo, squash=true, in this order.
5038 options.squash = settings.GetSquashGerritUploads()
Edward Lesmes0dd54822020-03-26 18:24:25 +00005039
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005040 cl = Changelist(branchref=options.target_branch)
Joanna Wang5051ffe2023-03-01 22:24:07 +00005041
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005042 # Warm change details cache now to avoid RPCs later, reducing latency for
5043 # developers.
5044 if cl.GetIssue():
5045 cl._GetChangeDetail([
5046 'DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'
5047 ])
Joanna Wang5051ffe2023-03-01 22:24:07 +00005048
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005049 if options.retry_failed and not cl.GetIssue():
5050 print('No previous patchsets, so --retry-failed has no effect.')
5051 options.retry_failed = False
Joanna Wang5051ffe2023-03-01 22:24:07 +00005052
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005053 disable_dogfood_stacked_changes = os.environ.get(
5054 DOGFOOD_STACKED_CHANGES_VAR) == '0'
5055 dogfood_stacked_changes = os.environ.get(DOGFOOD_STACKED_CHANGES_VAR) == '1'
Joanna Wang5051ffe2023-03-01 22:24:07 +00005056
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005057 # Only print message for folks who don't have DOGFOOD_STACKED_CHANGES set
5058 # to an expected value.
5059 if (options.squash and not dogfood_stacked_changes
5060 and not disable_dogfood_stacked_changes):
5061 print(
5062 'This repo has been enrolled in the stacked changes dogfood.\n'
5063 '`git cl upload` now uploads the current branch and all upstream '
5064 'branches that have un-uploaded updates.\n'
5065 'Patches can now be reapplied with --force:\n'
5066 '`git cl patch --reapply --force`.\n'
5067 'Googlers may visit go/stacked-changes-dogfood for more information.\n'
5068 '\n'
5069 'Depot Tools no longer sets new uploads to "WIP". Please update the\n'
5070 '"Set new changes to "work in progress" by default" checkbox at\n'
5071 'https://<host>-review.googlesource.com/settings/\n'
5072 '\n'
5073 'To opt-out use `export DOGFOOD_STACKED_CHANGES=0`.\n'
5074 'To hide this message use `export DOGFOOD_STACKED_CHANGES=1`.\n'
5075 'File bugs at https://bit.ly/3Y6opoI\n')
Joanna Wang4786a412023-05-16 18:23:08 +00005076
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005077 if options.squash and not disable_dogfood_stacked_changes:
5078 if options.dependencies:
5079 parser.error(
5080 '--dependencies is not available for this dogfood workflow.')
Joanna Wang5051ffe2023-03-01 22:24:07 +00005081
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005082 if options.cherry_pick_stacked:
5083 try:
5084 orig_args.remove('--cherry-pick-stacked')
5085 except ValueError:
5086 orig_args.remove('--cp')
5087 UploadAllSquashed(options, orig_args)
5088 return 0
Joanna Wang18de1f62023-01-21 01:24:24 +00005089
Joanna Wangd75fc882023-03-01 21:53:34 +00005090 if options.cherry_pick_stacked:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005091 parser.error(
5092 '--cherry-pick-stacked is not available for this workflow.')
Joanna Wang18de1f62023-01-21 01:24:24 +00005093
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005094 # cl.GetMostRecentPatchset uses cached information, and can return the last
5095 # patchset before upload. Calling it here makes it clear that it's the
5096 # last patchset before upload. Note that GetMostRecentPatchset will fail
5097 # if no CL has been uploaded yet.
5098 if options.retry_failed:
5099 patchset = cl.GetMostRecentPatchset()
Joanna Wangd75fc882023-03-01 21:53:34 +00005100
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005101 ret = cl.CMDUpload(options, args, orig_args)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00005102
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005103 if options.retry_failed:
5104 if ret != 0:
5105 print('Upload failed, so --retry-failed has no effect.')
5106 return ret
5107 builds, _ = _fetch_latest_builds(cl,
5108 DEFAULT_BUILDBUCKET_HOST,
5109 latest_patchset=patchset)
5110 jobs = _filter_failed_for_retry(builds)
5111 if len(jobs) == 0:
5112 print('No failed tryjobs, so --retry-failed has no effect.')
5113 return ret
5114 _trigger_tryjobs(cl, jobs, options, patchset + 1)
Quinten Yearsleya19d3532019-09-30 21:54:39 +00005115
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005116 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00005117
5118
Daniel Cheng66d0f152023-08-29 23:21:58 +00005119def UploadAllSquashed(options: optparse.Values,
5120 orig_args: Sequence[str]) -> int:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005121 """Uploads the current and upstream branches (if necessary)."""
5122 cls, cherry_pick_current = _UploadAllPrecheck(options, orig_args)
Joanna Wang18de1f62023-01-21 01:24:24 +00005123
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005124 # Create commits.
5125 uploads_by_cl: List[Tuple[Changelist, _NewUpload]] = []
5126 if cherry_pick_current:
5127 parent = cls[1]._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
5128 new_upload = cls[0].PrepareCherryPickSquashedCommit(options, parent)
5129 uploads_by_cl.append((cls[0], new_upload))
5130 else:
5131 ordered_cls = list(reversed(cls))
Joanna Wangc710e2d2023-01-25 14:53:22 +00005132
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005133 cl = ordered_cls[0]
5134 # We can only support external changes when we're only uploading one
5135 # branch.
5136 parent = cl._UpdateWithExternalChanges() if len(
5137 ordered_cls) == 1 else None
5138 orig_parent = None
5139 if parent is None:
5140 origin = '.'
5141 branch = cl.GetBranch()
Joanna Wang74c53b62023-03-01 22:00:22 +00005142
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005143 while origin == '.':
5144 # Search for cl's closest ancestor with a gerrit hash.
5145 origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(
5146 branch)
5147 if origin == '.':
5148 upstream_branch = scm.GIT.ShortBranchName(
5149 upstream_branch_ref)
Joanna Wang7603f042023-03-01 22:17:36 +00005150
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005151 # Support the `git merge` and `git pull` workflow.
5152 if upstream_branch in ['master', 'main']:
5153 parent = cl.GetCommonAncestorWithUpstream()
5154 else:
5155 orig_parent = scm.GIT.GetBranchConfig(
5156 settings.GetRoot(), upstream_branch,
5157 LAST_UPLOAD_HASH_CONFIG_KEY)
5158 parent = scm.GIT.GetBranchConfig(
5159 settings.GetRoot(), upstream_branch,
5160 GERRIT_SQUASH_HASH_CONFIG_KEY)
5161 if parent:
5162 break
5163 branch = upstream_branch
5164 else:
5165 # Either the root of the tree is the cl's direct parent and the
5166 # while loop above only found empty branches between cl and the
5167 # root of the tree.
5168 parent = cl.GetCommonAncestorWithUpstream()
Joanna Wang6215dd02023-02-07 15:58:03 +00005169
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005170 if orig_parent is None:
5171 orig_parent = parent
5172 for i, cl in enumerate(ordered_cls):
5173 # If we're in the middle of the stack, set end_commit to
5174 # downstream's direct ancestor.
5175 if i + 1 < len(ordered_cls):
5176 child_base_commit = ordered_cls[
5177 i + 1].GetCommonAncestorWithUpstream()
5178 else:
5179 child_base_commit = None
5180 new_upload = cl.PrepareSquashedCommit(options,
5181 parent,
5182 orig_parent,
5183 end_commit=child_base_commit)
5184 uploads_by_cl.append((cl, new_upload))
5185 parent = new_upload.commit_to_push
5186 orig_parent = child_base_commit
Joanna Wangc710e2d2023-01-25 14:53:22 +00005187
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005188 # Create refspec options
5189 cl, new_upload = uploads_by_cl[-1]
5190 refspec_opts = cl._GetRefSpecOptions(
5191 options,
5192 new_upload.change_desc,
5193 multi_change_upload=len(uploads_by_cl) > 1,
5194 dogfood_path=True)
5195 refspec_suffix = ''
5196 if refspec_opts:
5197 refspec_suffix = '%' + ','.join(refspec_opts)
5198 assert ' ' not in refspec_suffix, (
5199 'spaces not allowed in refspec: "%s"' % refspec_suffix)
Joanna Wangc710e2d2023-01-25 14:53:22 +00005200
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005201 remote, remote_branch = cl.GetRemoteBranch()
5202 branch = GetTargetRef(remote, remote_branch, options.target_branch)
5203 refspec = '%s:refs/for/%s%s' % (new_upload.commit_to_push, branch,
5204 refspec_suffix)
Joanna Wangc710e2d2023-01-25 14:53:22 +00005205
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005206 # Git push
5207 git_push_metadata = {
5208 'gerrit_host':
5209 cl.GetGerritHost(),
5210 'title':
5211 options.title or '<untitled>',
5212 'change_id':
5213 git_footers.get_footer_change_id(new_upload.change_desc.description),
5214 'description':
5215 new_upload.change_desc.description,
5216 }
5217 push_stdout = cl._RunGitPushWithTraces(refspec, refspec_opts,
5218 git_push_metadata,
5219 options.push_options)
Joanna Wangc710e2d2023-01-25 14:53:22 +00005220
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005221 # Post push updates
5222 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
5223 change_numbers = [
5224 m.group(1) for m in map(regex.match, push_stdout.splitlines()) if m
5225 ]
Joanna Wangc710e2d2023-01-25 14:53:22 +00005226
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005227 for i, (cl, new_upload) in enumerate(uploads_by_cl):
5228 cl.PostUploadUpdates(options, new_upload, change_numbers[i])
Joanna Wangc710e2d2023-01-25 14:53:22 +00005229
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005230 return 0
Joanna Wang18de1f62023-01-21 01:24:24 +00005231
5232
5233def _UploadAllPrecheck(options, orig_args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005234 # type: (optparse.Values, Sequence[str]) -> Tuple[Sequence[Changelist],
5235 # bool]
5236 """Checks the state of the tree and gives the user uploading options
Joanna Wang18de1f62023-01-21 01:24:24 +00005237
5238 Returns: A tuple of the ordered list of changes that have new commits
5239 since their last upload and a boolean of whether the user wants to
5240 cherry-pick and upload the current branch instead of uploading all cls.
5241 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005242 cl = Changelist()
5243 if cl.GetBranch() is None:
5244 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
Joanna Wang6b98cdc2023-02-16 00:37:20 +00005245
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005246 branch_ref = None
5247 cls = []
5248 must_upload_upstream = False
5249 first_pass = True
Joanna Wang18de1f62023-01-21 01:24:24 +00005250
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005251 Changelist._GerritCommitMsgHookCheck(offer_removal=not options.force)
Joanna Wang18de1f62023-01-21 01:24:24 +00005252
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005253 while True:
5254 if len(cls) > _MAX_STACKED_BRANCHES_UPLOAD:
5255 DieWithError(
5256 'More than %s branches in the stack have not been uploaded.\n'
5257 'Are your branches in a misconfigured state?\n'
5258 'If not, please upload some upstream changes first.' %
5259 (_MAX_STACKED_BRANCHES_UPLOAD))
Joanna Wang18de1f62023-01-21 01:24:24 +00005260
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005261 cl = Changelist(branchref=branch_ref)
Joanna Wang18de1f62023-01-21 01:24:24 +00005262
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005263 # Only add CL if it has anything to commit.
5264 base_commit = cl.GetCommonAncestorWithUpstream()
5265 end_commit = RunGit(['rev-parse', cl.GetBranchRef()]).strip()
Joanna Wang6215dd02023-02-07 15:58:03 +00005266
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005267 commit_summary = _GetCommitCountSummary(base_commit, end_commit)
5268 if commit_summary:
5269 cls.append(cl)
5270 if (not first_pass and
5271 cl._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
5272 is None):
5273 # We are mid-stack and the user must upload their upstream
5274 # branches.
5275 must_upload_upstream = True
5276 print(f'Found change with {commit_summary}...')
5277 elif first_pass: # The current branch has nothing to commit. Exit.
5278 DieWithError('Branch %s has nothing to commit' % cl.GetBranch())
5279 # Else: A mid-stack branch has nothing to commit. We do not add it to
5280 # cls.
5281 first_pass = False
Joanna Wang6215dd02023-02-07 15:58:03 +00005282
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005283 # Cases below determine if we should continue to traverse up the tree.
5284 origin, upstream_branch_ref = Changelist.FetchUpstreamTuple(
5285 cl.GetBranch())
5286 branch_ref = upstream_branch_ref # set branch for next run.
Joanna Wang18de1f62023-01-21 01:24:24 +00005287
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005288 upstream_branch = scm.GIT.ShortBranchName(upstream_branch_ref)
5289 upstream_last_upload = scm.GIT.GetBranchConfig(
5290 settings.GetRoot(), upstream_branch, LAST_UPLOAD_HASH_CONFIG_KEY)
Joanna Wang6215dd02023-02-07 15:58:03 +00005291
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005292 # Case 1: We've reached the beginning of the tree.
5293 if origin != '.':
5294 break
Joanna Wang18de1f62023-01-21 01:24:24 +00005295
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005296 # Case 2: If any upstream branches have never been uploaded,
5297 # the user MUST upload them unless they are empty. Continue to
5298 # next loop to add upstream if it is not empty.
5299 if not upstream_last_upload:
5300 continue
Joanna Wang18de1f62023-01-21 01:24:24 +00005301
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005302 # Case 3: If upstream's last_upload == cl.base_commit we do
5303 # not need to upload any more upstreams from this point on.
5304 # (Even if there may be diverged branches higher up the tree)
5305 if base_commit == upstream_last_upload:
5306 break
Joanna Wang18de1f62023-01-21 01:24:24 +00005307
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005308 # Case 4: If upstream's last_upload < cl.base_commit we are
5309 # uploading cl and upstream_cl.
5310 # Continue up the tree to check other branch relations.
5311 if scm.GIT.IsAncestor(upstream_last_upload, base_commit):
5312 continue
Joanna Wang18de1f62023-01-21 01:24:24 +00005313
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005314 # Case 5: If cl.base_commit < upstream's last_upload the user
5315 # must rebase before uploading.
5316 if scm.GIT.IsAncestor(base_commit, upstream_last_upload):
5317 DieWithError(
5318 'At least one branch in the stack has diverged from its upstream '
5319 'branch and does not contain its upstream\'s last upload.\n'
5320 'Please rebase the stack with `git rebase-update` before uploading.'
5321 )
Joanna Wang18de1f62023-01-21 01:24:24 +00005322
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005323 # The tree went through a rebase. LAST_UPLOAD_HASH_CONFIG_KEY no longer
5324 # has any relation to commits in the tree. Continue up the tree until we
5325 # hit the root.
Joanna Wang18de1f62023-01-21 01:24:24 +00005326
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005327 # We assume all cls in the stack have the same auth requirements and only
5328 # check this once.
5329 cls[0].EnsureAuthenticated(force=options.force)
Joanna Wang18de1f62023-01-21 01:24:24 +00005330
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005331 cherry_pick = False
5332 if len(cls) > 1:
5333 opt_message = ''
5334 branches = ', '.join([cl.branch for cl in cls])
5335 if len(orig_args):
5336 opt_message = ('options %s will be used for all uploads.\n' %
5337 orig_args)
5338 if must_upload_upstream:
5339 msg = ('At least one parent branch in `%s` has never been uploaded '
5340 'and must be uploaded before/with `%s`.\n' %
5341 (branches, cls[1].branch))
5342 if options.cherry_pick_stacked:
5343 DieWithError(msg)
5344 if not options.force:
5345 confirm_or_exit('\n' + opt_message + msg)
5346 else:
5347 if options.cherry_pick_stacked:
5348 print('cherry-picking `%s` on %s\'s last upload' %
5349 (cls[0].branch, cls[1].branch))
5350 cherry_pick = True
5351 elif not options.force:
5352 answer = gclient_utils.AskForData(
5353 '\n' + opt_message +
5354 'Press enter to update branches %s.\nOr type `n` to upload only '
5355 '`%s` cherry-picked on %s\'s last upload:' %
5356 (branches, cls[0].branch, cls[1].branch))
5357 if answer.lower() == 'n':
5358 cherry_pick = True
5359 return cls, cherry_pick
Joanna Wang18de1f62023-01-21 01:24:24 +00005360
5361
Francois Dorayd42c6812017-05-30 15:10:20 -04005362@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005363@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04005364def CMDsplit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005365 """Splits a branch into smaller branches and uploads CLs.
Francois Dorayd42c6812017-05-30 15:10:20 -04005366
5367 Creates a branch and uploads a CL for each group of files modified in the
5368 current branch that share a common OWNERS file. In the CL description and
Edward Lemurac5c55f2020-02-29 00:17:16 +00005369 comment, the string '$directory', is replaced with the directory containing
5370 the shared OWNERS file.
Francois Dorayd42c6812017-05-30 15:10:20 -04005371 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005372 parser.add_option('-d',
5373 '--description',
5374 dest='description_file',
5375 help='A text file containing a CL description in which '
5376 '$directory will be replaced by each CL\'s directory.')
5377 parser.add_option('-c',
5378 '--comment',
5379 dest='comment_file',
5380 help='A text file containing a CL comment.')
5381 parser.add_option(
5382 '-n',
5383 '--dry-run',
5384 dest='dry_run',
5385 action='store_true',
5386 default=False,
5387 help='List the files and reviewers for each CL that would '
5388 'be created, but don\'t create branches or CLs.')
5389 parser.add_option('--cq-dry-run',
5390 action='store_true',
5391 help='If set, will do a cq dry run for each uploaded CL. '
5392 'Please be careful when doing this; more than ~10 CLs '
5393 'has the potential to overload our build '
5394 'infrastructure. Try to upload these not during high '
5395 'load times (usually 11-3 Mountain View time). Email '
5396 'infra-dev@chromium.org with any questions.')
5397 parser.add_option(
5398 '-a',
5399 '--enable-auto-submit',
5400 action='store_true',
5401 dest='enable_auto_submit',
5402 default=True,
5403 help='Sends your change to the CQ after an approval. Only '
5404 'works on repos that have the Auto-Submit label '
5405 'enabled')
5406 parser.add_option(
5407 '--disable-auto-submit',
5408 action='store_false',
5409 dest='enable_auto_submit',
5410 help='Disables automatic sending of the changes to the CQ '
5411 'after approval. Note that auto-submit only works for '
5412 'repos that have the Auto-Submit label enabled.')
5413 parser.add_option('--max-depth',
5414 type='int',
5415 default=0,
5416 help='The max depth to look for OWNERS files. Useful for '
5417 'controlling the granularity of the split CLs, e.g. '
5418 '--max-depth=1 will only split by top-level '
5419 'directory. Specifying a value less than 1 means no '
5420 'limit on max depth.')
5421 parser.add_option('--topic',
5422 default=None,
5423 help='Topic to specify when uploading')
5424 options, _ = parser.parse_args(args)
Francois Dorayd42c6812017-05-30 15:10:20 -04005425
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005426 if not options.description_file:
5427 parser.error('No --description flag specified.')
Francois Dorayd42c6812017-05-30 15:10:20 -04005428
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005429 def WrappedCMDupload(args):
5430 return CMDupload(OptionParser(), args)
Francois Dorayd42c6812017-05-30 15:10:20 -04005431
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005432 return split_cl.SplitCl(options.description_file, options.comment_file,
5433 Changelist, WrappedCMDupload, options.dry_run,
5434 options.cq_dry_run, options.enable_auto_submit,
5435 options.max_depth, options.topic,
5436 settings.GetRoot())
Francois Dorayd42c6812017-05-30 15:10:20 -04005437
5438
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005439@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005440@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005441def CMDdcommit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005442 """DEPRECATED: Used to commit the current changelist via git-svn."""
5443 message = ('git-cl no longer supports committing to SVN repositories via '
5444 'git-svn. You probably want to use `git cl land` instead.')
5445 print(message)
5446 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005447
5448
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005449@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005450@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005451def CMDland(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005452 """Commits the current changelist via git.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005453
5454 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5455 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005456 """
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005457 parser.add_option('--bypass-hooks',
5458 action='store_true',
5459 dest='bypass_hooks',
5460 help='bypass upload presubmit hook')
5461 parser.add_option('-f',
5462 '--force',
5463 action='store_true',
5464 dest='force',
5465 help="force yes to questions (don't prompt)")
5466 parser.add_option(
5467 '--parallel',
5468 action='store_true',
5469 help='Run all tests specified by input_api.RunTests in all '
5470 'PRESUBMIT files in parallel.')
5471 parser.add_option('--resultdb',
5472 action='store_true',
5473 help='Run presubmit checks in the ResultSink environment '
5474 'and send results to the ResultDB database.')
5475 parser.add_option('--realm', help='LUCI realm if reporting to ResultDB')
5476 (options, args) = parser.parse_args(args)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005477
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005478 cl = Changelist()
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005479
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005480 if not cl.GetIssue():
5481 DieWithError('You must upload the change first to Gerrit.\n'
5482 ' If you would rather have `git cl land` upload '
5483 'automatically for you, see http://crbug.com/642759')
5484 return cl.CMDLand(options.force, options.bypass_hooks, options.verbose,
5485 options.parallel, options.resultdb, options.realm)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005486
5487
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005488@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005489@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005490def CMDpatch(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005491 """Applies (cherry-picks) a Gerrit changelist locally."""
5492 parser.add_option('-b',
5493 dest='newbranch',
5494 help='create a new branch off trunk for the patch')
5495 parser.add_option('-f',
5496 '--force',
5497 action='store_true',
5498 help='overwrite state on the current or chosen branch')
5499 parser.add_option('-n',
5500 '--no-commit',
5501 action='store_true',
5502 dest='nocommit',
5503 help='don\'t commit after patch applies.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005504
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005505 group = optparse.OptionGroup(
5506 parser,
5507 'Options for continuing work on the current issue uploaded from a '
5508 'different clone (e.g. different machine). Must be used independently '
5509 'from the other options. No issue number should be specified, and the '
5510 'branch must have an issue number associated with it')
5511 group.add_option('--reapply',
5512 action='store_true',
5513 dest='reapply',
5514 help='Reset the branch and reapply the issue.\n'
5515 'CAUTION: This will undo any local changes in this '
5516 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005517
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005518 group.add_option('--pull',
5519 action='store_true',
5520 dest='pull',
5521 help='Performs a pull before reapplying.')
5522 parser.add_option_group(group)
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005523
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005524 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005525
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005526 if options.reapply:
5527 if options.newbranch:
5528 parser.error('--reapply works on the current branch only.')
5529 if len(args) > 0:
5530 parser.error('--reapply implies no additional arguments.')
5531
5532 cl = Changelist()
5533 if not cl.GetIssue():
5534 parser.error('Current branch must have an associated issue.')
5535
5536 upstream = cl.GetUpstreamBranch()
5537 if upstream is None:
5538 parser.error('No upstream branch specified. Cannot reset branch.')
5539
5540 RunGit(['reset', '--hard', upstream])
5541 if options.pull:
5542 RunGit(['pull'])
5543
5544 target_issue_arg = ParseIssueNumberArgument(cl.GetIssue())
5545 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit,
5546 options.force, False)
5547
5548 if len(args) != 1 or not args[0]:
5549 parser.error('Must specify issue number or URL.')
5550
5551 target_issue_arg = ParseIssueNumberArgument(args[0])
5552 if not target_issue_arg.valid:
5553 parser.error('Invalid issue ID or URL.')
5554
5555 # We don't want uncommitted changes mixed up with the patch.
5556 if git_common.is_dirty_git_tree('patch'):
5557 return 1
5558
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005559 if options.newbranch:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005560 if options.force:
5561 RunGit(['branch', '-D', options.newbranch],
5562 stderr=subprocess2.PIPE,
5563 error_ok=True)
5564 git_new_branch.create_new_branch(options.newbranch)
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005565
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005566 cl = Changelist(codereview_host=target_issue_arg.hostname,
5567 issue=target_issue_arg.issue)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005568
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005569 if not args[0].isdigit():
5570 print('canonical issue/change URL: %s\n' % cl.GetIssueURL())
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005571
Joanna Wang44e9bee2023-01-25 21:51:42 +00005572 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.nocommit,
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005573 options.force, options.newbranch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005574
5575
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005576def GetTreeStatus(url=None):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005577 """Fetches the tree status and returns either 'open', 'closed',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005578 'unknown' or 'unset'."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005579 url = url or settings.GetTreeStatusUrl(error_ok=True)
5580 if url:
5581 status = str(urllib.request.urlopen(url).read().lower())
5582 if status.find('closed') != -1 or status == '0':
5583 return 'closed'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005584
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005585 if status.find('open') != -1 or status == '1':
5586 return 'open'
Aravind Vasudevanc5f0cbb2022-01-24 23:56:57 +00005587
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005588 return 'unknown'
5589 return 'unset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005590
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005591
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005592def GetTreeStatusReason():
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005593 """Fetches the tree status from a json url and returns the message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005594 with the reason for the tree to be opened or closed."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005595 url = settings.GetTreeStatusUrl()
5596 json_url = urllib.parse.urljoin(url, '/current?format=json')
5597 connection = urllib.request.urlopen(json_url)
5598 status = json.loads(connection.read())
5599 connection.close()
5600 return status['message']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005601
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005602
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005603@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005604def CMDtree(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005605 """Shows the status of the tree."""
5606 _, args = parser.parse_args(args)
5607 status = GetTreeStatus()
5608 if 'unset' == status:
5609 print(
5610 'You must configure your tree status URL by running "git cl config".'
5611 )
5612 return 2
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005613
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005614 print('The tree is %s' % status)
5615 print()
5616 print(GetTreeStatusReason())
5617 if status != 'open':
5618 return 1
5619 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005620
5621
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005622@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005623def CMDtry(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005624 """Triggers tryjobs using either Buildbucket or CQ dry run."""
5625 group = optparse.OptionGroup(parser, 'Tryjob options')
5626 group.add_option(
5627 '-b',
5628 '--bot',
5629 action='append',
5630 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5631 'times to specify multiple builders. ex: '
5632 '"-b win_rel -b win_layout". See '
5633 'the try server waterfall for the builders name and the tests '
5634 'available.'))
5635 group.add_option(
5636 '-B',
5637 '--bucket',
5638 default='',
5639 help=('Buildbucket bucket to send the try requests. Format: '
5640 '"luci.$LUCI_PROJECT.$LUCI_BUCKET". eg: "luci.chromium.try"'))
5641 group.add_option(
5642 '-r',
5643 '--revision',
5644 help='Revision to use for the tryjob; default: the revision will '
5645 'be determined by the try recipe that builder runs, which usually '
5646 'defaults to HEAD of origin/master or origin/main')
5647 group.add_option(
5648 '-c',
5649 '--clobber',
5650 action='store_true',
5651 default=False,
5652 help='Force a clobber before building; that is don\'t do an '
5653 'incremental build')
5654 group.add_option('--category',
5655 default='git_cl_try',
5656 help='Specify custom build category.')
5657 group.add_option(
5658 '--project',
5659 help='Override which project to use. Projects are defined '
5660 'in recipe to determine to which repository or directory to '
5661 'apply the patch')
5662 group.add_option(
5663 '-p',
5664 '--property',
5665 dest='properties',
5666 action='append',
5667 default=[],
5668 help='Specify generic properties in the form -p key1=value1 -p '
5669 'key2=value2 etc. The value will be treated as '
5670 'json if decodable, or as string otherwise. '
5671 'NOTE: using this may make your tryjob not usable for CQ, '
5672 'which will then schedule another tryjob with default properties')
5673 group.add_option('--buildbucket-host',
5674 default='cr-buildbucket.appspot.com',
5675 help='Host of buildbucket. The default host is %default.')
5676 parser.add_option_group(group)
5677 parser.add_option('-R',
5678 '--retry-failed',
5679 action='store_true',
5680 default=False,
5681 help='Retry failed jobs from the latest set of tryjobs. '
5682 'Not allowed with --bucket and --bot options.')
5683 parser.add_option(
5684 '-i',
5685 '--issue',
5686 type=int,
5687 help='Operate on this issue instead of the current branch\'s implicit '
5688 'issue.')
5689 options, args = parser.parse_args(args)
maruel@chromium.org15192402012-09-06 12:38:29 +00005690
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005691 # Make sure that all properties are prop=value pairs.
5692 bad_params = [x for x in options.properties if '=' not in x]
5693 if bad_params:
5694 parser.error('Got properties with missing "=": %s' % bad_params)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005695
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005696 if args:
5697 parser.error('Unknown arguments: %s' % args)
maruel@chromium.org15192402012-09-06 12:38:29 +00005698
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005699 cl = Changelist(issue=options.issue)
5700 if not cl.GetIssue():
5701 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005702
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005703 # HACK: warm up Gerrit change detail cache to save on RPCs.
5704 cl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005705
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005706 error_message = cl.CannotTriggerTryJobReason()
5707 if error_message:
5708 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005709
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005710 if options.bot:
5711 if options.retry_failed:
5712 parser.error('--bot is not compatible with --retry-failed.')
5713 if not options.bucket:
5714 parser.error('A bucket (e.g. "chromium/try") is required.')
Edward Lemur45768512020-03-02 19:03:14 +00005715
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005716 triggered = [b for b in options.bot if 'triggered' in b]
5717 if triggered:
5718 parser.error(
5719 'Cannot schedule builds on triggered bots: %s.\n'
5720 'This type of bot requires an initial job from a parent (usually a '
5721 'builder). Schedule a job on the parent instead.\n' % triggered)
Edward Lemur45768512020-03-02 19:03:14 +00005722
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005723 if options.bucket.startswith('.master'):
5724 parser.error('Buildbot masters are not supported.')
Edward Lemur45768512020-03-02 19:03:14 +00005725
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005726 project, bucket = _parse_bucket(options.bucket)
5727 if project is None or bucket is None:
5728 parser.error('Invalid bucket: %s.' % options.bucket)
5729 jobs = sorted((project, bucket, bot) for bot in options.bot)
5730 elif options.retry_failed:
5731 print('Searching for failed tryjobs...')
5732 builds, patchset = _fetch_latest_builds(cl, DEFAULT_BUILDBUCKET_HOST)
5733 if options.verbose:
5734 print('Got %d builds in patchset #%d' % (len(builds), patchset))
5735 jobs = _filter_failed_for_retry(builds)
5736 if not jobs:
5737 print('There are no failed jobs in the latest set of jobs '
5738 '(patchset #%d), doing nothing.' % patchset)
5739 return 0
5740 num_builders = len(jobs)
5741 if num_builders > 10:
5742 confirm_or_exit('There are %d builders with failed builds.' %
5743 num_builders,
5744 action='continue')
5745 else:
5746 if options.verbose:
5747 print('git cl try with no bots now defaults to CQ dry run.')
5748 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5749 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005750
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005751 patchset = cl.GetMostRecentPatchset()
5752 try:
5753 _trigger_tryjobs(cl, jobs, options, patchset)
5754 except BuildbucketResponseException as ex:
5755 print('ERROR: %s' % ex)
5756 return 1
5757 return 0
maruel@chromium.org15192402012-09-06 12:38:29 +00005758
5759
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005760@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005761def CMDtry_results(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005762 """Prints info about results for tryjobs associated with the current CL."""
5763 group = optparse.OptionGroup(parser, 'Tryjob results options')
5764 group.add_option('-p',
5765 '--patchset',
5766 type=int,
5767 help='patchset number if not current.')
5768 group.add_option('--print-master',
5769 action='store_true',
5770 help='print master name as well.')
5771 group.add_option('--color',
5772 action='store_true',
5773 default=setup_color.IS_TTY,
5774 help='force color output, useful when piping output.')
5775 group.add_option('--buildbucket-host',
5776 default='cr-buildbucket.appspot.com',
5777 help='Host of buildbucket. The default host is %default.')
5778 group.add_option(
5779 '--json',
5780 help=('Path of JSON output file to write tryjob results to,'
5781 'or "-" for stdout.'))
5782 parser.add_option_group(group)
5783 parser.add_option(
5784 '-i',
5785 '--issue',
5786 type=int,
5787 help='Operate on this issue instead of the current branch\'s implicit '
5788 'issue.')
5789 options, args = parser.parse_args(args)
5790 if args:
5791 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005792
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005793 cl = Changelist(issue=options.issue)
5794 if not cl.GetIssue():
5795 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005796
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005797 patchset = options.patchset
tandrii221ab252016-10-06 08:12:04 -07005798 if not patchset:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005799 patchset = cl.GetMostRecentDryRunPatchset()
5800 if not patchset:
5801 parser.error('Code review host doesn\'t know about issue %s. '
5802 'No access to issue or wrong issue number?\n'
5803 'Either upload first, or pass --patchset explicitly.' %
5804 cl.GetIssue())
tandrii221ab252016-10-06 08:12:04 -07005805
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005806 try:
5807 jobs = _fetch_tryjobs(cl, DEFAULT_BUILDBUCKET_HOST, patchset)
5808 except BuildbucketResponseException as ex:
5809 print('Buildbucket error: %s' % ex)
5810 return 1
5811 if options.json:
5812 write_json(options.json, jobs)
5813 else:
5814 _print_tryjobs(options, jobs)
5815 return 0
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005816
5817
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005818@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005819@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005820def CMDupstream(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005821 """Prints or sets the name of the upstream branch, if any."""
5822 _, args = parser.parse_args(args)
5823 if len(args) > 1:
5824 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005825
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005826 cl = Changelist()
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005827 if args:
5828 # One arg means set upstream branch.
5829 branch = cl.GetBranch()
5830 RunGit(['branch', '--set-upstream-to', args[0], branch])
5831 cl = Changelist()
5832 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(), ))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005833
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005834 # Clear configured merge-base, if there is one.
5835 git_common.remove_merge_base(branch)
5836 else:
5837 print(cl.GetUpstreamBranch())
5838 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005839
5840
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005841@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005842def CMDweb(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005843 """Opens the current CL in the web browser."""
5844 parser.add_option('-p',
5845 '--print-only',
5846 action='store_true',
5847 dest='print_only',
5848 help='Only print the Gerrit URL, don\'t open it in the '
5849 'browser.')
5850 (options, args) = parser.parse_args(args)
5851 if args:
5852 parser.error('Unrecognized args: %s' % ' '.join(args))
thestig@chromium.org00858c82013-12-02 23:08:03 +00005853
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005854 issue_url = Changelist().GetIssueURL()
5855 if not issue_url:
5856 print('ERROR No issue to open', file=sys.stderr)
5857 return 1
thestig@chromium.org00858c82013-12-02 23:08:03 +00005858
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005859 if options.print_only:
5860 print(issue_url)
5861 return 0
5862
5863 # Redirect I/O before invoking browser to hide its output. For example, this
5864 # allows us to hide the "Created new window in existing browser session."
5865 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
5866 saved_stdout = os.dup(1)
5867 saved_stderr = os.dup(2)
5868 os.close(1)
5869 os.close(2)
5870 os.open(os.devnull, os.O_RDWR)
5871 try:
5872 webbrowser.open(issue_url)
5873 finally:
5874 os.dup2(saved_stdout, 1)
5875 os.dup2(saved_stderr, 2)
Orr Bernstein0b960582022-12-22 20:16:18 +00005876 return 0
5877
thestig@chromium.org00858c82013-12-02 23:08:03 +00005878
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005879@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005880def CMDset_commit(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005881 """Sets the commit bit to trigger the CQ."""
5882 parser.add_option('-d',
5883 '--dry-run',
5884 action='store_true',
5885 help='trigger in dry run mode')
5886 parser.add_option('-c',
5887 '--clear',
5888 action='store_true',
5889 help='stop CQ run, if any')
5890 parser.add_option(
5891 '-i',
5892 '--issue',
5893 type=int,
5894 help='Operate on this issue instead of the current branch\'s implicit '
5895 'issue.')
5896 options, args = parser.parse_args(args)
5897 if args:
5898 parser.error('Unrecognized args: %s' % ' '.join(args))
5899 if [options.dry_run, options.clear].count(True) > 1:
5900 parser.error('Only one of --dry-run, and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005901
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005902 cl = Changelist(issue=options.issue)
5903 if not cl.GetIssue():
5904 parser.error('Must upload the issue first.')
Greg Gutermanbe5fccd2021-06-14 17:58:20 +00005905
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005906 if options.clear:
5907 state = _CQState.NONE
5908 elif options.dry_run:
5909 state = _CQState.DRY_RUN
5910 else:
5911 state = _CQState.COMMIT
5912 cl.SetCQState(state)
5913 return 0
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005914
5915
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005916@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005917def CMDset_close(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005918 """Closes the issue."""
5919 parser.add_option(
5920 '-i',
5921 '--issue',
5922 type=int,
5923 help='Operate on this issue instead of the current branch\'s implicit '
5924 'issue.')
5925 options, args = parser.parse_args(args)
5926 if args:
5927 parser.error('Unrecognized args: %s' % ' '.join(args))
5928 cl = Changelist(issue=options.issue)
5929 # Ensure there actually is an issue to close.
5930 if not cl.GetIssue():
5931 DieWithError('ERROR: No issue to close.')
5932 cl.CloseIssue()
5933 return 0
groby@chromium.org411034a2013-02-26 15:12:01 +00005934
5935
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005936@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005937def CMDdiff(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005938 """Shows differences between local tree and last upload."""
5939 parser.add_option('--stat',
5940 action='store_true',
5941 dest='stat',
5942 help='Generate a diffstat')
5943 options, args = parser.parse_args(args)
5944 if args:
5945 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005946
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005947 cl = Changelist()
5948 issue = cl.GetIssue()
5949 branch = cl.GetBranch()
5950 if not issue:
5951 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005952
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005953 base = cl._GitGetBranchConfigValue(LAST_UPLOAD_HASH_CONFIG_KEY)
5954 if not base:
5955 base = cl._GitGetBranchConfigValue(GERRIT_SQUASH_HASH_CONFIG_KEY)
5956 if not base:
5957 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5958 revision_info = detail['revisions'][detail['current_revision']]
5959 fetch_info = revision_info['fetch']['http']
5960 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5961 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005962
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005963 cmd = ['git', 'diff']
5964 if options.stat:
5965 cmd.append('--stat')
5966 cmd.append(base)
5967 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005968
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005969 return 0
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005970
5971
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005972@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005973def CMDowners(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005974 """Finds potential owners for reviewing."""
5975 parser.add_option(
5976 '--ignore-current',
5977 action='store_true',
5978 help='Ignore the CL\'s current reviewers and start from scratch.')
5979 parser.add_option('--ignore-self',
5980 action='store_true',
5981 help='Do not consider CL\'s author as an owners.')
5982 parser.add_option('--no-color',
5983 action='store_true',
5984 help='Use this option to disable color output')
5985 parser.add_option('--batch',
5986 action='store_true',
5987 help='Do not run interactively, just suggest some')
5988 # TODO: Consider moving this to another command, since other
5989 # git-cl owners commands deal with owners for a given CL.
5990 parser.add_option('--show-all',
5991 action='store_true',
5992 help='Show all owners for a particular file')
5993 options, args = parser.parse_args(args)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005994
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005995 cl = Changelist()
5996 author = cl.GetAuthor()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005997
Mike Frysinger124bb8e2023-09-06 05:48:55 +00005998 if options.show_all:
5999 if len(args) == 0:
6000 print('No files specified for --show-all. Nothing to do.')
6001 return 0
6002 owners_by_path = cl.owners_client.BatchListOwners(args)
6003 for path in args:
6004 print('Owners for %s:' % path)
6005 print('\n'.join(
6006 ' - %s' % owner
6007 for owner in owners_by_path.get(path, ['No owners found'])))
6008 return 0
Yang Guo6e269a02019-06-26 11:17:02 +00006009
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006010 if args:
6011 if len(args) > 1:
6012 parser.error('Unknown args.')
6013 base_branch = args[0]
6014 else:
6015 # Default to diffing against the common ancestor of the upstream branch.
6016 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00006017
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006018 affected_files = cl.GetAffectedFiles(base_branch)
Dirk Prankebf980882017-09-02 15:08:00 -07006019
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006020 if options.batch:
6021 owners = cl.owners_client.SuggestOwners(affected_files,
6022 exclude=[author])
6023 print('\n'.join(owners))
6024 return 0
Dirk Prankebf980882017-09-02 15:08:00 -07006025
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006026 return owners_finder.OwnersFinder(
6027 affected_files,
6028 author, [] if options.ignore_current else cl.GetReviewers(),
6029 cl.owners_client,
6030 disable_color=options.no_color,
6031 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00006032
6033
Aiden Bennerc08566e2018-10-03 17:52:42 +00006034def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006035 """Generates a diff command."""
6036 # Generate diff for the current branch's changes.
6037 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
Aiden Bennerc08566e2018-10-03 17:52:42 +00006038
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006039 if allow_prefix:
6040 # explicitly setting --src-prefix and --dst-prefix is necessary in the
6041 # case that diff.noprefix is set in the user's git config.
6042 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
6043 else:
6044 diff_cmd += ['--no-prefix']
Aiden Bennerc08566e2018-10-03 17:52:42 +00006045
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006046 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006047
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006048 if args:
6049 for arg in args:
6050 if os.path.isdir(arg) or os.path.isfile(arg):
6051 diff_cmd.append(arg)
6052 else:
6053 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006054
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006055 return diff_cmd
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006056
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006057
Jamie Madill5e96ad12020-01-13 16:08:35 +00006058def _RunClangFormatDiff(opts, clang_diff_files, top_dir, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006059 """Runs clang-format-diff and sets a return value if necessary."""
Jamie Madill5e96ad12020-01-13 16:08:35 +00006060
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006061 if not clang_diff_files:
6062 return 0
Jamie Madill5e96ad12020-01-13 16:08:35 +00006063
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006064 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
6065 # formatted. This is used to block during the presubmit.
6066 return_value = 0
Jamie Madill5e96ad12020-01-13 16:08:35 +00006067
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006068 # Locate the clang-format binary in the checkout
Jamie Madill5e96ad12020-01-13 16:08:35 +00006069 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006070 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
Jamie Madill5e96ad12020-01-13 16:08:35 +00006071 except clang_format.NotFoundError as e:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006072 DieWithError(e)
Jamie Madill5e96ad12020-01-13 16:08:35 +00006073
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006074 if opts.full or settings.GetFormatFullByDefault():
6075 cmd = [clang_format_tool]
6076 if not opts.dry_run and not opts.diff:
6077 cmd.append('-i')
6078 if opts.dry_run:
6079 for diff_file in clang_diff_files:
6080 with open(diff_file, 'r') as myfile:
6081 code = myfile.read().replace('\r\n', '\n')
6082 stdout = RunCommand(cmd + [diff_file], cwd=top_dir)
6083 stdout = stdout.replace('\r\n', '\n')
6084 if opts.diff:
6085 sys.stdout.write(stdout)
6086 if code != stdout:
6087 return_value = 2
6088 else:
6089 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
6090 if opts.diff:
6091 sys.stdout.write(stdout)
6092 else:
6093 try:
6094 script = clang_format.FindClangFormatScriptInChromiumTree(
6095 'clang-format-diff.py')
6096 except clang_format.NotFoundError as e:
6097 DieWithError(e)
Jamie Madill5e96ad12020-01-13 16:08:35 +00006098
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006099 cmd = ['vpython3', script, '-p0']
6100 if not opts.dry_run and not opts.diff:
6101 cmd.append('-i')
Jamie Madill5e96ad12020-01-13 16:08:35 +00006102
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006103 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
6104 diff_output = RunGit(diff_cmd).encode('utf-8')
Jamie Madill5e96ad12020-01-13 16:08:35 +00006105
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006106 env = os.environ.copy()
6107 env['PATH'] = (str(os.path.dirname(clang_format_tool)) + os.pathsep +
6108 env['PATH'])
6109 stdout = RunCommand(cmd,
6110 stdin=diff_output,
6111 cwd=top_dir,
6112 env=env,
6113 shell=sys.platform.startswith('win32'))
6114 if opts.diff:
6115 sys.stdout.write(stdout)
6116 if opts.dry_run and len(stdout) > 0:
6117 return_value = 2
6118
6119 return return_value
Jamie Madill5e96ad12020-01-13 16:08:35 +00006120
6121
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006122def _RunRustFmt(opts, rust_diff_files, top_dir, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006123 """Runs rustfmt. Just like _RunClangFormatDiff returns 2 to indicate that
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006124 presubmit checks have failed (and returns 0 otherwise)."""
6125
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006126 if not rust_diff_files:
6127 return 0
6128
6129 # Locate the rustfmt binary.
6130 try:
6131 rustfmt_tool = rustfmt.FindRustfmtToolInChromiumTree()
6132 except rustfmt.NotFoundError as e:
6133 DieWithError(e)
6134
6135 # TODO(crbug.com/1440869): Support formatting only the changed lines
6136 # if `opts.full or settings.GetFormatFullByDefault()` is False.
6137 cmd = [rustfmt_tool]
6138 if opts.dry_run:
6139 cmd.append('--check')
6140 cmd += rust_diff_files
6141 rustfmt_exitcode = subprocess2.call(cmd)
6142
6143 if opts.presubmit and rustfmt_exitcode != 0:
6144 return 2
6145
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006146 return 0
6147
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006148
Olivier Robin0a6b5442022-04-07 07:25:04 +00006149def _RunSwiftFormat(opts, swift_diff_files, top_dir, upstream_commit):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006150 """Runs swift-format. Just like _RunClangFormatDiff returns 2 to indicate
Olivier Robin0a6b5442022-04-07 07:25:04 +00006151 that presubmit checks have failed (and returns 0 otherwise)."""
6152
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006153 if not swift_diff_files:
6154 return 0
6155
6156 # Locate the swift-format binary.
6157 try:
6158 swift_format_tool = swift_format.FindSwiftFormatToolInChromiumTree()
6159 except swift_format.NotFoundError as e:
6160 DieWithError(e)
6161
6162 cmd = [swift_format_tool]
6163 if opts.dry_run:
6164 cmd += ['lint', '-s']
6165 else:
6166 cmd += ['format', '-i']
6167 cmd += swift_diff_files
6168 swift_format_exitcode = subprocess2.call(cmd)
6169
6170 if opts.presubmit and swift_format_exitcode != 0:
6171 return 2
6172
Olivier Robin0a6b5442022-04-07 07:25:04 +00006173 return 0
6174
Olivier Robin0a6b5442022-04-07 07:25:04 +00006175
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006176def MatchingFileType(file_name, extensions):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006177 """Returns True if the file name ends with one of the given extensions."""
6178 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006179
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006180
enne@chromium.org555cfe42014-01-29 18:21:39 +00006181@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006182@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006183def CMDformat(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006184 """Runs auto-formatting tools (clang-format etc.) on the diff."""
6185 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
6186 GN_EXTS = ['.gn', '.gni', '.typemap']
6187 RUST_EXTS = ['.rs']
6188 SWIFT_EXTS = ['.swift']
6189 parser.add_option('--full',
6190 action='store_true',
6191 help='Reformat the full content of all touched files')
6192 parser.add_option('--upstream', help='Branch to check against')
6193 parser.add_option('--dry-run',
6194 action='store_true',
6195 help='Don\'t modify any file on disk.')
6196 parser.add_option(
6197 '--no-clang-format',
6198 dest='clang_format',
6199 action='store_false',
6200 default=True,
6201 help='Disables formatting of various file types using clang-format.')
6202 parser.add_option('--python',
6203 action='store_true',
6204 default=None,
6205 help='Enables python formatting on all python files.')
6206 parser.add_option(
6207 '--no-python',
6208 action='store_true',
6209 default=False,
6210 help='Disables python formatting on all python files. '
6211 'If neither --python or --no-python are set, python files that have a '
6212 '.style.yapf file in an ancestor directory will be formatted. '
6213 'It is an error to set both.')
6214 parser.add_option('--js',
6215 action='store_true',
6216 help='Format javascript code with clang-format. '
6217 'Has no effect if --no-clang-format is set.')
6218 parser.add_option('--diff',
6219 action='store_true',
6220 help='Print diff to stdout rather than modifying files.')
6221 parser.add_option('--presubmit',
6222 action='store_true',
6223 help='Used when running the script from a presubmit.')
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006224
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006225 parser.add_option(
6226 '--rust-fmt',
6227 dest='use_rust_fmt',
6228 action='store_true',
6229 default=rustfmt.IsRustfmtSupported(),
6230 help='Enables formatting of Rust file types using rustfmt.')
6231 parser.add_option(
6232 '--no-rust-fmt',
6233 dest='use_rust_fmt',
6234 action='store_false',
6235 help='Disables formatting of Rust file types using rustfmt.')
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006236
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006237 parser.add_option(
6238 '--swift-format',
6239 dest='use_swift_format',
6240 action='store_true',
6241 default=swift_format.IsSwiftFormatSupported(),
6242 help='Enables formatting of Swift file types using swift-format '
6243 '(macOS host only).')
6244 parser.add_option(
6245 '--no-swift-format',
6246 dest='use_swift_format',
6247 action='store_false',
6248 help='Disables formatting of Swift file types using swift-format.')
Olivier Robin0a6b5442022-04-07 07:25:04 +00006249
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006250 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006251
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006252 if opts.python is not None and opts.no_python:
6253 raise parser.error('Cannot set both --python and --no-python')
6254 if opts.no_python:
6255 opts.python = False
Garrett Beaty91a6f332020-01-06 16:57:24 +00006256
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006257 # Normalize any remaining args against the current path, so paths relative
6258 # to the current directory are still resolved as expected.
6259 args = [os.path.join(os.getcwd(), arg) for arg in args]
Daniel Chengc55eecf2016-12-30 03:11:02 -08006260
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006261 # git diff generates paths against the root of the repository. Change
6262 # to that directory so clang-format can find files even within subdirs.
6263 rel_base_path = settings.GetRelativeRoot()
6264 if rel_base_path:
6265 os.chdir(rel_base_path)
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00006266
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006267 # Grab the merge-base commit, i.e. the upstream commit of the current
6268 # branch when it was created or the last time it was rebased. This is
6269 # to cover the case where the user may have called "git fetch origin",
6270 # moving the origin branch to a newer commit, but hasn't rebased yet.
6271 upstream_commit = None
6272 upstream_branch = opts.upstream
6273 if not upstream_branch:
6274 cl = Changelist()
6275 upstream_branch = cl.GetUpstreamBranch()
6276 if upstream_branch:
6277 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
6278 upstream_commit = upstream_commit.strip()
digit@chromium.org29e47272013-05-17 17:01:46 +00006279
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006280 if not upstream_commit:
6281 DieWithError('Could not find base commit for this branch. '
6282 'Are you in detached state?')
digit@chromium.org29e47272013-05-17 17:01:46 +00006283
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006284 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
6285 diff_output = RunGit(changed_files_cmd)
6286 diff_files = diff_output.splitlines()
6287 # Filter out files deleted by this CL
6288 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006289
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006290 if opts.js:
6291 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11006292
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006293 clang_diff_files = []
6294 if opts.clang_format:
6295 clang_diff_files = [
6296 x for x in diff_files if MatchingFileType(x, CLANG_EXTS)
6297 ]
6298 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
6299 rust_diff_files = [x for x in diff_files if MatchingFileType(x, RUST_EXTS)]
6300 swift_diff_files = [
6301 x for x in diff_files if MatchingFileType(x, SWIFT_EXTS)
Garrett Beatyed0cc5f2020-01-06 17:26:13 +00006302 ]
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006303 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00006304
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006305 top_dir = settings.GetRoot()
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00006306
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006307 return_value = _RunClangFormatDiff(opts, clang_diff_files, top_dir,
6308 upstream_commit)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006309
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006310 if opts.use_rust_fmt:
6311 rust_fmt_return_value = _RunRustFmt(opts, rust_diff_files, top_dir,
6312 upstream_commit)
6313 if rust_fmt_return_value == 2:
6314 return_value = 2
Lukasz Anforowiczb4d39542021-09-30 23:39:25 +00006315
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006316 if opts.use_swift_format:
6317 if sys.platform != 'darwin':
6318 DieWithError('swift-format is only supported on macOS.')
6319 swift_format_return_value = _RunSwiftFormat(opts, swift_diff_files,
6320 top_dir, upstream_commit)
6321 if swift_format_return_value == 2:
6322 return_value = 2
Olivier Robin0a6b5442022-04-07 07:25:04 +00006323
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006324 # Similar code to above, but using yapf on .py files rather than
6325 # clang-format on C/C++ files
6326 py_explicitly_disabled = opts.python is not None and not opts.python
6327 if python_diff_files and not py_explicitly_disabled:
6328 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
6329 yapf_tool = os.path.join(depot_tools_path, 'yapf')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006330
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006331 # Used for caching.
6332 yapf_configs = {}
6333 for f in python_diff_files:
6334 # Find the yapf style config for the current file, defaults to depot
6335 # tools default.
6336 _FindYapfConfigFile(f, yapf_configs, top_dir)
Aiden Benner99b0ccb2018-11-20 19:53:31 +00006337
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006338 # Turn on python formatting by default if a yapf config is specified.
6339 # This breaks in the case of this repo though since the specified
6340 # style file is also the global default.
6341 if opts.python is None:
6342 filtered_py_files = []
6343 for f in python_diff_files:
6344 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
6345 filtered_py_files.append(f)
Andrew Grieveb9e694c2021-11-15 19:04:46 +00006346 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006347 filtered_py_files = python_diff_files
Peter Wend9399922020-06-17 17:33:49 +00006348
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006349 # Note: yapf still seems to fix indentation of the entire file
6350 # even if line ranges are specified.
6351 # See https://github.com/google/yapf/issues/499
6352 if not opts.full and filtered_py_files:
6353 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files,
6354 upstream_commit)
Aiden Bennerc08566e2018-10-03 17:52:42 +00006355
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006356 yapfignore_patterns = _GetYapfIgnorePatterns(top_dir)
6357 filtered_py_files = _FilterYapfIgnoredFiles(filtered_py_files,
6358 yapfignore_patterns)
Aiden Bennerc08566e2018-10-03 17:52:42 +00006359
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006360 for f in filtered_py_files:
6361 yapf_style = _FindYapfConfigFile(f, yapf_configs, top_dir)
6362 # Default to pep8 if not .style.yapf is found.
6363 if not yapf_style:
6364 yapf_style = 'pep8'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006365
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006366 with open(f, 'r') as py_f:
6367 if 'python2' in py_f.readline():
6368 vpython_script = 'vpython'
6369 else:
6370 vpython_script = 'vpython3'
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006371
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006372 cmd = [vpython_script, yapf_tool, '--style', yapf_style, f]
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006373
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006374 has_formattable_lines = False
6375 if not opts.full:
6376 # Only run yapf over changed line ranges.
6377 for diff_start, diff_len in py_line_diffs[f]:
6378 diff_end = diff_start + diff_len - 1
6379 # Yapf errors out if diff_end < diff_start but this
6380 # is a valid line range diff for a removal.
6381 if diff_end >= diff_start:
6382 has_formattable_lines = True
6383 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
6384 # If all line diffs were removals we have nothing to format.
6385 if not has_formattable_lines:
6386 continue
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006387
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006388 if opts.diff or opts.dry_run:
6389 cmd += ['--diff']
6390 # Will return non-zero exit code if non-empty diff.
6391 stdout = RunCommand(cmd,
6392 error_ok=True,
6393 stderr=subprocess2.PIPE,
6394 cwd=top_dir,
6395 shell=sys.platform.startswith('win32'))
6396 if opts.diff:
6397 sys.stdout.write(stdout)
6398 elif len(stdout) > 0:
6399 return_value = 2
6400 else:
6401 cmd += ['-i']
6402 RunCommand(cmd,
6403 cwd=top_dir,
6404 shell=sys.platform.startswith('win32'))
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006405
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006406 # Format GN build files. Always run on full build files for canonical form.
6407 if gn_diff_files:
6408 cmd = ['gn', 'format']
6409 if opts.dry_run or opts.diff:
6410 cmd.append('--dry-run')
6411 for gn_diff_file in gn_diff_files:
6412 gn_ret = subprocess2.call(cmd + [gn_diff_file],
6413 shell=sys.platform.startswith('win'),
6414 cwd=top_dir)
6415 if opts.dry_run and gn_ret == 2:
6416 return_value = 2 # Not formatted.
6417 elif opts.diff and gn_ret == 2:
6418 # TODO this should compute and print the actual diff.
6419 print('This change has GN build file diff for ' + gn_diff_file)
6420 elif gn_ret != 0:
6421 # For non-dry run cases (and non-2 return values for dry-run), a
6422 # nonzero error code indicates a failure, probably because the
6423 # file doesn't parse.
6424 DieWithError('gn format failed on ' + gn_diff_file +
6425 '\nTry running `gn format` on this file manually.')
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006426
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006427 # Skip the metrics formatting from the global presubmit hook. These files
6428 # have a separate presubmit hook that issues an error if the files need
6429 # formatting, whereas the top-level presubmit script merely issues a
6430 # warning. Formatting these files is somewhat slow, so it's important not to
6431 # duplicate the work.
6432 if not opts.presubmit:
6433 for diff_xml in GetDiffXMLs(diff_files):
6434 xml_dir = GetMetricsDir(diff_xml)
6435 if not xml_dir:
6436 continue
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006437
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006438 tool_dir = os.path.join(top_dir, xml_dir)
6439 pretty_print_tool = os.path.join(tool_dir, 'pretty_print.py')
Gavin Mak7f5b53f2023-09-07 18:13:01 +00006440 cmd = [
6441 shutil.which('vpython3'), pretty_print_tool, '--non-interactive'
6442 ]
Alexei Svitkinef3cac412017-02-06 11:08:50 -05006443
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006444 # If the XML file is histograms.xml or enums.xml, add the xml path
6445 # to the command as histograms/pretty_print.py now needs a relative
6446 # path argument after splitting the histograms into multiple
6447 # directories. For example, in tools/metrics/ukm, pretty-print could
6448 # be run using: $ python pretty_print.py But in
6449 # tools/metrics/histogrmas, pretty-print should be run with an
6450 # additional relative path argument, like: $ python pretty_print.py
6451 # metadata/UMA/histograms.xml $ python pretty_print.py enums.xml
6452
6453 if xml_dir == os.path.join('tools', 'metrics', 'histograms'):
6454 if os.path.basename(diff_xml) not in (
6455 'histograms.xml', 'enums.xml',
6456 'histogram_suffixes_list.xml'):
6457 # Skip this XML file if it's not one of the known types.
6458 continue
6459 cmd.append(diff_xml)
6460
6461 if opts.dry_run or opts.diff:
6462 cmd.append('--diff')
6463
Gavin Mak7f5b53f2023-09-07 18:13:01 +00006464 stdout = RunCommand(cmd, cwd=top_dir)
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006465 if opts.diff:
6466 sys.stdout.write(stdout)
6467 if opts.dry_run and stdout:
6468 return_value = 2 # Not formatted.
6469
6470 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006471
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00006472
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006473def GetDiffXMLs(diff_files):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006474 return [
6475 os.path.normpath(x) for x in diff_files
6476 if MatchingFileType(x, ['.xml'])
6477 ]
Wenhan (Han) Zhang3bd3c992020-08-14 16:27:39 +00006478
6479
6480def GetMetricsDir(diff_xml):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006481 metrics_xml_dirs = [
6482 os.path.join('tools', 'metrics', 'actions'),
6483 os.path.join('tools', 'metrics', 'histograms'),
6484 os.path.join('tools', 'metrics', 'structured'),
6485 os.path.join('tools', 'metrics', 'ukm'),
6486 ]
6487 for xml_dir in metrics_xml_dirs:
6488 if diff_xml.startswith(xml_dir):
6489 return xml_dir
6490 return None
Steven Holte2e664bf2017-04-21 13:10:47 -07006491
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006492
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006493@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006494@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006495def CMDcheckout(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006496 """Checks out a branch associated with a given Gerrit issue."""
6497 _, args = parser.parse_args(args)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006498
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006499 if len(args) != 1:
6500 parser.print_help()
6501 return 1
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006502
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006503 issue_arg = ParseIssueNumberArgument(args[0])
6504 if not issue_arg.valid:
6505 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006506
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006507 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006508
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006509 output = RunGit([
6510 'config', '--local', '--get-regexp', r'branch\..*\.' + ISSUE_CONFIG_KEY
6511 ],
6512 error_ok=True)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006513
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006514 branches = []
6515 for key, issue in [x.split() for x in output.splitlines()]:
6516 if issue == target_issue:
6517 branches.append(
6518 re.sub(r'branch\.(.*)\.' + ISSUE_CONFIG_KEY, r'\1', key))
Edward Lemur52969c92020-02-06 18:15:28 +00006519
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006520 if len(branches) == 0:
6521 print('No branch found for issue %s.' % target_issue)
6522 return 1
6523 if len(branches) == 1:
6524 RunGit(['checkout', branches[0]])
6525 else:
6526 print('Multiple branches match issue %s:' % target_issue)
6527 for i in range(len(branches)):
6528 print('%d: %s' % (i, branches[i]))
6529 which = gclient_utils.AskForData('Choose by index: ')
6530 try:
6531 RunGit(['checkout', branches[int(which)]])
6532 except (IndexError, ValueError):
6533 print('Invalid selection, not checking out any branch.')
6534 return 1
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006535
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006536 return 0
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006537
6538
maruel@chromium.org29404b52014-09-08 22:58:00 +00006539def CMDlol(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006540 # This command is intentionally undocumented.
6541 print(
6542 zlib.decompress(
6543 base64.b64decode(
6544 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6545 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6546 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
6547 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')).decode('utf-8'))
6548 return 0
maruel@chromium.org29404b52014-09-08 22:58:00 +00006549
6550
Josip Sokcevic0399e172022-03-21 23:11:51 +00006551def CMDversion(parser, args):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006552 import utils
6553 print(utils.depot_tools_version())
Josip Sokcevic0399e172022-03-21 23:11:51 +00006554
6555
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006556class OptionParser(optparse.OptionParser):
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006557 """Creates the option parse and add --verbose support."""
6558 def __init__(self, *args, **kwargs):
6559 optparse.OptionParser.__init__(self,
6560 *args,
6561 prog='git cl',
6562 version=__version__,
6563 **kwargs)
6564 self.add_option('-v',
6565 '--verbose',
6566 action='count',
6567 default=0,
6568 help='Use 2 times for more debugging info')
Sigurd Schneider9abde8c2020-11-17 08:44:52 +00006569
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006570 def parse_args(self, args=None, _values=None):
Joanna Wangc5b38322023-03-15 20:38:46 +00006571 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006572 return self._parse_args(args)
6573 finally:
6574 # Regardless of success or failure of args parsing, we want to
6575 # report metrics, but only after logging has been initialized (if
6576 # parsing succeeded).
6577 global settings
6578 settings = Settings()
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00006579
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006580 if metrics.collector.config.should_collect_metrics:
6581 try:
6582 # GetViewVCUrl ultimately calls logging method.
6583 project_url = settings.GetViewVCUrl().strip('/+')
6584 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
6585 metrics.collector.add('project_urls', [project_url])
6586 except subprocess2.CalledProcessError:
6587 # Occurs when command is not executed in a git repository
6588 # We should not fail here. If the command needs to be
6589 # executed in a repo, it will be raised later.
6590 pass
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00006591
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006592 def _parse_args(self, args=None):
6593 # Create an optparse.Values object that will store only the actual
6594 # passed options, without the defaults.
6595 actual_options = optparse.Values()
6596 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
6597 # Create an optparse.Values object with the default options.
6598 options = optparse.Values(self.get_default_values().__dict__)
6599 # Update it with the options passed by the user.
6600 options._update_careful(actual_options.__dict__)
6601 # Store the options passed by the user in an _actual_options attribute.
6602 # We store only the keys, and not the values, since the values can
6603 # contain arbitrary information, which might be PII.
6604 metrics.collector.add('arguments', list(actual_options.__dict__.keys()))
Edward Lemur83bd7f42018-10-10 00:14:21 +00006605
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006606 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
6607 logging.basicConfig(
6608 level=levels[min(options.verbose,
6609 len(levels) - 1)],
6610 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6611 '%(filename)s] %(message)s')
6612
6613 return options, args
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006614
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006615
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006616def main(argv):
Gavin Mak7f5b53f2023-09-07 18:13:01 +00006617 if sys.version_info[0] < 3:
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006618 print('\nYour Python version %s is unsupported, please upgrade.\n' %
6619 (sys.version.split(' ', 1)[0], ),
6620 file=sys.stderr)
6621 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006622
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006623 colorize_CMDstatus_doc()
6624 dispatcher = subcommand.CommandDispatcher(__name__)
6625 try:
6626 return dispatcher.execute(OptionParser(), argv)
6627 except auth.LoginRequiredError as e:
6628 DieWithError(str(e))
6629 except urllib.error.HTTPError as e:
6630 if e.code != 500:
6631 raise
6632 DieWithError((
6633 'App Engine is misbehaving and returned HTTP %d, again. Keep faith '
6634 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
6635 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006636
6637
6638if __name__ == '__main__':
Mike Frysinger124bb8e2023-09-06 05:48:55 +00006639 # These affect sys.stdout, so do it outside of main() to simplify mocks in
6640 # the unit tests.
6641 fix_encoding.fix_encoding()
6642 setup_color.init()
6643 with metrics.collector.print_notice_and_exit():
6644 sys.exit(main(sys.argv[1:]))