blob: 19b7c952b733d103d1b1c1f6ac8207711edd732b [file] [log] [blame]
Edward Lesmes98eda3f2019-08-12 21:09:53 +00001#!/usr/bin/env python
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002# Copyright (c) 2013 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00008"""A git-command for integrating reviews on Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +010016import contextlib
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010017import datetime
sheyang@google.com6ebaf782015-05-12 19:17:54 +000018import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010019import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000020import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000022import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import optparse
24import os
25import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010026import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000027import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070029import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000031import time
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000034import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000035import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000036import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000037import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000039from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000040from third_party import httplib2
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000041import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000042import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000043import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000044import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000045import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000046import gerrit_util
iannucci@chromium.org9e849272014-04-04 00:31:55 +000047import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000048import git_footers
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000049import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000050import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000051import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000053import presubmit_support
54import scm
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000055import setup_color
Francois Dorayd42c6812017-05-30 15:10:20 -040056import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000057import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000058import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import watchlists
60
tandrii7400cf02016-06-21 08:48:07 -070061__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062
Edward Lemur0f58ae42019-04-30 17:24:12 +000063# Traces for git push will be stored in a traces directory inside the
64# depot_tools checkout.
65DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
66TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
67
68# When collecting traces, Git hashes will be reduced to 6 characters to reduce
69# the size after compression.
70GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
71# Used to redact the cookies from the gitcookies file.
72GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
73
Edward Lemur1b52d872019-05-09 21:12:12 +000074# The maximum number of traces we will keep. Multiplied by 3 since we store
75# 3 files per trace.
76MAX_TRACES = 3 * 10
Edward Lemur5737f022019-05-17 01:24:00 +000077# Message to be displayed to the user to inform where to find the traces for a
78# git-cl upload execution.
Edward Lemur0f58ae42019-04-30 17:24:12 +000079TRACES_MESSAGE = (
Edward Lemur1b52d872019-05-09 21:12:12 +000080'\n'
Edward Lemur5737f022019-05-17 01:24:00 +000081'The traces of this git-cl execution have been recorded at:\n'
Edward Lemur1b52d872019-05-09 21:12:12 +000082' %(trace_name)s-traces.zip\n'
Edward Lemur5737f022019-05-17 01:24:00 +000083'Copies of your gitcookies file and git config have been recorded at:\n'
84' %(trace_name)s-git-info.zip\n')
Edward Lemur1b52d872019-05-09 21:12:12 +000085# Format of the message to be stored as part of the traces to give developers a
86# better context when they go through traces.
87TRACES_README_FORMAT = (
88'Date: %(now)s\n'
89'\n'
90'Change: https://%(gerrit_host)s/q/%(change_id)s\n'
91'Title: %(title)s\n'
92'\n'
93'%(description)s\n'
94'\n'
95'Execution time: %(execution_time)s\n'
96'Exit code: %(exit_code)s\n') + TRACES_MESSAGE
Edward Lemur0f58ae42019-04-30 17:24:12 +000097
tandrii9d2c7a32016-06-22 03:42:45 -070098COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080099POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000100DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +0000101REFS_THAT_ALIAS_TO_OTHER_REFS = {
102 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
103 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
104}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000105
thestig@chromium.org44202a22014-03-11 19:22:18 +0000106# Valid extensions for files we want to lint.
107DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
108DEFAULT_LINT_IGNORE_REGEX = r"$^"
109
Aiden Bennerc08566e2018-10-03 17:52:42 +0000110# File name for yapf style config files.
111YAPF_CONFIG_FILENAME = '.style.yapf'
112
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000113# Shortcut since it quickly becomes repetitive.
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000114Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000115
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000116# Initialized in main()
117settings = None
118
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100119# Used by tests/git_cl_test.py to add extra logging.
120# Inside the weirdly failing test, add this:
121# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700122# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100123_IS_BEING_TESTED = False
124
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000125
Christopher Lamf732cd52017-01-24 12:40:11 +1100126def DieWithError(message, change_desc=None):
127 if change_desc:
128 SaveDescriptionBackup(change_desc)
129
vapiera7fbd5a2016-06-16 09:17:49 -0700130 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000131 sys.exit(1)
132
133
Christopher Lamf732cd52017-01-24 12:40:11 +1100134def SaveDescriptionBackup(change_desc):
135 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000136 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100137 backup_file = open(backup_path, 'w')
138 backup_file.write(change_desc.description)
139 backup_file.close()
140
141
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000142def GetNoGitPagerEnv():
143 env = os.environ.copy()
144 # 'cat' is a magical git string that disables pagers on all platforms.
145 env['GIT_PAGER'] = 'cat'
146 return env
147
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000148
bsep@chromium.org627d9002016-04-29 00:00:52 +0000149def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000150 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000151 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000152 except subprocess2.CalledProcessError as e:
153 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000154 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000155 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000156 'Command "%s" failed.\n%s' % (
157 ' '.join(args), error_message or e.stdout or ''))
158 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000159
160
161def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000162 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000163 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000164
165
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000166def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000167 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700168 if suppress_stderr:
169 stderr = subprocess2.VOID
170 else:
171 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000172 try:
tandrii5d48c322016-08-18 16:19:37 -0700173 (out, _), code = subprocess2.communicate(['git'] + args,
174 env=GetNoGitPagerEnv(),
175 stdout=subprocess2.PIPE,
176 stderr=stderr)
177 return code, out
178 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900179 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700180 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000181
182
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000183def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000184 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000185 return RunGitWithCode(args, suppress_stderr=True)[1]
186
187
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000188def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000189 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000190 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000191 return (version.startswith(prefix) and
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000192 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000193
194
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000195def BranchExists(branch):
196 """Return True if specified branch exists."""
197 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
198 suppress_stderr=True)
199 return not code
200
201
tandrii2a16b952016-10-19 07:09:44 -0700202def time_sleep(seconds):
203 # Use this so that it can be mocked in tests without interfering with python
204 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700205 return time.sleep(seconds)
206
207
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000208def time_time():
209 # Use this so that it can be mocked in tests without interfering with python
210 # system machinery.
211 return time.time()
212
213
Edward Lemur1b52d872019-05-09 21:12:12 +0000214def datetime_now():
215 # Use this so that it can be mocked in tests without interfering with python
216 # system machinery.
217 return datetime.datetime.now()
218
219
maruel@chromium.org90541732011-04-01 17:54:18 +0000220def ask_for_data(prompt):
221 try:
222 return raw_input(prompt)
223 except KeyboardInterrupt:
224 # Hide the exception.
225 sys.exit(1)
226
227
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100228def confirm_or_exit(prefix='', action='confirm'):
229 """Asks user to press enter to continue or press Ctrl+C to abort."""
230 if not prefix or prefix.endswith('\n'):
231 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100232 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100233 mid = ' Press'
234 elif prefix.endswith(' '):
235 mid = 'press'
236 else:
237 mid = ' press'
238 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
239
240
241def ask_for_explicit_yes(prompt):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000242 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100243 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
244 while True:
245 if 'yes'.startswith(result):
246 return True
247 if 'no'.startswith(result):
248 return False
249 result = ask_for_data('Please, type yes or no: ').lower()
250
251
tandrii5d48c322016-08-18 16:19:37 -0700252def _git_branch_config_key(branch, key):
253 """Helper method to return Git config key for a branch."""
254 assert branch, 'branch name is required to set git config for it'
255 return 'branch.%s.%s' % (branch, key)
256
257
258def _git_get_branch_config_value(key, default=None, value_type=str,
259 branch=False):
260 """Returns git config value of given or current branch if any.
261
262 Returns default in all other cases.
263 """
264 assert value_type in (int, str, bool)
265 if branch is False: # Distinguishing default arg value from None.
266 branch = GetCurrentBranch()
267
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000268 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700269 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000270
tandrii5d48c322016-08-18 16:19:37 -0700271 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700272 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700273 args.append('--bool')
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000274 # `git config` also has --int, but apparently git config suffers from integer
tandrii33a46ff2016-08-23 05:53:40 -0700275 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700276 args.append(_git_branch_config_key(branch, key))
277 code, out = RunGitWithCode(args)
278 if code == 0:
279 value = out.strip()
280 if value_type == int:
281 return int(value)
282 if value_type == bool:
283 return bool(value.lower() == 'true')
284 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000285 return default
286
287
tandrii5d48c322016-08-18 16:19:37 -0700288def _git_set_branch_config_value(key, value, branch=None, **kwargs):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000289 """Sets or unsets the git branch config value.
tandrii5d48c322016-08-18 16:19:37 -0700290
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000291 If value is None, the key will be unset, otherwise it will be set.
292 If no branch is given, the currently checked out branch is used.
tandrii5d48c322016-08-18 16:19:37 -0700293 """
294 if not branch:
295 branch = GetCurrentBranch()
296 assert branch, 'a branch name OR currently checked out branch is required'
297 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700298 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700299 if value is None:
300 args.append('--unset')
301 elif isinstance(value, bool):
302 args.append('--bool')
303 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700304 else:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000305 # `git config` also has --int, but apparently git config suffers from
306 # integer overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700307 value = str(value)
308 args.append(_git_branch_config_key(branch, key))
309 if value is not None:
310 args.append(value)
311 RunGit(args, **kwargs)
312
313
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100314def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700315 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100316
317 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
318 """
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000319 # Git also stores timezone offset, but it only affects visual display;
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100320 # actual point in time is defined by this timestamp only.
321 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
322
323
324def _git_amend_head(message, committer_timestamp):
325 """Amends commit with new message and desired committer_timestamp.
326
327 Sets committer timezone to UTC.
328 """
329 env = os.environ.copy()
330 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
331 return RunGit(['commit', '--amend', '-m', message], env=env)
332
333
machenbach@chromium.org45453142015-09-15 08:45:22 +0000334def _get_properties_from_options(options):
335 properties = dict(x.split('=', 1) for x in options.properties)
336 for key, val in properties.iteritems():
337 try:
338 properties[key] = json.loads(val)
339 except ValueError:
340 pass # If a value couldn't be evaluated, treat it as a string.
341 return properties
342
343
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000344def _buildbucket_retry(operation_name, http, *args, **kwargs):
345 """Retries requests to buildbucket service and returns parsed json content."""
346 try_count = 0
347 while True:
348 response, content = http.request(*args, **kwargs)
349 try:
350 content_json = json.loads(content)
351 except ValueError:
352 content_json = None
353
354 # Buildbucket could return an error even if status==200.
355 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000356 error = content_json.get('error')
357 if error.get('code') == 403:
358 raise BuildbucketResponseException(
359 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000360 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000361 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000362 raise BuildbucketResponseException(msg)
363
364 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700365 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000366 raise BuildbucketResponseException(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000367 'Buildbucket returned invalid JSON content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700368 'Please file bugs at http://crbug.com, '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000369 'component "Infra>Platform>Buildbucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000370 content)
371 return content_json
372 if response.status < 500 or try_count >= 2:
373 raise httplib2.HttpLib2Error(content)
374
375 # status >= 500 means transient failures.
376 logging.debug('Transient errors when %s. Will retry.', operation_name)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000377 time_sleep(0.5 + (1.5 * try_count))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000378 try_count += 1
379 assert False, 'unreachable'
380
381
qyearsley1fdfcb62016-10-24 13:22:03 -0700382def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700383 """Returns a dict mapping bucket names to builders and tests,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000384 for triggering tryjobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700385 """
qyearsleydd49f942016-10-28 11:57:22 -0700386 # If no bots are listed, we try to get a set of builders and tests based
387 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700388 if not options.bot:
389 change = changelist.GetChange(
390 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700391 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700392 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700393 change=change,
394 changed_files=change.LocalPaths(),
395 repository_root=settings.GetRoot(),
396 default_presubmit=None,
397 project=None,
398 verbose=options.verbose,
399 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700400 if masters is None:
401 return None
Edward Lemurc8b67ed2019-09-12 20:28:58 +0000402 return {m: b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700403
qyearsley1fdfcb62016-10-24 13:22:03 -0700404 if options.bucket:
405 return {options.bucket: {b: [] for b in options.bot}}
Andrii Shyshkalov75424372019-08-30 22:48:15 +0000406 option_parser.error(
407 'Please specify the bucket, e.g. "-B luci.chromium.try".')
qyearsley1fdfcb62016-10-24 13:22:03 -0700408
409
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800410def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000411 """Sends a request to Buildbucket to trigger tryjobs for a changelist.
qyearsley1fdfcb62016-10-24 13:22:03 -0700412
413 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700414 auth_config: AuthConfig for Buildbucket.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000415 changelist: Changelist that the tryjobs are associated with.
qyearsley1fdfcb62016-10-24 13:22:03 -0700416 buckets: A nested dict mapping bucket names to builders to tests.
417 options: Command-line options.
418 """
Edward Lemur2c210a42019-09-16 23:58:35 +0000419 assert changelist.GetIssue(), 'CL must be uploaded first'
420 codereview_url = changelist.GetCodereviewServer()
421 assert codereview_url, 'CL must be uploaded first'
422 patchset = patchset or changelist.GetMostRecentPatchset()
423 assert patchset, 'CL must be uploaded first'
tandriide281ae2016-10-12 06:02:30 -0700424
Edward Lemur2c210a42019-09-16 23:58:35 +0000425 codereview_host = urlparse.urlparse(codereview_url).hostname
426 # Cache the buildbucket credentials under the codereview host key, so that
427 # users can use different credentials for different buckets.
428 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
429 http = authenticator.authorize(httplib2.Http())
430 http.force_exception_to_status_code = True
431
432 buildbucket_put_url = (
433 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
434 hostname=options.buildbucket_host))
435 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
436 hostname=codereview_host,
437 issue=changelist.GetIssue(),
438 patch=patchset)
439
440 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
441 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700442 if options.clobber:
Edward Lemur2c210a42019-09-16 23:58:35 +0000443 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700444 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700445 if extra_properties:
Edward Lemur2c210a42019-09-16 23:58:35 +0000446 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000447
Edward Lemur2c210a42019-09-16 23:58:35 +0000448 batch_req_body = {'builds': []}
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000449 print_text = []
450 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700451 for bucket, builders_and_tests in sorted(buckets.iteritems()):
452 print_text.append('Bucket: %s' % bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000453 for builder, tests in sorted(builders_and_tests.iteritems()):
454 print_text.append(' %s: %s' % (builder, tests))
Edward Lemur2c210a42019-09-16 23:58:35 +0000455 parameters = {
456 'builder_name': builder,
457 'changes': [{
458 'author': {'email': changelist.GetIssueOwner()},
459 'revision': options.revision,
460 }],
461 'properties': shared_parameters_properties.copy(),
462 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000463 if 'presubmit' in builder.lower():
Edward Lemur2c210a42019-09-16 23:58:35 +0000464 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000465 if tests:
Edward Lemur2c210a42019-09-16 23:58:35 +0000466 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700467
468 tags = [
Edward Lemur2c210a42019-09-16 23:58:35 +0000469 'builder:%s' % builder,
470 'buildset:%s' % buildset,
471 'user_agent:git_cl_try',
borenet6c0efe62016-10-19 08:13:29 -0700472 ]
borenet6c0efe62016-10-19 08:13:29 -0700473
Edward Lemur2c210a42019-09-16 23:58:35 +0000474 batch_req_body['builds'].append(
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000475 {
Edward Lemur2c210a42019-09-16 23:58:35 +0000476 'bucket': bucket,
477 'parameters_json': json.dumps(parameters),
478 'client_operation_id': str(uuid.uuid4()),
479 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000480 }
481 )
482
Edward Lemur2c210a42019-09-16 23:58:35 +0000483 _buildbucket_retry(
484 'triggering tryjobs',
485 http,
486 buildbucket_put_url,
487 'PUT',
488 body=json.dumps(batch_req_body),
489 headers={'Content-Type': 'application/json'}
490 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000491 print_text.append('To see results here, run: git cl try-results')
492 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700493 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000494
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000495
tandrii221ab252016-10-06 08:12:04 -0700496def fetch_try_jobs(auth_config, changelist, buildbucket_host,
497 patchset=None):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000498 """Fetches tryjobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000499
qyearsley53f48a12016-09-01 10:45:13 -0700500 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000501 """
tandrii221ab252016-10-06 08:12:04 -0700502 assert buildbucket_host
503 assert changelist.GetIssue(), 'CL must be uploaded first'
504 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
505 patchset = patchset or changelist.GetMostRecentPatchset()
506 assert patchset, 'CL must be uploaded first'
507
508 codereview_url = changelist.GetCodereviewServer()
509 codereview_host = urlparse.urlparse(codereview_url).hostname
510 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000511 if authenticator.has_cached_credentials():
512 http = authenticator.authorize(httplib2.Http())
513 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700514 print('Warning: Some results might be missing because %s' %
515 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700516 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000517 http = httplib2.Http()
518
519 http.force_exception_to_status_code = True
520
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000521 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandrii221ab252016-10-06 08:12:04 -0700522 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000523 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700524 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000525 params = {'tag': 'buildset:%s' % buildset}
526
527 builds = {}
528 while True:
529 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700530 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000531 params=urllib.urlencode(params))
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000532 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000533 for build in content.get('builds', []):
534 builds[build['id']] = build
535 if 'next_cursor' in content:
536 params['start_cursor'] = content['next_cursor']
537 else:
538 break
539 return builds
540
541
qyearsleyeab3c042016-08-24 09:18:28 -0700542def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000543 """Prints nicely result of fetch_try_jobs."""
544 if not builds:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000545 print('No tryjobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000546 return
547
548 # Make a copy, because we'll be modifying builds dictionary.
549 builds = builds.copy()
550 builder_names_cache = {}
551
552 def get_builder(b):
553 try:
554 return builder_names_cache[b['id']]
555 except KeyError:
556 try:
557 parameters = json.loads(b['parameters_json'])
558 name = parameters['builder_name']
559 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700560 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700561 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000562 name = None
563 builder_names_cache[b['id']] = name
564 return name
565
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000566 if options.print_master:
567 name_fmt = '%%-%ds %%-%ds' % (
Edward Lemurc8b67ed2019-09-12 20:28:58 +0000568 max(len(str(b['bucket'])) for b in builds.itervalues()),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000569 max(len(str(get_builder(b))) for b in builds.itervalues()))
570 def get_name(b):
Edward Lemurc8b67ed2019-09-12 20:28:58 +0000571 return name_fmt % (b['bucket'], get_builder(b))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000572 else:
573 name_fmt = '%%-%ds' % (
574 max(len(str(get_builder(b))) for b in builds.itervalues()))
575 def get_name(b):
576 return name_fmt % get_builder(b)
577
578 def sort_key(b):
579 return b['status'], b.get('result'), get_name(b), b.get('url')
580
581 def pop(title, f, color=None, **kwargs):
582 """Pop matching builds from `builds` dict and print them."""
583
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000584 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000585 colorize = str
586 else:
587 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
588
589 result = []
590 for b in builds.values():
591 if all(b.get(k) == v for k, v in kwargs.iteritems()):
592 builds.pop(b['id'])
593 result.append(b)
594 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700595 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000596 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700597 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000598
599 total = len(builds)
600 pop(status='COMPLETED', result='SUCCESS',
601 title='Successes:', color=Fore.GREEN,
602 f=lambda b: (get_name(b), b.get('url')))
603 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
604 title='Infra Failures:', color=Fore.MAGENTA,
605 f=lambda b: (get_name(b), b.get('url')))
606 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
607 title='Failures:', color=Fore.RED,
608 f=lambda b: (get_name(b), b.get('url')))
609 pop(status='COMPLETED', result='CANCELED',
610 title='Canceled:', color=Fore.MAGENTA,
611 f=lambda b: (get_name(b),))
612 pop(status='COMPLETED', result='FAILURE',
613 failure_reason='INVALID_BUILD_DEFINITION',
614 title='Wrong master/builder name:', color=Fore.MAGENTA,
615 f=lambda b: (get_name(b),))
616 pop(status='COMPLETED', result='FAILURE',
617 title='Other failures:',
618 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
619 pop(status='COMPLETED',
620 title='Other finished:',
621 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
622 pop(status='STARTED',
623 title='Started:', color=Fore.YELLOW,
624 f=lambda b: (get_name(b), b.get('url')))
625 pop(status='SCHEDULED',
626 title='Scheduled:',
627 f=lambda b: (get_name(b), 'id=%s' % b['id']))
628 # The last section is just in case buildbucket API changes OR there is a bug.
629 pop(title='Other:',
630 f=lambda b: (get_name(b), 'id=%s' % b['id']))
631 assert len(builds) == 0
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000632 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000633
634
Aiden Bennerc08566e2018-10-03 17:52:42 +0000635def _ComputeDiffLineRanges(files, upstream_commit):
636 """Gets the changed line ranges for each file since upstream_commit.
637
638 Parses a git diff on provided files and returns a dict that maps a file name
639 to an ordered list of range tuples in the form (start_line, count).
640 Ranges are in the same format as a git diff.
641 """
642 # If files is empty then diff_output will be a full diff.
643 if len(files) == 0:
644 return {}
645
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000646 # Take the git diff and find the line ranges where there are changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000647 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
648 diff_output = RunGit(diff_cmd)
649
650 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
651 # 2 capture groups
652 # 0 == fname of diff file
653 # 1 == 'diff_start,diff_count' or 'diff_start'
654 # will match each of
655 # diff --git a/foo.foo b/foo.py
656 # @@ -12,2 +14,3 @@
657 # @@ -12,2 +17 @@
658 # running re.findall on the above string with pattern will give
659 # [('foo.py', ''), ('', '14,3'), ('', '17')]
660
661 curr_file = None
662 line_diffs = {}
663 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
664 if match[0] != '':
665 # Will match the second filename in diff --git a/a.py b/b.py.
666 curr_file = match[0]
667 line_diffs[curr_file] = []
668 else:
669 # Matches +14,3
670 if ',' in match[1]:
671 diff_start, diff_count = match[1].split(',')
672 else:
673 # Single line changes are of the form +12 instead of +12,1.
674 diff_start = match[1]
675 diff_count = 1
676
677 diff_start = int(diff_start)
678 diff_count = int(diff_count)
679
680 # If diff_count == 0 this is a removal we can ignore.
681 line_diffs[curr_file].append((diff_start, diff_count))
682
683 return line_diffs
684
685
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000686def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000687 """Checks if a yapf file is in any parent directory of fpath until top_dir.
688
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000689 Recursively checks parent directories to find yapf file and if no yapf file
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000690 is found returns None. Uses yapf_config_cache as a cache for previously found
691 configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000692 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000693 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000694 # Return result if we've already computed it.
695 if fpath in yapf_config_cache:
696 return yapf_config_cache[fpath]
697
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000698 parent_dir = os.path.dirname(fpath)
699 if os.path.isfile(fpath):
700 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000701 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000702 # Otherwise fpath is a directory
703 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
704 if os.path.isfile(yapf_file):
705 ret = yapf_file
706 elif fpath == top_dir or parent_dir == fpath:
707 # If we're at the top level directory, or if we're at root
708 # there is no provided style.
709 ret = None
710 else:
711 # Otherwise recurse on the current directory.
712 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000713 yapf_config_cache[fpath] = ret
714 return ret
715
716
qyearsley53f48a12016-09-01 10:45:13 -0700717def write_try_results_json(output_file, builds):
718 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
719
720 The input |builds| dict is assumed to be generated by Buildbucket.
721 Buildbucket documentation: http://goo.gl/G0s101
722 """
723
724 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800725 """Extracts some of the information from one build dict."""
726 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700727 return {
728 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700729 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800730 'builder_name': parameters.get('builder_name'),
731 'created_ts': build.get('created_ts'),
732 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700733 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800734 'result': build.get('result'),
735 'status': build.get('status'),
736 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700737 'url': build.get('url'),
738 }
739
740 converted = []
741 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000742 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 10:45:13 -0700743 write_json(output_file, converted)
744
745
Aaron Gable13101a62018-02-09 13:20:41 -0800746def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000747 """Prints statistics about the change to the user."""
748 # --no-ext-diff is broken in some versions of Git, so try to work around
749 # this by overriding the environment (but there is still a problem if the
750 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000751 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000752 if 'GIT_EXTERNAL_DIFF' in env:
753 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000754
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000755 try:
756 stdout = sys.stdout.fileno()
757 except AttributeError:
758 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000759 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800760 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000761 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000762
763
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000764class BuildbucketResponseException(Exception):
765 pass
766
767
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000768class Settings(object):
769 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000770 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000771 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000772 self.tree_status_url = None
773 self.viewvc_url = None
774 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000775 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000776 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000777 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000778 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000779
780 def LazyUpdateIfNeeded(self):
781 """Updates the settings from a codereview.settings file, if available."""
782 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000783 # The only value that actually changes the behavior is
784 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000785 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000786 error_ok=True
787 ).strip().lower()
788
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000789 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000790 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000791 LoadCodereviewSettingsFromFile(cr_settings_file)
792 self.updated = True
793
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000794 @staticmethod
795 def GetRelativeRoot():
796 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000797
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000798 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000799 if self.root is None:
800 self.root = os.path.abspath(self.GetRelativeRoot())
801 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000802
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000803 def GetTreeStatusUrl(self, error_ok=False):
804 if not self.tree_status_url:
805 error_message = ('You must configure your tree status URL by running '
806 '"git cl config".')
Edward Lemur61ea3072018-12-01 00:34:36 +0000807 self.tree_status_url = self._GetConfig(
808 'rietveld.tree-status-url', error_ok=error_ok,
809 error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000810 return self.tree_status_url
811
812 def GetViewVCUrl(self):
813 if not self.viewvc_url:
Edward Lemur61ea3072018-12-01 00:34:36 +0000814 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000815 return self.viewvc_url
816
rmistry@google.com90752582014-01-14 21:04:50 +0000817 def GetBugPrefix(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000818 return self._GetConfig('rietveld.bug-prefix', error_ok=True)
rmistry@google.com78948ed2015-07-08 23:09:57 +0000819
rmistry@google.com5626a922015-02-26 14:03:30 +0000820 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000821 run_post_upload_hook = self._GetConfig(
822 'rietveld.run-post-upload-hook', error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +0000823 return run_post_upload_hook == "True"
824
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000825 def GetDefaultCCList(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000826 return self._GetConfig('rietveld.cc', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000827
ukai@chromium.orge8077812012-02-03 03:41:46 +0000828 def GetIsGerrit(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000829 """Returns True if this repo is associated with Gerrit."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000830 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700831 self.is_gerrit = (
832 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000833 return self.is_gerrit
834
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000835 def GetSquashGerritUploads(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000836 """Returns True if uploads to Gerrit should be squashed by default."""
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000837 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700838 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
839 if self.squash_gerrit_uploads is None:
840 # Default is squash now (http://crbug.com/611892#c23).
841 self.squash_gerrit_uploads = not (
842 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
843 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000844 return self.squash_gerrit_uploads
845
tandriia60502f2016-06-20 02:01:53 -0700846 def GetSquashGerritUploadsOverride(self):
847 """Return True or False if codereview.settings should be overridden.
848
849 Returns None if no override has been defined.
850 """
851 # See also http://crbug.com/611892#c23
852 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
853 error_ok=True).strip()
854 if result == 'true':
855 return True
856 if result == 'false':
857 return False
858 return None
859
tandrii@chromium.org28253532016-04-14 13:46:56 +0000860 def GetGerritSkipEnsureAuthenticated(self):
861 """Return True if EnsureAuthenticated should not be done for Gerrit
862 uploads."""
863 if self.gerrit_skip_ensure_authenticated is None:
864 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000865 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000866 error_ok=True).strip() == 'true')
867 return self.gerrit_skip_ensure_authenticated
868
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000869 def GetGitEditor(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000870 """Returns the editor specified in the git config, or None if none is."""
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000871 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000872 # Git requires single quotes for paths with spaces. We need to replace
873 # them with double quotes for Windows to treat such paths as a single
874 # path.
875 self.git_editor = self._GetConfig(
876 'core.editor', error_ok=True).replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000877 return self.git_editor or None
878
thestig@chromium.org44202a22014-03-11 19:22:18 +0000879 def GetLintRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000880 return (self._GetConfig('rietveld.cpplint-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000881 DEFAULT_LINT_REGEX)
882
883 def GetLintIgnoreRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000884 return (self._GetConfig('rietveld.cpplint-ignore-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000885 DEFAULT_LINT_IGNORE_REGEX)
886
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000887 def _GetConfig(self, param, **kwargs):
888 self.LazyUpdateIfNeeded()
889 return RunGit(['config', param], **kwargs).strip()
890
891
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100892@contextlib.contextmanager
893def _get_gerrit_project_config_file(remote_url):
894 """Context manager to fetch and store Gerrit's project.config from
895 refs/meta/config branch and store it in temp file.
896
897 Provides a temporary filename or None if there was error.
898 """
899 error, _ = RunGitWithCode([
900 'fetch', remote_url,
901 '+refs/meta/config:refs/git_cl/meta/config'])
902 if error:
903 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700904 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100905 (remote_url, error))
906 yield None
907 return
908
909 error, project_config_data = RunGitWithCode(
910 ['show', 'refs/git_cl/meta/config:project.config'])
911 if error:
912 print('WARNING: project.config file not found')
913 yield None
914 return
915
916 with gclient_utils.temporary_directory() as tempdir:
917 project_config_file = os.path.join(tempdir, 'project.config')
918 gclient_utils.FileWrite(project_config_file, project_config_data)
919 yield project_config_file
920
921
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000922def ShortBranchName(branch):
923 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000924 return branch.replace('refs/heads/', '', 1)
925
926
927def GetCurrentBranchRef():
928 """Returns branch ref (e.g., refs/heads/master) or None."""
929 return RunGit(['symbolic-ref', 'HEAD'],
930 stderr=subprocess2.VOID, error_ok=True).strip() or None
931
932
933def GetCurrentBranch():
934 """Returns current branch or None.
935
936 For refs/heads/* branches, returns just last part. For others, full ref.
937 """
938 branchref = GetCurrentBranchRef()
939 if branchref:
940 return ShortBranchName(branchref)
941 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000942
943
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000944class _CQState(object):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +0000945 """Enum for states of CL with respect to CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000946 NONE = 'none'
947 DRY_RUN = 'dry_run'
948 COMMIT = 'commit'
949
950 ALL_STATES = [NONE, DRY_RUN, COMMIT]
951
952
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000953class _ParsedIssueNumberArgument(object):
Edward Lemurf38bc172019-09-03 21:02:13 +0000954 def __init__(self, issue=None, patchset=None, hostname=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000955 self.issue = issue
956 self.patchset = patchset
957 self.hostname = hostname
958
959 @property
960 def valid(self):
961 return self.issue is not None
962
963
Edward Lemurf38bc172019-09-03 21:02:13 +0000964def ParseIssueNumberArgument(arg):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000965 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
966 fail_result = _ParsedIssueNumberArgument()
967
968 if arg.isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +0000969 return _ParsedIssueNumberArgument(issue=int(arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000970 if not arg.startswith('http'):
971 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -0700972
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000973 url = gclient_utils.UpgradeToHttps(arg)
974 try:
975 parsed_url = urlparse.urlparse(url)
976 except ValueError:
977 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +0200978
Edward Lemur125d60a2019-09-13 18:25:41 +0000979 return Changelist.ParseIssueURL(parsed_url) or fail_result
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000980
981
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +0000982def _create_description_from_log(args):
983 """Pulls out the commit log to use as a base for the CL description."""
984 log_args = []
985 if len(args) == 1 and not args[0].endswith('.'):
986 log_args = [args[0] + '..']
987 elif len(args) == 1 and args[0].endswith('...'):
988 log_args = [args[0][:-1]]
989 elif len(args) == 2:
990 log_args = [args[0] + '..' + args[1]]
991 else:
992 log_args = args[:] # Hope for the best!
993 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
994
995
Aaron Gablea45ee112016-11-22 15:14:38 -0800996class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -0700997 def __init__(self, issue, url):
998 self.issue = issue
999 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001000 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001001
1002 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001003 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001004 self.issue, self.url)
1005
1006
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001007_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001008 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001009 # TODO(tandrii): these two aren't known in Gerrit.
1010 'approval', 'disapproval'])
1011
1012
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001013class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001014 """Changelist works with one changelist in local branch.
1015
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001016 Notes:
1017 * Not safe for concurrent multi-{thread,process} use.
1018 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001019 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001020 """
1021
Edward Lemur125d60a2019-09-13 18:25:41 +00001022 def __init__(self, branchref=None, issue=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001023 """Create a new ChangeList instance.
1024
Edward Lemurf38bc172019-09-03 21:02:13 +00001025 **kwargs will be passed directly to Gerrit implementation.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001026 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001027 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001028 global settings
1029 if not settings:
1030 # Happens when git_cl.py is used as a utility library.
1031 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001032
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001033 self.branchref = branchref
1034 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001035 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001036 self.branch = ShortBranchName(self.branchref)
1037 else:
1038 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001039 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001040 self.lookedup_issue = False
1041 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001042 self.has_description = False
1043 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001044 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001045 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001046 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001047 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001048 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001049 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001050
Edward Lemur125d60a2019-09-13 18:25:41 +00001051 self._change_id = None
1052 # Lazily cached values.
1053 self._gerrit_host = None # e.g. chromium-review.googlesource.com
1054 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
1055 # Map from change number (issue) to its detail cache.
1056 self._detail_cache = {}
1057
1058 if codereview_host is not None:
1059 assert not codereview_host.startswith('https://'), codereview_host
1060 self._gerrit_host = codereview_host
1061 self._gerrit_server = 'https://%s' % codereview_host
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001062
1063 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001064 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001065
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001066 The return value is a string suitable for passing to git cl with the --cc
1067 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001068 """
1069 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001070 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001071 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001072 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1073 return self.cc
1074
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001075 def GetCCListWithoutDefault(self):
1076 """Return the users cc'd on this CL excluding default ones."""
1077 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001078 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001079 return self.cc
1080
Daniel Cheng7227d212017-11-17 08:12:37 -08001081 def ExtendCC(self, more_cc):
1082 """Extends the list of users to cc on this CL based on the changed files."""
1083 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001084
1085 def GetBranch(self):
1086 """Returns the short branch name, e.g. 'master'."""
1087 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001088 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001089 if not branchref:
1090 return None
1091 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092 self.branch = ShortBranchName(self.branchref)
1093 return self.branch
1094
1095 def GetBranchRef(self):
1096 """Returns the full branch name, e.g. 'refs/heads/master'."""
1097 self.GetBranch() # Poke the lazy loader.
1098 return self.branchref
1099
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001100 def ClearBranch(self):
1101 """Clears cached branch data of this object."""
1102 self.branch = self.branchref = None
1103
tandrii5d48c322016-08-18 16:19:37 -07001104 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1105 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1106 kwargs['branch'] = self.GetBranch()
1107 return _git_get_branch_config_value(key, default, **kwargs)
1108
1109 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1110 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1111 assert self.GetBranch(), (
1112 'this CL must have an associated branch to %sset %s%s' %
1113 ('un' if value is None else '',
1114 key,
1115 '' if value is None else ' to %r' % value))
1116 kwargs['branch'] = self.GetBranch()
1117 return _git_set_branch_config_value(key, value, **kwargs)
1118
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001119 @staticmethod
1120 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001121 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001122 e.g. 'origin', 'refs/heads/master'
1123 """
1124 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001125 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1126
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001127 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001128 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001130 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1131 error_ok=True).strip()
1132 if upstream_branch:
1133 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001135 # Else, try to guess the origin remote.
1136 remote_branches = RunGit(['branch', '-r']).split()
1137 if 'origin/master' in remote_branches:
1138 # Fall back on origin/master if it exits.
1139 remote = 'origin'
1140 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001142 DieWithError(
1143 'Unable to determine default branch to diff against.\n'
1144 'Either pass complete "git diff"-style arguments, like\n'
1145 ' git cl upload origin/master\n'
1146 'or verify this branch is set up to track another \n'
1147 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148
1149 return remote, upstream_branch
1150
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001151 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001152 upstream_branch = self.GetUpstreamBranch()
1153 if not BranchExists(upstream_branch):
1154 DieWithError('The upstream for the current branch (%s) does not exist '
1155 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001156 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001157 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001158
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001159 def GetUpstreamBranch(self):
1160 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001161 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Raul Tambrefe1dbe12019-05-02 04:43:57 +00001162 if remote != '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001163 upstream_branch = upstream_branch.replace('refs/heads/',
1164 'refs/remotes/%s/' % remote)
1165 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1166 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001167 self.upstream_branch = upstream_branch
1168 return self.upstream_branch
1169
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001170 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001171 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001172 remote, branch = None, self.GetBranch()
1173 seen_branches = set()
1174 while branch not in seen_branches:
1175 seen_branches.add(branch)
1176 remote, branch = self.FetchUpstreamTuple(branch)
1177 branch = ShortBranchName(branch)
1178 if remote != '.' or branch.startswith('refs/remotes'):
1179 break
1180 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001181 remotes = RunGit(['remote'], error_ok=True).split()
1182 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001183 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001184 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001185 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001186 logging.warn('Could not determine which remote this change is '
1187 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001188 else:
1189 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001190 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001191 branch = 'HEAD'
1192 if branch.startswith('refs/remotes'):
1193 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001194 elif branch.startswith('refs/branch-heads/'):
1195 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001196 else:
1197 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001198 return self._remote
1199
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001200 def GitSanityChecks(self, upstream_git_obj):
1201 """Checks git repo status and ensures diff is from local commits."""
1202
sbc@chromium.org79706062015-01-14 21:18:12 +00001203 if upstream_git_obj is None:
1204 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001205 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001206 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001207 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001208 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001209 return False
1210
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001211 # Verify the commit we're diffing against is in our current branch.
1212 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1213 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1214 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001215 print('ERROR: %s is not in the current branch. You may need to rebase '
1216 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001217 return False
1218
1219 # List the commits inside the diff, and verify they are all local.
1220 commits_in_diff = RunGit(
1221 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1222 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1223 remote_branch = remote_branch.strip()
1224 if code != 0:
1225 _, remote_branch = self.GetRemoteBranch()
1226
1227 commits_in_remote = RunGit(
1228 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1229
1230 common_commits = set(commits_in_diff) & set(commits_in_remote)
1231 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001232 print('ERROR: Your diff contains %d commits already in %s.\n'
1233 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1234 'the diff. If you are using a custom git flow, you can override'
1235 ' the reference used for this check with "git config '
1236 'gitcl.remotebranch <git-ref>".' % (
1237 len(common_commits), remote_branch, upstream_git_obj),
1238 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001239 return False
1240 return True
1241
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001242 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001243 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001244
1245 Returns None if it is not set.
1246 """
tandrii5d48c322016-08-18 16:19:37 -07001247 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001248
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249 def GetRemoteUrl(self):
1250 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1251
1252 Returns None if there is no remote.
1253 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001254 is_cached, value = self._cached_remote_url
1255 if is_cached:
1256 return value
1257
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001258 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001259 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1260
Edward Lemur298f2cf2019-02-22 21:40:39 +00001261 # Check if the remote url can be parsed as an URL.
1262 host = urlparse.urlparse(url).netloc
1263 if host:
1264 self._cached_remote_url = (True, url)
1265 return url
1266
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001267 # If it cannot be parsed as an url, assume it is a local directory,
1268 # probably a git cache.
Edward Lemur298f2cf2019-02-22 21:40:39 +00001269 logging.warning('"%s" doesn\'t appear to point to a git host. '
1270 'Interpreting it as a local directory.', url)
1271 if not os.path.isdir(url):
1272 logging.error(
1273 'Remote "%s" for branch "%s" points to "%s", but it doesn\'t exist.',
Daniel Bratell4a60db42019-09-16 17:02:52 +00001274 remote, self.GetBranch(), url)
Edward Lemur298f2cf2019-02-22 21:40:39 +00001275 return None
1276
1277 cache_path = url
1278 url = RunGit(['config', 'remote.%s.url' % remote],
1279 error_ok=True,
1280 cwd=url).strip()
1281
1282 host = urlparse.urlparse(url).netloc
1283 if not host:
1284 logging.error(
1285 'Remote "%(remote)s" for branch "%(branch)s" points to '
1286 '"%(cache_path)s", but it is misconfigured.\n'
1287 '"%(cache_path)s" must be a git repo and must have a remote named '
1288 '"%(remote)s" pointing to the git host.', {
1289 'remote': remote,
1290 'cache_path': cache_path,
1291 'branch': self.GetBranch()})
1292 return None
1293
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001294 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001295 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001296
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001297 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001298 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001299 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001300 self.issue = self._GitGetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001301 self.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001302 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303 return self.issue
1304
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001305 def GetIssueURL(self):
1306 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001307 issue = self.GetIssue()
1308 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001309 return None
Edward Lemur125d60a2019-09-13 18:25:41 +00001310 return '%s/%s' % (self.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001311
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001312 def GetDescription(self, pretty=False, force=False):
1313 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001314 if self.GetIssue():
Edward Lemur125d60a2019-09-13 18:25:41 +00001315 self.description = self.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001316 self.has_description = True
1317 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001318 # Set width to 72 columns + 2 space indent.
1319 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001320 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001321 lines = self.description.splitlines()
1322 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001323 return self.description
1324
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001325 def GetDescriptionFooters(self):
1326 """Returns (non_footer_lines, footers) for the commit message.
1327
1328 Returns:
1329 non_footer_lines (list(str)) - Simple list of description lines without
1330 any footer. The lines do not contain newlines, nor does the list contain
1331 the empty line between the message and the footers.
1332 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1333 [("Change-Id", "Ideadbeef...."), ...]
1334 """
1335 raw_description = self.GetDescription()
1336 msg_lines, _, footers = git_footers.split_footers(raw_description)
1337 if footers:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001338 msg_lines = msg_lines[:len(msg_lines) - 1]
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001339 return msg_lines, footers
1340
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001341 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001342 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001343 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001344 self.patchset = self._GitGetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001345 self.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001346 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001347 return self.patchset
1348
1349 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001350 """Set this branch's patchset. If patchset=0, clears the patchset."""
1351 assert self.GetBranch()
1352 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001353 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001354 else:
1355 self.patchset = int(patchset)
1356 self._GitSetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001357 self.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001358
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001359 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001360 """Set this branch's issue. If issue isn't given, clears the issue."""
1361 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001363 issue = int(issue)
1364 self._GitSetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001365 self.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001366 self.issue = issue
Edward Lemur125d60a2019-09-13 18:25:41 +00001367 codereview_server = self.GetCodereviewServer()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001368 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001369 self._GitSetBranchConfigValue(
Edward Lemur125d60a2019-09-13 18:25:41 +00001370 self.CodereviewServerConfigKey(),
tandrii5d48c322016-08-18 16:19:37 -07001371 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001372 else:
tandrii5d48c322016-08-18 16:19:37 -07001373 # Reset all of these just to be clean.
1374 reset_suffixes = [
1375 'last-upload-hash',
Edward Lemur125d60a2019-09-13 18:25:41 +00001376 self.IssueConfigKey(),
1377 self.PatchsetConfigKey(),
1378 self.CodereviewServerConfigKey(),
tandrii5d48c322016-08-18 16:19:37 -07001379 ] + self._PostUnsetIssueProperties()
1380 for prop in reset_suffixes:
1381 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001382 msg = RunGit(['log', '-1', '--format=%B']).strip()
1383 if msg and git_footers.get_footer_change_id(msg):
1384 print('WARNING: The change patched into this branch has a Change-Id. '
1385 'Removing it.')
1386 RunGit(['commit', '--amend', '-m',
1387 git_footers.remove_footer(msg, 'Change-Id')])
Edward Lemurf38bc172019-09-03 21:02:13 +00001388 self.lookedup_issue = True
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001389 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001390 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001391
dnjba1b0f32016-09-02 12:37:42 -07001392 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001393 if not self.GitSanityChecks(upstream_branch):
1394 DieWithError('\nGit sanity check failure')
1395
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001396 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001397 if not root:
1398 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001399 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001400
1401 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001402 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001403 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001404 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001405 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001406 except subprocess2.CalledProcessError:
1407 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001408 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001409 'This branch probably doesn\'t exist anymore. To reset the\n'
1410 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001411 ' git branch --set-upstream-to origin/master %s\n'
1412 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001413 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001414
maruel@chromium.org52424302012-08-29 15:14:30 +00001415 issue = self.GetIssue()
1416 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001417 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001418 description = self.GetDescription()
1419 else:
1420 # If the change was never uploaded, use the log messages of all commits
1421 # up to the branch point, as git cl upload will prefill the description
1422 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001423 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1424 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001425
1426 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001427 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001428 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001429 name,
1430 description,
1431 absroot,
1432 files,
1433 issue,
1434 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001435 author,
1436 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001437
dsansomee2d6fd92016-09-08 00:10:47 -07001438 def UpdateDescription(self, description, force=False):
Edward Lemur125d60a2019-09-13 18:25:41 +00001439 self.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001440 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001441 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001442
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001443 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1444 """Sets the description for this CL remotely.
1445
1446 You can get description_lines and footers with GetDescriptionFooters.
1447
1448 Args:
1449 description_lines (list(str)) - List of CL description lines without
1450 newline characters.
1451 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1452 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1453 `List-Of-Tokens`). It will be case-normalized so that each token is
1454 title-cased.
1455 """
1456 new_description = '\n'.join(description_lines)
1457 if footers:
1458 new_description += '\n'
1459 for k, v in footers:
1460 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1461 if not git_footers.FOOTER_PATTERN.match(foot):
1462 raise ValueError('Invalid footer %r' % foot)
1463 new_description += foot + '\n'
1464 self.UpdateDescription(new_description, force)
1465
Edward Lesmes8e282792018-04-03 18:50:29 -04001466 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001467 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1468 try:
Edward Lemur2c48f242019-06-04 16:14:09 +00001469 start = time_time()
1470 result = presubmit_support.DoPresubmitChecks(change, committing,
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001471 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1472 default_presubmit=None, may_prompt=may_prompt,
Edward Lemur125d60a2019-09-13 18:25:41 +00001473 gerrit_obj=self.GetGerritObjForPresubmit(),
Edward Lesmes8e282792018-04-03 18:50:29 -04001474 parallel=parallel)
Edward Lemur2c48f242019-06-04 16:14:09 +00001475 metrics.collector.add_repeated('sub_commands', {
1476 'command': 'presubmit',
1477 'execution_time': time_time() - start,
1478 'exit_code': 0 if result.should_continue() else 1,
1479 })
1480 return result
vapierfd77ac72016-06-16 08:33:57 -07001481 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001482 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001483
Edward Lemurf38bc172019-09-03 21:02:13 +00001484 def CMDPatchIssue(self, issue_arg, nocommit):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001485 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001486 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1487 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001488 else:
1489 # Assume url.
Edward Lemur125d60a2019-09-13 18:25:41 +00001490 parsed_issue_arg = self.ParseIssueURL(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001491 urlparse.urlparse(issue_arg))
1492 if not parsed_issue_arg or not parsed_issue_arg.valid:
1493 DieWithError('Failed to parse issue argument "%s". '
1494 'Must be an issue number or a valid URL.' % issue_arg)
Edward Lemur125d60a2019-09-13 18:25:41 +00001495 return self.CMDPatchWithParsedIssue(
Edward Lemurf38bc172019-09-03 21:02:13 +00001496 parsed_issue_arg, nocommit, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001497
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001498 def CMDUpload(self, options, git_diff_args, orig_args):
1499 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001500 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001501 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001502 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001503 else:
1504 if self.GetBranch() is None:
1505 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1506
1507 # Default to diffing against common ancestor of upstream branch
1508 base_branch = self.GetCommonAncestorWithUpstream()
1509 git_diff_args = [base_branch, 'HEAD']
1510
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001511 # Fast best-effort checks to abort before running potentially expensive
1512 # hooks if uploading is likely to fail anyway. Passing these checks does
1513 # not guarantee that uploading will not fail.
Edward Lemur125d60a2019-09-13 18:25:41 +00001514 self.EnsureAuthenticated(force=options.force)
1515 self.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001516
1517 # Apply watchlists on upload.
1518 change = self.GetChange(base_branch, None)
1519 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1520 files = [f.LocalPath() for f in change.AffectedFiles()]
1521 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001522 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001523
1524 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001525 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001526 # Set the reviewer list now so that presubmit checks can access it.
1527 change_description = ChangeDescription(change.FullDescriptionText())
1528 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001529 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001530 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001531 change)
1532 change.SetDescriptionText(change_description.description)
1533 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001534 may_prompt=not options.force,
1535 verbose=options.verbose,
1536 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001537 if not hook_results.should_continue():
1538 return 1
1539 if not options.reviewers and hook_results.reviewers:
1540 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001541 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001542
Aaron Gable13101a62018-02-09 13:20:41 -08001543 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001544 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001545 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001546 _git_set_branch_config_value('last-upload-hash',
1547 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001548 # Run post upload hooks, if specified.
1549 if settings.GetRunPostUploadHook():
1550 presubmit_support.DoPostUploadExecuter(
1551 change,
1552 self,
1553 settings.GetRoot(),
1554 options.verbose,
1555 sys.stdout)
1556
1557 # Upload all dependencies if specified.
1558 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001559 print()
1560 print('--dependencies has been specified.')
1561 print('All dependent local branches will be re-uploaded.')
1562 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001563 # Remove the dependencies flag from args so that we do not end up in a
1564 # loop.
1565 orig_args.remove('--dependencies')
1566 ret = upload_branch_deps(self, orig_args)
1567 return ret
1568
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001569 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001570 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001571
1572 Issue must have been already uploaded and known.
1573 """
1574 assert new_state in _CQState.ALL_STATES
1575 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001576 try:
Edward Lemur125d60a2019-09-13 18:25:41 +00001577 vote_map = {
1578 _CQState.NONE: 0,
1579 _CQState.DRY_RUN: 1,
1580 _CQState.COMMIT: 2,
1581 }
1582 labels = {'Commit-Queue': vote_map[new_state]}
1583 notify = False if new_state == _CQState.DRY_RUN else None
1584 gerrit_util.SetReview(
1585 self._GetGerritHost(), self._GerritChangeIdentifier(),
1586 labels=labels, notify=notify)
qyearsley1fdfcb62016-10-24 13:22:03 -07001587 return 0
1588 except KeyboardInterrupt:
1589 raise
1590 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001591 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001592 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001593 ' * Your project has no CQ,\n'
1594 ' * You don\'t have permission to change the CQ state,\n'
1595 ' * There\'s a bug in this code (see stack trace below).\n'
1596 'Consider specifying which bots to trigger manually or asking your '
1597 'project owners for permissions or contacting Chrome Infra at:\n'
1598 'https://www.chromium.org/infra\n\n' %
1599 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001600 # Still raise exception so that stack trace is printed.
1601 raise
1602
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001603 def _GetGerritHost(self):
1604 # Lazy load of configs.
1605 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001606 if self._gerrit_host and '.' not in self._gerrit_host:
1607 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1608 # This happens for internal stuff http://crbug.com/614312.
1609 parsed = urlparse.urlparse(self.GetRemoteUrl())
1610 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001611 print('WARNING: using non-https URLs for remote is likely broken\n'
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001612 ' Your current remote is: %s' % self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001613 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1614 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001615 return self._gerrit_host
1616
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001617 def _GetGitHost(self):
1618 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001619 remote_url = self.GetRemoteUrl()
1620 if not remote_url:
1621 return None
1622 return urlparse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001623
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001624 def GetCodereviewServer(self):
1625 if not self._gerrit_server:
1626 # If we're on a branch then get the server potentially associated
1627 # with that branch.
1628 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001629 self._gerrit_server = self._GitGetBranchConfigValue(
1630 self.CodereviewServerConfigKey())
1631 if self._gerrit_server:
1632 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001633 if not self._gerrit_server:
1634 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1635 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001636 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001637 parts[0] = parts[0] + '-review'
1638 self._gerrit_host = '.'.join(parts)
1639 self._gerrit_server = 'https://%s' % self._gerrit_host
1640 return self._gerrit_server
1641
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001642 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001643 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001644 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001645 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001646 logging.warn('can\'t detect Gerrit project.')
1647 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001648 project = urlparse.urlparse(remote_url).path.strip('/')
1649 if project.endswith('.git'):
1650 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001651 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1652 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1653 # gitiles/git-over-https protocol. E.g.,
1654 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1655 # as
1656 # https://chromium.googlesource.com/v8/v8
1657 if project.startswith('a/'):
1658 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001659 return project
1660
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001661 def _GerritChangeIdentifier(self):
1662 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1663
1664 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001665 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001666 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001667 project = self._GetGerritProject()
1668 if project:
1669 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1670 # Fall back on still unique, but less efficient change number.
1671 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001672
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001673 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001674 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001675 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001676
tandrii5d48c322016-08-18 16:19:37 -07001677 @classmethod
1678 def PatchsetConfigKey(cls):
1679 return 'gerritpatchset'
1680
1681 @classmethod
1682 def CodereviewServerConfigKey(cls):
1683 return 'gerritserver'
1684
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001685 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001686 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00001687 if settings.GetGerritSkipEnsureAuthenticated():
1688 # For projects with unusual authentication schemes.
1689 # See http://crbug.com/603378.
1690 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001691
1692 # Check presence of cookies only if using cookies-based auth method.
1693 cookie_auth = gerrit_util.Authenticator.get()
1694 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001695 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001696
Daniel Chengcf6269b2019-05-18 01:02:12 +00001697 if urlparse.urlparse(self.GetRemoteUrl()).scheme != 'https':
1698 print('WARNING: Ignoring branch %s with non-https remote %s' %
Edward Lemur125d60a2019-09-13 18:25:41 +00001699 (self.branch, self.GetRemoteUrl()))
Daniel Chengcf6269b2019-05-18 01:02:12 +00001700 return
1701
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001702 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001703 self.GetCodereviewServer()
1704 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00001705 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001706
1707 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1708 git_auth = cookie_auth.get_auth_header(git_host)
1709 if gerrit_auth and git_auth:
1710 if gerrit_auth == git_auth:
1711 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001712 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
Raul Tambre80ee78e2019-05-06 22:41:05 +00001713 print(
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001714 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001715 ' %s\n'
1716 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001717 ' Consider running the following command:\n'
1718 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001719 ' %s\n'
Raul Tambre80ee78e2019-05-06 22:41:05 +00001720 ' %s' %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001721 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001722 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001723 cookie_auth.get_new_password_message(git_host)))
1724 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001725 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001726 return
1727 else:
1728 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02001729 ([] if gerrit_auth else [self._gerrit_host]) +
1730 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001731 DieWithError('Credentials for the following hosts are required:\n'
1732 ' %s\n'
1733 'These are read from %s (or legacy %s)\n'
1734 '%s' % (
1735 '\n '.join(missing),
1736 cookie_auth.get_gitcookies_path(),
1737 cookie_auth.get_netrc_path(),
1738 cookie_auth.get_new_password_message(git_host)))
1739
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001740 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001741 if not self.GetIssue():
1742 return
1743
1744 # Warm change details cache now to avoid RPCs later, reducing latency for
1745 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001746 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00001747 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001748
1749 status = self._GetChangeDetail()['status']
1750 if status in ('MERGED', 'ABANDONED'):
1751 DieWithError('Change %s has been %s, new uploads are not allowed' %
1752 (self.GetIssueURL(),
1753 'submitted' if status == 'MERGED' else 'abandoned'))
1754
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001755 # TODO(vadimsh): For some reason the chunk of code below was skipped if
1756 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
1757 # Apparently this check is not very important? Otherwise get_auth_email
1758 # could have been added to other implementations of Authenticator.
1759 cookies_auth = gerrit_util.Authenticator.get()
1760 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001761 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001762
1763 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001764 if self.GetIssueOwner() == cookies_user:
1765 return
1766 logging.debug('change %s owner is %s, cookies user is %s',
1767 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001768 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001769 # so ask what Gerrit thinks of this user.
1770 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
1771 if details['email'] == self.GetIssueOwner():
1772 return
1773 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001774 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001775 'as %s.\n'
1776 'Uploading may fail due to lack of permissions.' %
1777 (self.GetIssue(), self.GetIssueOwner(), details['email']))
1778 confirm_or_exit(action='upload')
1779
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001780 def _PostUnsetIssueProperties(self):
1781 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001782 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001783
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001784 def GetGerritObjForPresubmit(self):
1785 return presubmit_support.GerritAccessor(self._GetGerritHost())
1786
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001787 def GetStatus(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001788 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001789 or CQ status, assuming adherence to a common workflow.
1790
1791 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001792 * 'error' - error from review tool (including deleted issues)
1793 * 'unsent' - no reviewers added
1794 * 'waiting' - waiting for review
1795 * 'reply' - waiting for uploader to reply to review
1796 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00001797 * 'dry-run' - dry-running in the CQ
1798 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07001799 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001800 """
1801 if not self.GetIssue():
1802 return None
1803
1804 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001805 data = self._GetChangeDetail([
1806 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08001807 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001808 return 'error'
1809
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00001810 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001811 return 'closed'
1812
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001813 cq_label = data['labels'].get('Commit-Queue', {})
1814 max_cq_vote = 0
1815 for vote in cq_label.get('all', []):
1816 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
1817 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07001818 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00001819 if max_cq_vote == 1:
1820 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001821
Aaron Gable9ab38c62017-04-06 14:36:33 -07001822 if data['labels'].get('Code-Review', {}).get('approved'):
1823 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001824
1825 if not data.get('reviewers', {}).get('REVIEWER', []):
1826 return 'unsent'
1827
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001828 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07001829 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
1830 last_message_author = messages.pop().get('author', {})
1831 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001832 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
1833 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07001834 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01001835 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07001836 if last_message_author.get('_account_id') == owner:
1837 # Most recent message was by owner.
1838 return 'waiting'
1839 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001840 # Some reply from non-owner.
1841 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07001842
1843 # Somehow there are no messages even though there are reviewers.
1844 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001845
1846 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00001847 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08001848 patchset = data['revisions'][data['current_revision']]['_number']
1849 self.SetPatchset(patchset)
1850 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001851
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001852 def FetchDescription(self, force=False):
1853 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
1854 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00001855 current_rev = data['current_revision']
Dan Beamcf6df902018-11-08 01:48:37 +00001856 return data['revisions'][current_rev]['commit']['message'].encode(
1857 'utf-8', 'ignore')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001858
dsansomee2d6fd92016-09-08 00:10:47 -07001859 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001860 if gerrit_util.HasPendingChangeEdit(
1861 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07001862 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001863 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07001864 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001865 'unpublished edit. Either publish the edit in the Gerrit web UI '
1866 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07001867
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001868 gerrit_util.DeletePendingChangeEdit(
1869 self._GetGerritHost(), self._GerritChangeIdentifier())
1870 gerrit_util.SetCommitMessage(
1871 self._GetGerritHost(), self._GerritChangeIdentifier(),
1872 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001873
Aaron Gable636b13f2017-07-14 10:42:48 -07001874 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001875 gerrit_util.SetReview(
1876 self._GetGerritHost(), self._GerritChangeIdentifier(),
1877 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001878
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001879 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01001880 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001881 # CURRENT_REVISION is included to get the latest patchset so that
1882 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001883 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001884 options=['MESSAGES', 'DETAILED_ACCOUNTS',
1885 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001886 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001887 self._GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001888 robot_file_comments = gerrit_util.GetChangeRobotComments(
1889 self._GetGerritHost(), self._GerritChangeIdentifier())
1890
1891 # Add the robot comments onto the list of comments, but only
1892 # keep those that are from the latest pachset.
1893 latest_patch_set = self.GetMostRecentPatchset()
1894 for path, robot_comments in robot_file_comments.iteritems():
1895 line_comments = file_comments.setdefault(path, [])
1896 line_comments.extend(
1897 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001898
1899 # Build dictionary of file comments for easy access and sorting later.
1900 # {author+date: {path: {patchset: {line: url+message}}}}
1901 comments = collections.defaultdict(
1902 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
1903 for path, line_comments in file_comments.iteritems():
1904 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001905 tag = comment.get('tag', '')
1906 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001907 continue
1908 key = (comment['author']['email'], comment['updated'])
1909 if comment.get('side', 'REVISION') == 'PARENT':
1910 patchset = 'Base'
1911 else:
1912 patchset = 'PS%d' % comment['patch_set']
1913 line = comment.get('line', 0)
1914 url = ('https://%s/c/%s/%s/%s#%s%s' %
1915 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
1916 'b' if comment.get('side') == 'PARENT' else '',
1917 str(line) if line else ''))
1918 comments[key][path][patchset][line] = (url, comment['message'])
1919
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001920 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001921 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001922 summary = self._BuildCommentSummary(msg, comments, readable)
1923 if summary:
1924 summaries.append(summary)
1925 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001926
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001927 @staticmethod
1928 def _BuildCommentSummary(msg, comments, readable):
1929 key = (msg['author']['email'], msg['date'])
1930 # Don't bother showing autogenerated messages that don't have associated
1931 # file or line comments. this will filter out most autogenerated
1932 # messages, but will keep robot comments like those from Tricium.
1933 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
1934 if is_autogenerated and not comments.get(key):
1935 return None
1936 message = msg['message']
1937 # Gerrit spits out nanoseconds.
1938 assert len(msg['date'].split('.')[-1]) == 9
1939 date = datetime.datetime.strptime(msg['date'][:-3],
1940 '%Y-%m-%d %H:%M:%S.%f')
1941 if key in comments:
1942 message += '\n'
1943 for path, patchsets in sorted(comments.get(key, {}).items()):
1944 if readable:
1945 message += '\n%s' % path
1946 for patchset, lines in sorted(patchsets.items()):
1947 for line, (url, content) in sorted(lines.items()):
1948 if line:
1949 line_str = 'Line %d' % line
1950 path_str = '%s:%d:' % (path, line)
1951 else:
1952 line_str = 'File comment'
1953 path_str = '%s:0:' % path
1954 if readable:
1955 message += '\n %s, %s: %s' % (patchset, line_str, url)
1956 message += '\n %s\n' % content
1957 else:
1958 message += '\n%s ' % path_str
1959 message += '\n%s\n' % content
1960
1961 return _CommentSummary(
1962 date=date,
1963 message=message,
1964 sender=msg['author']['email'],
1965 autogenerated=is_autogenerated,
1966 # These could be inferred from the text messages and correlated with
1967 # Code-Review label maximum, however this is not reliable.
1968 # Leaving as is until the need arises.
1969 approval=False,
1970 disapproval=False,
1971 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001972
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001973 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001974 gerrit_util.AbandonChange(
1975 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001976
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001977 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00001978 gerrit_util.SubmitChange(
1979 self._GetGerritHost(), self._GerritChangeIdentifier(),
1980 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001981
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00001982 def _GetChangeDetail(self, options=None, no_cache=False):
1983 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001984
1985 If fresh data is needed, set no_cache=True which will clear cache and
1986 thus new data will be fetched from Gerrit.
1987 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001988 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001989 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001990
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001991 # Optimization to avoid multiple RPCs:
1992 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
1993 'CURRENT_COMMIT' not in options):
1994 options.append('CURRENT_COMMIT')
1995
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001996 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00001997 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001998 options = [o.upper() for o in options]
1999
2000 # Check in cache first unless no_cache is True.
2001 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002002 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002003 else:
2004 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002005 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002006 # Assumption: data fetched before with extra options is suitable
2007 # for return for a smaller set of options.
2008 # For example, if we cached data for
2009 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2010 # and request is for options=[CURRENT_REVISION],
2011 # THEN we can return prior cached data.
2012 if options_set.issubset(cached_options_set):
2013 return data
2014
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002015 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002016 data = gerrit_util.GetChangeDetail(
2017 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002018 except gerrit_util.GerritError as e:
2019 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002020 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002021 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002022
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002023 self._detail_cache.setdefault(cache_key, []).append(
2024 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002025 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002026
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002027 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002028 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002029 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002030 data = gerrit_util.GetChangeCommit(
2031 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002032 except gerrit_util.GerritError as e:
2033 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002034 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002035 raise
agable32978d92016-11-01 12:55:02 -07002036 return data
2037
Karen Qian40c19422019-03-13 21:28:29 +00002038 def _IsCqConfigured(self):
2039 detail = self._GetChangeDetail(['LABELS'])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002040 if u'Commit-Queue' not in detail.get('labels', {}):
Karen Qian40c19422019-03-13 21:28:29 +00002041 return False
2042 # TODO(crbug/753213): Remove temporary hack
2043 if ('https://chromium.googlesource.com/chromium/src' ==
Edward Lemur125d60a2019-09-13 18:25:41 +00002044 self.GetRemoteUrl() and
Karen Qian40c19422019-03-13 21:28:29 +00002045 detail['branch'].startswith('refs/branch-heads/')):
2046 return False
2047 return True
2048
Olivier Robin75ee7252018-04-13 10:02:56 +02002049 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002050 if git_common.is_dirty_git_tree('land'):
2051 return 1
Karen Qian40c19422019-03-13 21:28:29 +00002052
tandriid60367b2016-06-22 05:25:12 -07002053 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00002054 if not force and self._IsCqConfigured():
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002055 confirm_or_exit('\nIt seems this repository has a CQ, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002056 'which can test and land changes for you. '
2057 'Are you sure you wish to bypass it?\n',
2058 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002059 differs = True
tandriic4344b52016-08-29 06:04:54 -07002060 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002061 # Note: git diff outputs nothing if there is no diff.
2062 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002063 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002064 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002065 if detail['current_revision'] == last_upload:
2066 differs = False
2067 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002068 print('WARNING: Local branch contents differ from latest uploaded '
2069 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002070 if differs:
2071 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002072 confirm_or_exit(
2073 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2074 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002075 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002076 elif not bypass_hooks:
2077 hook_results = self.RunHook(
2078 committing=True,
2079 may_prompt=not force,
2080 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002081 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2082 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002083 if not hook_results.should_continue():
2084 return 1
2085
2086 self.SubmitIssue(wait_for_merge=True)
2087 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002088 links = self._GetChangeCommit().get('web_links', [])
2089 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002090 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002091 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002092 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002093 return 0
2094
Edward Lemurf38bc172019-09-03 21:02:13 +00002095 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002096 assert parsed_issue_arg.valid
2097
Edward Lemur125d60a2019-09-13 18:25:41 +00002098 self.issue = parsed_issue_arg.issue
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002099
2100 if parsed_issue_arg.hostname:
2101 self._gerrit_host = parsed_issue_arg.hostname
2102 self._gerrit_server = 'https://%s' % self._gerrit_host
2103
tandriic2405f52016-10-10 08:13:15 -07002104 try:
2105 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002106 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002107 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002108
2109 if not parsed_issue_arg.patchset:
2110 # Use current revision by default.
2111 revision_info = detail['revisions'][detail['current_revision']]
2112 patchset = int(revision_info['_number'])
2113 else:
2114 patchset = parsed_issue_arg.patchset
2115 for revision_info in detail['revisions'].itervalues():
2116 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2117 break
2118 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002119 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002120 (parsed_issue_arg.patchset, self.GetIssue()))
2121
Edward Lemur125d60a2019-09-13 18:25:41 +00002122 remote_url = self.GetRemoteUrl()
Aaron Gable697a91b2018-01-19 15:20:15 -08002123 if remote_url.endswith('.git'):
2124 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002125 remote_url = remote_url.rstrip('/')
2126
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002127 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002128 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002129
2130 if remote_url != fetch_info['url']:
2131 DieWithError('Trying to patch a change from %s but this repo appears '
2132 'to be %s.' % (fetch_info['url'], remote_url))
2133
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002134 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002135
Aaron Gable62619a32017-06-16 08:22:09 -07002136 if force:
2137 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2138 print('Checked out commit for change %i patchset %i locally' %
2139 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002140 elif nocommit:
2141 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2142 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002143 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002144 RunGit(['cherry-pick', 'FETCH_HEAD'])
2145 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002146 (parsed_issue_arg.issue, patchset))
2147 print('Note: this created a local commit which does not have '
2148 'the same hash as the one uploaded for review. This will make '
2149 'uploading changes based on top of this branch difficult.\n'
2150 'If you want to do that, use "git cl patch --force" instead.')
2151
Stefan Zagerd08043c2017-10-12 12:07:02 -07002152 if self.GetBranch():
2153 self.SetIssue(parsed_issue_arg.issue)
2154 self.SetPatchset(patchset)
2155 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2156 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2157 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2158 else:
2159 print('WARNING: You are in detached HEAD state.\n'
2160 'The patch has been applied to your checkout, but you will not be '
2161 'able to upload a new patch set to the gerrit issue.\n'
2162 'Try using the \'-b\' option if you would like to work on a '
2163 'branch and/or upload a new patch set.')
2164
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002165 return 0
2166
2167 @staticmethod
2168 def ParseIssueURL(parsed_url):
2169 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2170 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002171 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2172 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002173 # Short urls like https://domain/<issue_number> can be used, but don't allow
2174 # specifying the patchset (you'd 404), but we allow that here.
2175 if parsed_url.path == '/':
2176 part = parsed_url.fragment
2177 else:
2178 part = parsed_url.path
Bruce Dawson9c062012019-05-02 19:20:28 +00002179 match = re.match(r'(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002180 if match:
2181 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002182 issue=int(match.group(3)),
2183 patchset=int(match.group(5)) if match.group(5) else None,
Edward Lemurf38bc172019-09-03 21:02:13 +00002184 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002185 return None
2186
tandrii16e0b4e2016-06-07 10:34:28 -07002187 def _GerritCommitMsgHookCheck(self, offer_removal):
2188 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2189 if not os.path.exists(hook):
2190 return
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002191 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
2192 # custom developer-made one.
tandrii16e0b4e2016-06-07 10:34:28 -07002193 data = gclient_utils.FileRead(hook)
2194 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2195 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002196 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002197 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002198 'and may interfere with it in subtle ways.\n'
2199 'We recommend you remove the commit-msg hook.')
2200 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002201 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002202 gclient_utils.rm_file_or_tree(hook)
2203 print('Gerrit commit-msg hook removed.')
2204 else:
2205 print('OK, will keep Gerrit commit-msg hook in place.')
2206
Edward Lemur1b52d872019-05-09 21:12:12 +00002207 def _CleanUpOldTraces(self):
2208 """Keep only the last |MAX_TRACES| traces."""
2209 try:
2210 traces = sorted([
2211 os.path.join(TRACES_DIR, f)
2212 for f in os.listdir(TRACES_DIR)
2213 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2214 and not f.startswith('tmp'))
2215 ])
2216 traces_to_delete = traces[:-MAX_TRACES]
2217 for trace in traces_to_delete:
Daniel Chengcf6269b2019-05-18 01:02:12 +00002218 os.remove(trace)
Edward Lemur1b52d872019-05-09 21:12:12 +00002219 except OSError:
2220 print('WARNING: Failed to remove old git traces from\n'
2221 ' %s'
2222 'Consider removing them manually.' % TRACES_DIR)
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002223
Edward Lemur5737f022019-05-17 01:24:00 +00002224 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
Edward Lemur1b52d872019-05-09 21:12:12 +00002225 """Zip and write the git push traces stored in traces_dir."""
2226 gclient_utils.safe_makedirs(TRACES_DIR)
Edward Lemur1b52d872019-05-09 21:12:12 +00002227 traces_zip = trace_name + '-traces'
2228 traces_readme = trace_name + '-README'
Michael Mosse7f0b4c2019-05-08 04:36:24 +00002229 # Create a temporary dir to store git config and gitcookies in. It will be
2230 # compressed and stored next to the traces.
2231 git_info_dir = tempfile.mkdtemp()
Edward Lemur1b52d872019-05-09 21:12:12 +00002232 git_info_zip = trace_name + '-git-info'
2233
Edward Lemur5737f022019-05-17 01:24:00 +00002234 git_push_metadata['now'] = datetime_now().strftime('%c')
Eric Boren67c48202019-05-30 16:52:51 +00002235 if sys.stdin.encoding and sys.stdin.encoding != 'utf-8':
sangwoo.ko7a614332019-05-22 02:46:19 +00002236 git_push_metadata['now'] = git_push_metadata['now'].decode(
2237 sys.stdin.encoding)
2238
Edward Lemur1b52d872019-05-09 21:12:12 +00002239 git_push_metadata['trace_name'] = trace_name
2240 gclient_utils.FileWrite(
2241 traces_readme, TRACES_README_FORMAT % git_push_metadata)
2242
2243 # Keep only the first 6 characters of the git hashes on the packet
2244 # trace. This greatly decreases size after compression.
2245 packet_traces = os.path.join(traces_dir, 'trace-packet')
2246 if os.path.isfile(packet_traces):
2247 contents = gclient_utils.FileRead(packet_traces)
2248 gclient_utils.FileWrite(
2249 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2250 shutil.make_archive(traces_zip, 'zip', traces_dir)
2251
2252 # Collect and compress the git config and gitcookies.
2253 git_config = RunGit(['config', '-l'])
2254 gclient_utils.FileWrite(
2255 os.path.join(git_info_dir, 'git-config'),
2256 git_config)
2257
2258 cookie_auth = gerrit_util.Authenticator.get()
2259 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2260 gitcookies_path = cookie_auth.get_gitcookies_path()
2261 if os.path.isfile(gitcookies_path):
2262 gitcookies = gclient_utils.FileRead(gitcookies_path)
2263 gclient_utils.FileWrite(
2264 os.path.join(git_info_dir, 'gitcookies'),
2265 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2266 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2267
Edward Lemur1b52d872019-05-09 21:12:12 +00002268 gclient_utils.rmtree(git_info_dir)
2269
2270 def _RunGitPushWithTraces(
2271 self, change_desc, refspec, refspec_opts, git_push_metadata):
2272 """Run git push and collect the traces resulting from the execution."""
2273 # Create a temporary directory to store traces in. Traces will be compressed
2274 # and stored in a 'traces' dir inside depot_tools.
2275 traces_dir = tempfile.mkdtemp()
Edward Lemur5737f022019-05-17 01:24:00 +00002276 trace_name = os.path.join(
2277 TRACES_DIR, datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
Edward Lemur0f58ae42019-04-30 17:24:12 +00002278
2279 env = os.environ.copy()
2280 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2281 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Jonathan Nieder9779b142019-05-29 23:19:29 +00002282 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002283 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2284 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2285 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2286
2287 try:
2288 push_returncode = 0
Edward Lemur1b52d872019-05-09 21:12:12 +00002289 remote_url = self.GetRemoteUrl()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002290 before_push = time_time()
2291 push_stdout = gclient_utils.CheckCallAndFilter(
Edward Lemur1b52d872019-05-09 21:12:12 +00002292 ['git', 'push', remote_url, refspec],
Edward Lemur0f58ae42019-04-30 17:24:12 +00002293 env=env,
2294 print_stdout=True,
2295 # Flush after every line: useful for seeing progress when running as
2296 # recipe.
2297 filter_fn=lambda _: sys.stdout.flush())
2298 except subprocess2.CalledProcessError as e:
2299 push_returncode = e.returncode
2300 DieWithError('Failed to create a change. Please examine output above '
2301 'for the reason of the failure.\n'
2302 'Hint: run command below to diagnose common Git/Gerrit '
2303 'credential problems:\n'
Edward Lemur5737f022019-05-17 01:24:00 +00002304 ' git cl creds-check\n'
2305 '\n'
2306 'If git-cl is not working correctly, file a bug under the '
2307 'Infra>SDK component including the files below.\n'
2308 'Review the files before upload, since they might contain '
2309 'sensitive information.\n'
2310 'Set the Restrict-View-Google label so that they are not '
2311 'publicly accessible.\n'
2312 + TRACES_MESSAGE % {'trace_name': trace_name},
Edward Lemur0f58ae42019-04-30 17:24:12 +00002313 change_desc)
2314 finally:
2315 execution_time = time_time() - before_push
2316 metrics.collector.add_repeated('sub_commands', {
2317 'command': 'git push',
2318 'execution_time': execution_time,
2319 'exit_code': push_returncode,
2320 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2321 })
2322
Edward Lemur1b52d872019-05-09 21:12:12 +00002323 git_push_metadata['execution_time'] = execution_time
2324 git_push_metadata['exit_code'] = push_returncode
Edward Lemur5737f022019-05-17 01:24:00 +00002325 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002326
Edward Lemur1b52d872019-05-09 21:12:12 +00002327 self._CleanUpOldTraces()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002328 gclient_utils.rmtree(traces_dir)
2329
2330 return push_stdout
2331
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002332 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002333 """Upload the current branch to Gerrit."""
Mike Frysingera989d552019-08-14 20:51:23 +00002334 if options.squash is None:
tandriia60502f2016-06-20 02:01:53 -07002335 # Load default for user, repo, squash=true, in this order.
2336 options.squash = settings.GetSquashGerritUploads()
tandrii26f3e4e2016-06-10 08:37:04 -07002337
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002338 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002339 branch = GetTargetRef(remote, remote_branch, options.target_branch)
Aaron Gableb56ad332017-01-06 15:24:31 -08002340 # This may be None; default fallback value is determined in logic below.
2341 title = options.title
2342
Dominic Battre7d1c4842017-10-27 09:17:28 +02002343 # Extract bug number from branch name.
2344 bug = options.bug
2345 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2346 if not bug and match:
2347 bug = match.group(1)
2348
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002349 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002350 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002351 if self.GetIssue():
2352 # Try to get the message from a previous upload.
2353 message = self.GetDescription()
2354 if not message:
2355 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002356 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002357 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002358 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002359 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002360 # When uploading a subsequent patchset, -m|--message is taken
2361 # as the patchset title if --title was not provided.
2362 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002363 else:
2364 default_title = RunGit(
2365 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002366 if options.force:
2367 title = default_title
2368 else:
2369 title = ask_for_data(
2370 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002371 change_id = self._GetChangeDetail()['change_id']
2372 while True:
2373 footer_change_ids = git_footers.get_footer_change_id(message)
2374 if footer_change_ids == [change_id]:
2375 break
2376 if not footer_change_ids:
2377 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002378 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002379 continue
2380 # There is already a valid footer but with different or several ids.
2381 # Doing this automatically is non-trivial as we don't want to lose
2382 # existing other footers, yet we want to append just 1 desired
2383 # Change-Id. Thus, just create a new footer, but let user verify the
2384 # new description.
2385 message = '%s\n\nChange-Id: %s' % (message, change_id)
2386 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002387 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002388 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002389 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002390 'Please, check the proposed correction to the description, '
2391 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2392 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2393 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002394 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002395 if not options.force:
2396 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002397 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002398 message = change_desc.description
2399 if not message:
2400 DieWithError("Description is empty. Aborting...")
2401 # Continue the while loop.
2402 # Sanity check of this code - we should end up with proper message
2403 # footer.
2404 assert [change_id] == git_footers.get_footer_change_id(message)
2405 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002406 else: # if not self.GetIssue()
2407 if options.message:
2408 message = options.message
2409 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002410 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002411 if options.title:
2412 message = options.title + '\n\n' + message
2413 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002414
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002415 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002416 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002417 # On first upload, patchset title is always this string, while
2418 # --title flag gets converted to first line of message.
2419 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002420 if not change_desc.description:
2421 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002422 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002423 if len(change_ids) > 1:
2424 DieWithError('too many Change-Id footers, at most 1 allowed.')
2425 if not change_ids:
2426 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002427 change_desc.set_description(git_footers.add_footer_change_id(
2428 change_desc.description,
2429 GenerateGerritChangeId(change_desc.description)))
2430 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002431 assert len(change_ids) == 1
2432 change_id = change_ids[0]
2433
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002434 if options.reviewers or options.tbrs or options.add_owners_to:
2435 change_desc.update_reviewers(options.reviewers, options.tbrs,
2436 options.add_owners_to, change)
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002437 if options.preserve_tryjobs:
2438 change_desc.set_preserve_tryjobs()
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002439
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002440 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002441 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2442 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002443 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002444 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2445 desc_tempfile.write(change_desc.description)
2446 desc_tempfile.close()
2447 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2448 '-F', desc_tempfile.name]).strip()
2449 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002450 else:
2451 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002452 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002453 if not change_desc.description:
2454 DieWithError("Description is empty. Aborting...")
2455
2456 if not git_footers.get_footer_change_id(change_desc.description):
2457 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002458 change_desc.set_description(
2459 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002460 if options.reviewers or options.tbrs or options.add_owners_to:
2461 change_desc.update_reviewers(options.reviewers, options.tbrs,
2462 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002463 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002464 # For no-squash mode, we assume the remote called "origin" is the one we
2465 # want. It is not worthwhile to support different workflows for
2466 # no-squash mode.
2467 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002468 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2469
2470 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002471 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002472 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2473 ref_to_push)]).splitlines()
2474 if len(commits) > 1:
2475 print('WARNING: This will upload %d commits. Run the following command '
2476 'to see which commits will be uploaded: ' % len(commits))
2477 print('git log %s..%s' % (parent, ref_to_push))
2478 print('You can also use `git squash-branch` to squash these into a '
2479 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002480 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002481
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002482 if options.reviewers or options.tbrs or options.add_owners_to:
2483 change_desc.update_reviewers(options.reviewers, options.tbrs,
2484 options.add_owners_to, change)
2485
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002486 reviewers = sorted(change_desc.get_reviewers())
2487 # Add cc's from the CC_LIST and --cc flag (if any).
2488 if not options.private and not options.no_autocc:
2489 cc = self.GetCCList().split(',')
2490 else:
2491 cc = []
2492 if options.cc:
2493 cc.extend(options.cc)
2494 cc = filter(None, [email.strip() for email in cc])
2495 if change_desc.get_cced():
2496 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002497 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2498 valid_accounts = set(reviewers + cc)
2499 # TODO(crbug/877717): relax this for all hosts.
2500 else:
2501 valid_accounts = gerrit_util.ValidAccounts(
2502 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002503 logging.info('accounts %s are recognized, %s invalid',
2504 sorted(valid_accounts),
2505 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002506
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002507 # Extra options that can be specified at push time. Doc:
2508 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002509 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002510
Aaron Gable844cf292017-06-28 11:32:59 -07002511 # By default, new changes are started in WIP mode, and subsequent patchsets
2512 # don't send email. At any time, passing --send-mail will mark the change
2513 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002514 if options.send_mail:
2515 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002516 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002517 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002518 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002519 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002520 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002521
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002522 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002523 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002524
Aaron Gable9b713dd2016-12-14 16:04:21 -08002525 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002526 # Punctuation and whitespace in |title| must be percent-encoded.
2527 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002528
agablec6787972016-09-09 16:13:34 -07002529 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002530 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002531
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002532 for r in sorted(reviewers):
2533 if r in valid_accounts:
2534 refspec_opts.append('r=%s' % r)
2535 reviewers.remove(r)
2536 else:
2537 # TODO(tandrii): this should probably be a hard failure.
2538 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2539 % r)
2540 for c in sorted(cc):
2541 # refspec option will be rejected if cc doesn't correspond to an
2542 # account, even though REST call to add such arbitrary cc may succeed.
2543 if c in valid_accounts:
2544 refspec_opts.append('cc=%s' % c)
2545 cc.remove(c)
2546
rmistry9eadede2016-09-19 11:22:43 -07002547 if options.topic:
2548 # Documentation on Gerrit topics is here:
2549 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002550 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002551
Edward Lemur687ca902018-12-05 02:30:30 +00002552 if options.enable_auto_submit:
2553 refspec_opts.append('l=Auto-Submit+1')
2554 if options.use_commit_queue:
2555 refspec_opts.append('l=Commit-Queue+2')
2556 elif options.cq_dry_run:
2557 refspec_opts.append('l=Commit-Queue+1')
2558
2559 if change_desc.get_reviewers(tbr_only=True):
2560 score = gerrit_util.GetCodeReviewTbrScore(
2561 self._GetGerritHost(),
2562 self._GetGerritProject())
2563 refspec_opts.append('l=Code-Review+%s' % score)
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002564
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002565 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002566 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002567 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002568 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002569 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2570
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002571 refspec_suffix = ''
2572 if refspec_opts:
2573 refspec_suffix = '%' + ','.join(refspec_opts)
2574 assert ' ' not in refspec_suffix, (
2575 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2576 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2577
Edward Lemur1b52d872019-05-09 21:12:12 +00002578 git_push_metadata = {
2579 'gerrit_host': self._GetGerritHost(),
2580 'title': title or '<untitled>',
2581 'change_id': change_id,
2582 'description': change_desc.description,
2583 }
2584 push_stdout = self._RunGitPushWithTraces(
2585 change_desc, refspec, refspec_opts, git_push_metadata)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002586
2587 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002588 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002589 change_numbers = [m.group(1)
2590 for m in map(regex.match, push_stdout.splitlines())
2591 if m]
2592 if len(change_numbers) != 1:
2593 DieWithError(
2594 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002595 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002596 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002597 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002598
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002599 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002600 # GetIssue() is not set in case of non-squash uploads according to tests.
2601 # TODO(agable): non-squash uploads in git cl should be removed.
2602 gerrit_util.AddReviewers(
2603 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002604 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002605 reviewers, cc,
2606 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002607
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002608 return 0
2609
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002610 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2611 change_desc):
2612 """Computes parent of the generated commit to be uploaded to Gerrit.
2613
2614 Returns revision or a ref name.
2615 """
2616 if custom_cl_base:
2617 # Try to avoid creating additional unintended CLs when uploading, unless
2618 # user wants to take this risk.
2619 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2620 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2621 local_ref_of_target_remote])
2622 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002623 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002624 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2625 'If you proceed with upload, more than 1 CL may be created by '
2626 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2627 'If you are certain that specified base `%s` has already been '
2628 'uploaded to Gerrit as another CL, you may proceed.\n' %
2629 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2630 if not force:
2631 confirm_or_exit(
2632 'Do you take responsibility for cleaning up potential mess '
2633 'resulting from proceeding with upload?',
2634 action='upload')
2635 return custom_cl_base
2636
Aaron Gablef97e33d2017-03-30 15:44:27 -07002637 if remote != '.':
2638 return self.GetCommonAncestorWithUpstream()
2639
2640 # If our upstream branch is local, we base our squashed commit on its
2641 # squashed version.
2642 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2643
Aaron Gablef97e33d2017-03-30 15:44:27 -07002644 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002645 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002646
2647 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002648 # TODO(tandrii): consider checking parent change in Gerrit and using its
2649 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2650 # the tree hash of the parent branch. The upside is less likely bogus
2651 # requests to reupload parent change just because it's uploadhash is
2652 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07002653 parent = RunGit(['config',
2654 'branch.%s.gerritsquashhash' % upstream_branch_name],
2655 error_ok=True).strip()
2656 # Verify that the upstream branch has been uploaded too, otherwise
2657 # Gerrit will create additional CLs when uploading.
2658 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2659 RunGitSilent(['rev-parse', parent + ':'])):
2660 DieWithError(
2661 '\nUpload upstream branch %s first.\n'
2662 'It is likely that this branch has been rebased since its last '
2663 'upload, so you just need to upload it again.\n'
2664 '(If you uploaded it with --no-squash, then branch dependencies '
2665 'are not supported, and you should reupload with --squash.)'
2666 % upstream_branch_name,
2667 change_desc)
2668 return parent
2669
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002670 def _AddChangeIdToCommitMessage(self, options, args):
2671 """Re-commits using the current message, assumes the commit hook is in
2672 place.
2673 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002674 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002675 git_command = ['commit', '--amend', '-m', log_desc]
2676 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002677 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002678 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002679 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002680 return new_log_desc
2681 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002682 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002683
tandriie113dfd2016-10-11 10:20:12 -07002684 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002685 try:
2686 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002687 except GerritChangeNotExists:
2688 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002689
2690 if data['status'] in ('ABANDONED', 'MERGED'):
2691 return 'CL %s is closed' % self.GetIssue()
2692
Edward Lemur2c210a42019-09-16 23:58:35 +00002693 def GetTryJobProperties(self, patchset=None):
2694 """Returns dictionary of properties to launch a tryjob."""
Edward Lemur5b6ae8b2019-09-12 23:27:24 +00002695 data = self._GetChangeDetail(['ALL_REVISIONS'])
Edward Lemur2c210a42019-09-16 23:58:35 +00002696 patchset = int(patchset or self.GetPatchset())
2697 assert patchset
2698 revision_data = None # Pylint wants it to be defined.
2699 for revision_data in data['revisions'].itervalues():
2700 if int(revision_data['_number']) == patchset:
2701 break
2702 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002703 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002704 (patchset, self.GetIssue()))
2705 return {
Edward Lemur2c210a42019-09-16 23:58:35 +00002706 'patch_issue': self.GetIssue(),
2707 'patch_set': patchset or self.GetPatchset(),
2708 'patch_project': data['project'],
2709 'patch_storage': 'gerrit',
2710 'patch_ref': revision_data['fetch']['http']['ref'],
2711 'patch_repository_url': revision_data['fetch']['http']['url'],
2712 'patch_gerrit_url': self.GetCodereviewServer(),
tandrii8c5a3532016-11-04 07:52:02 -07002713 }
tandriie113dfd2016-10-11 10:20:12 -07002714
tandriide281ae2016-10-12 06:02:30 -07002715 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002716 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002717
Edward Lemur707d70b2018-02-07 00:50:14 +01002718 def GetReviewers(self):
2719 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00002720 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01002721
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002722
2723_CODEREVIEW_IMPLEMENTATIONS = {
Edward Lemur125d60a2019-09-13 18:25:41 +00002724 'gerrit': Changelist,
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002725}
2726
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002727
iannuccie53c9352016-08-17 14:40:40 -07002728def _add_codereview_issue_select_options(parser, extra=""):
2729 _add_codereview_select_options(parser)
2730
2731 text = ('Operate on this issue number instead of the current branch\'s '
2732 'implicit issue.')
2733 if extra:
2734 text += ' '+extra
2735 parser.add_option('-i', '--issue', type=int, help=text)
2736
2737
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002738def _add_codereview_select_options(parser):
Edward Lemurf38bc172019-09-03 21:02:13 +00002739 """Appends --gerrit option to force specific codereview."""
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002740 parser.codereview_group = optparse.OptionGroup(
Edward Lemurf38bc172019-09-03 21:02:13 +00002741 parser, 'DEPRECATED! Codereview override options')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002742 parser.add_option_group(parser.codereview_group)
2743 parser.codereview_group.add_option(
2744 '--gerrit', action='store_true',
Edward Lemurf38bc172019-09-03 21:02:13 +00002745 help='Deprecated. Noop. Do not use.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002746
2747
2748def _process_codereview_select_options(parser, options):
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002749 options.forced_codereview = None
2750 if options.gerrit:
2751 options.forced_codereview = 'gerrit'
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002752
2753
tandriif9aefb72016-07-01 09:06:51 -07002754def _get_bug_line_values(default_project, bugs):
2755 """Given default_project and comma separated list of bugs, yields bug line
2756 values.
2757
2758 Each bug can be either:
2759 * a number, which is combined with default_project
2760 * string, which is left as is.
2761
2762 This function may produce more than one line, because bugdroid expects one
2763 project per line.
2764
2765 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2766 ['v8:123', 'chromium:789']
2767 """
2768 default_bugs = []
2769 others = []
2770 for bug in bugs.split(','):
2771 bug = bug.strip()
2772 if bug:
2773 try:
2774 default_bugs.append(int(bug))
2775 except ValueError:
2776 others.append(bug)
2777
2778 if default_bugs:
2779 default_bugs = ','.join(map(str, default_bugs))
2780 if default_project:
2781 yield '%s:%s' % (default_project, default_bugs)
2782 else:
2783 yield default_bugs
2784 for other in sorted(others):
2785 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2786 yield other
2787
2788
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002789class ChangeDescription(object):
2790 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002791 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07002792 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07002793 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01002794 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08002795 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
2796 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
2797 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
2798 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002799
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002800 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00002801 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002802
agable@chromium.org42c20792013-09-12 17:34:49 +00002803 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08002804 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00002805 return '\n'.join(self._description_lines)
2806
2807 def set_description(self, desc):
2808 if isinstance(desc, basestring):
2809 lines = desc.splitlines()
2810 else:
2811 lines = [line.rstrip() for line in desc]
2812 while lines and not lines[0]:
2813 lines.pop(0)
2814 while lines and not lines[-1]:
2815 lines.pop(-1)
2816 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002817
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002818 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
2819 """Rewrites the R=/TBR= line(s) as a single line each.
2820
2821 Args:
2822 reviewers (list(str)) - list of additional emails to use for reviewers.
2823 tbrs (list(str)) - list of additional emails to use for TBRs.
2824 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
2825 the change that are missing OWNER coverage. If this is not None, you
2826 must also pass a value for `change`.
2827 change (Change) - The Change that should be used for OWNERS lookups.
2828 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002829 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002830 assert isinstance(tbrs, list), tbrs
2831
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002832 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07002833 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002834
2835 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002836 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002837
2838 reviewers = set(reviewers)
2839 tbrs = set(tbrs)
2840 LOOKUP = {
2841 'TBR': tbrs,
2842 'R': reviewers,
2843 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002844
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002845 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00002846 regexp = re.compile(self.R_LINE)
2847 matches = [regexp.match(line) for line in self._description_lines]
2848 new_desc = [l for i, l in enumerate(self._description_lines)
2849 if not matches[i]]
2850 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002851
agable@chromium.org42c20792013-09-12 17:34:49 +00002852 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002853
2854 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00002855 for match in matches:
2856 if not match:
2857 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002858 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
2859
2860 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002861 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00002862 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02002863 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00002864 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07002865 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002866 LOOKUP[add_owners_to].update(
2867 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002868
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002869 # If any folks ended up in both groups, remove them from tbrs.
2870 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002871
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002872 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
2873 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00002874
2875 # Put the new lines in the description where the old first R= line was.
2876 line_loc = next((i for i, match in enumerate(matches) if match), -1)
2877 if 0 <= line_loc < len(self._description_lines):
2878 if new_tbr_line:
2879 self._description_lines.insert(line_loc, new_tbr_line)
2880 if new_r_line:
2881 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002882 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00002883 if new_r_line:
2884 self.append_footer(new_r_line)
2885 if new_tbr_line:
2886 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002887
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002888 def set_preserve_tryjobs(self):
2889 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
2890 footers = git_footers.parse_footers(self.description)
2891 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
2892 if v.lower() == 'true':
2893 return
2894 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
2895
Aaron Gable3a16ed12017-03-23 10:51:55 -07002896 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002897 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002898 self.set_description([
2899 '# Enter a description of the change.',
2900 '# This will be displayed on the codereview site.',
2901 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00002902 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00002903 '--------------------',
2904 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002905
agable@chromium.org42c20792013-09-12 17:34:49 +00002906 regexp = re.compile(self.BUG_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00002907 prefix = settings.GetBugPrefix()
agable@chromium.org42c20792013-09-12 17:34:49 +00002908 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07002909 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07002910 if git_footer:
2911 self.append_footer('Bug: %s' % ', '.join(values))
2912 else:
2913 for value in values:
2914 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07002915
agable@chromium.org42c20792013-09-12 17:34:49 +00002916 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00002917 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002918 if not content:
2919 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00002920 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002921
Bruce Dawson2377b012018-01-11 16:46:49 -08002922 # Strip off comments and default inserted "Bug:" line.
2923 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00002924 (line.startswith('#') or
2925 line.rstrip() == "Bug:" or
2926 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00002927 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00002928 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00002929 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002930
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002931 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00002932 """Adds a footer line to the description.
2933
2934 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
2935 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
2936 that Gerrit footers are always at the end.
2937 """
2938 parsed_footer_line = git_footers.parse_footer(line)
2939 if parsed_footer_line:
2940 # Line is a gerrit footer in the form: Footer-Key: any value.
2941 # Thus, must be appended observing Gerrit footer rules.
2942 self.set_description(
2943 git_footers.add_footer(self.description,
2944 key=parsed_footer_line[0],
2945 value=parsed_footer_line[1]))
2946 return
2947
2948 if not self._description_lines:
2949 self._description_lines.append(line)
2950 return
2951
2952 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
2953 if gerrit_footers:
2954 # git_footers.split_footers ensures that there is an empty line before
2955 # actual (gerrit) footers, if any. We have to keep it that way.
2956 assert top_lines and top_lines[-1] == ''
2957 top_lines, separator = top_lines[:-1], top_lines[-1:]
2958 else:
2959 separator = [] # No need for separator if there are no gerrit_footers.
2960
2961 prev_line = top_lines[-1] if top_lines else ''
2962 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
2963 not presubmit_support.Change.TAG_LINE_RE.match(line)):
2964 top_lines.append('')
2965 top_lines.append(line)
2966 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002967
tandrii99a72f22016-08-17 14:33:24 -07002968 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002969 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00002970 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07002971 reviewers = [match.group(2).strip()
2972 for match in matches
2973 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002974 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002975
bradnelsond975b302016-10-23 12:20:23 -07002976 def get_cced(self):
2977 """Retrieves the list of reviewers."""
2978 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
2979 cced = [match.group(2).strip() for match in matches if match]
2980 return cleanup_list(cced)
2981
Nodir Turakulov23b82142017-11-16 11:04:25 -08002982 def get_hash_tags(self):
2983 """Extracts and sanitizes a list of Gerrit hashtags."""
2984 subject = (self._description_lines or ('',))[0]
2985 subject = re.sub(
2986 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
2987
2988 tags = []
2989 start = 0
2990 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
2991 while True:
2992 m = bracket_exp.match(subject, start)
2993 if not m:
2994 break
2995 tags.append(self.sanitize_hash_tag(m.group(1)))
2996 start = m.end()
2997
2998 if not tags:
2999 # Try "Tag: " prefix.
3000 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3001 if m:
3002 tags.append(self.sanitize_hash_tag(m.group(1)))
3003 return tags
3004
3005 @classmethod
3006 def sanitize_hash_tag(cls, tag):
3007 """Returns a sanitized Gerrit hash tag.
3008
3009 A sanitized hashtag can be used as a git push refspec parameter value.
3010 """
3011 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3012
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003013 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3014 """Updates this commit description given the parent.
3015
3016 This is essentially what Gnumbd used to do.
3017 Consult https://goo.gl/WMmpDe for more details.
3018 """
3019 assert parent_msg # No, orphan branch creation isn't supported.
3020 assert parent_hash
3021 assert dest_ref
3022 parent_footer_map = git_footers.parse_footers(parent_msg)
3023 # This will also happily parse svn-position, which GnumbD is no longer
3024 # supporting. While we'd generate correct footers, the verifier plugin
3025 # installed in Gerrit will block such commit (ie git push below will fail).
3026 parent_position = git_footers.get_position(parent_footer_map)
3027
3028 # Cherry-picks may have last line obscuring their prior footers,
3029 # from git_footers perspective. This is also what Gnumbd did.
3030 cp_line = None
3031 if (self._description_lines and
3032 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3033 cp_line = self._description_lines.pop()
3034
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003035 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003036
3037 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3038 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003039 for i, line in enumerate(footer_lines):
3040 k, v = git_footers.parse_footer(line) or (None, None)
3041 if k and k.startswith('Cr-'):
3042 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003043
3044 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003045 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003046 if parent_position[0] == dest_ref:
3047 # Same branch as parent.
3048 number = int(parent_position[1]) + 1
3049 else:
3050 number = 1 # New branch, and extra lineage.
3051 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3052 int(parent_position[1])))
3053
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003054 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3055 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003056
3057 self._description_lines = top_lines
3058 if cp_line:
3059 self._description_lines.append(cp_line)
3060 if self._description_lines[-1] != '':
3061 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003062 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003063
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003064
Aaron Gablea1bab272017-04-11 16:38:18 -07003065def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003066 """Retrieves the reviewers that approved a CL from the issue properties with
3067 messages.
3068
3069 Note that the list may contain reviewers that are not committer, thus are not
3070 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003071
3072 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003073 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003074 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003075 return sorted(
3076 set(
3077 message['sender']
3078 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003079 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003080 )
3081 )
3082
3083
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003084def FindCodereviewSettingsFile(filename='codereview.settings'):
3085 """Finds the given file starting in the cwd and going up.
3086
3087 Only looks up to the top of the repository unless an
3088 'inherit-review-settings-ok' file exists in the root of the repository.
3089 """
3090 inherit_ok_file = 'inherit-review-settings-ok'
3091 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003092 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003093 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3094 root = '/'
3095 while True:
3096 if filename in os.listdir(cwd):
3097 if os.path.isfile(os.path.join(cwd, filename)):
3098 return open(os.path.join(cwd, filename))
3099 if cwd == root:
3100 break
3101 cwd = os.path.dirname(cwd)
3102
3103
3104def LoadCodereviewSettingsFromFile(fileobj):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003105 """Parses a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003106 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003107
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003108 def SetProperty(name, setting, unset_error_ok=False):
3109 fullname = 'rietveld.' + name
3110 if setting in keyvals:
3111 RunGit(['config', fullname, keyvals[setting]])
3112 else:
3113 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3114
tandrii48df5812016-10-17 03:55:37 -07003115 if not keyvals.get('GERRIT_HOST', False):
3116 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003117 # Only server setting is required. Other settings can be absent.
3118 # In that case, we ignore errors raised during option deletion attempt.
3119 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3120 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3121 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003122 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003123 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3124 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003125 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3126 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003127
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003128 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003129 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003130
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003131 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003132 RunGit(['config', 'gerrit.squash-uploads',
3133 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003134
tandrii@chromium.org28253532016-04-14 13:46:56 +00003135 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003136 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003137 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3138
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003139 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003140 # should be of the form
3141 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3142 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003143 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3144 keyvals['ORIGIN_URL_CONFIG']])
3145
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003146
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003147def urlretrieve(source, destination):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003148 """Downloads a network object to a local file, like urllib.urlretrieve.
3149
3150 This is necessary because urllib is broken for SSL connections via a proxy.
3151 """
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003152 with open(destination, 'w') as f:
3153 f.write(urllib2.urlopen(source).read())
3154
3155
ukai@chromium.org712d6102013-11-27 00:52:58 +00003156def hasSheBang(fname):
3157 """Checks fname is a #! script."""
3158 with open(fname) as f:
3159 return f.read(2).startswith('#!')
3160
3161
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003162# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3163def DownloadHooks(*args, **kwargs):
3164 pass
3165
3166
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003167def DownloadGerritHook(force):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003168 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003169
3170 Args:
3171 force: True to update hooks. False to install hooks if not present.
3172 """
3173 if not settings.GetIsGerrit():
3174 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003175 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003176 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3177 if not os.access(dst, os.X_OK):
3178 if os.path.exists(dst):
3179 if not force:
3180 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003181 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003182 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003183 if not hasSheBang(dst):
3184 DieWithError('Not a script: %s\n'
3185 'You need to download from\n%s\n'
3186 'into .git/hooks/commit-msg and '
3187 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003188 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3189 except Exception:
3190 if os.path.exists(dst):
3191 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003192 DieWithError('\nFailed to download hooks.\n'
3193 'You need to download from\n%s\n'
3194 'into .git/hooks/commit-msg and '
3195 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003196
3197
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003198class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003199 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003200
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003201 _GOOGLESOURCE = 'googlesource.com'
3202
3203 def __init__(self):
3204 # Cached list of [host, identity, source], where source is either
3205 # .gitcookies or .netrc.
3206 self._all_hosts = None
3207
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003208 def ensure_configured_gitcookies(self):
3209 """Runs checks and suggests fixes to make git use .gitcookies from default
3210 path."""
3211 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3212 configured_path = RunGitSilent(
3213 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003214 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003215 if configured_path:
3216 self._ensure_default_gitcookies_path(configured_path, default)
3217 else:
3218 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003219
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003220 @staticmethod
3221 def _ensure_default_gitcookies_path(configured_path, default_path):
3222 assert configured_path
3223 if configured_path == default_path:
3224 print('git is already configured to use your .gitcookies from %s' %
3225 configured_path)
3226 return
3227
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003228 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003229 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3230 (configured_path, default_path))
3231
3232 if not os.path.exists(configured_path):
3233 print('However, your configured .gitcookies file is missing.')
3234 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3235 action='reconfigure')
3236 RunGit(['config', '--global', 'http.cookiefile', default_path])
3237 return
3238
3239 if os.path.exists(default_path):
3240 print('WARNING: default .gitcookies file already exists %s' %
3241 default_path)
3242 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3243 default_path)
3244
3245 confirm_or_exit('Move existing .gitcookies to default location?',
3246 action='move')
3247 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003248 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003249 print('Moved and reconfigured git to use .gitcookies from %s' %
3250 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003251
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003252 @staticmethod
3253 def _configure_gitcookies_path(default_path):
3254 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3255 if os.path.exists(netrc_path):
3256 print('You seem to be using outdated .netrc for git credentials: %s' %
3257 netrc_path)
3258 print('This tool will guide you through setting up recommended '
3259 '.gitcookies store for git credentials.\n'
3260 '\n'
3261 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3262 ' git config --global --unset http.cookiefile\n'
3263 ' mv %s %s.backup\n\n' % (default_path, default_path))
3264 confirm_or_exit(action='setup .gitcookies')
3265 RunGit(['config', '--global', 'http.cookiefile', default_path])
3266 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003267
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003268 def get_hosts_with_creds(self, include_netrc=False):
3269 if self._all_hosts is None:
3270 a = gerrit_util.CookiesAuthenticator()
3271 self._all_hosts = [
3272 (h, u, s)
3273 for h, u, s in itertools.chain(
3274 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3275 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3276 )
3277 if h.endswith(self._GOOGLESOURCE)
3278 ]
3279
3280 if include_netrc:
3281 return self._all_hosts
3282 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3283
3284 def print_current_creds(self, include_netrc=False):
3285 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3286 if not hosts:
3287 print('No Git/Gerrit credentials found')
3288 return
3289 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3290 header = [('Host', 'User', 'Which file'),
3291 ['=' * l for l in lengths]]
3292 for row in (header + hosts):
3293 print('\t'.join((('%%+%ds' % l) % s)
3294 for l, s in zip(lengths, row)))
3295
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003296 @staticmethod
3297 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003298 """Parses identity "git-<username>.domain" into <username> and domain."""
3299 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003300 # distinguishable from sub-domains. But we do know typical domains:
3301 if identity.endswith('.chromium.org'):
3302 domain = 'chromium.org'
3303 username = identity[:-len('.chromium.org')]
3304 else:
3305 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003306 if username.startswith('git-'):
3307 username = username[len('git-'):]
3308 return username, domain
3309
3310 def _get_usernames_of_domain(self, domain):
3311 """Returns list of usernames referenced by .gitcookies in a given domain."""
3312 identities_by_domain = {}
3313 for _, identity, _ in self.get_hosts_with_creds():
3314 username, domain = self._parse_identity(identity)
3315 identities_by_domain.setdefault(domain, []).append(username)
3316 return identities_by_domain.get(domain)
3317
3318 def _canonical_git_googlesource_host(self, host):
3319 """Normalizes Gerrit hosts (with '-review') to Git host."""
3320 assert host.endswith(self._GOOGLESOURCE)
3321 # Prefix doesn't include '.' at the end.
3322 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3323 if prefix.endswith('-review'):
3324 prefix = prefix[:-len('-review')]
3325 return prefix + '.' + self._GOOGLESOURCE
3326
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003327 def _canonical_gerrit_googlesource_host(self, host):
3328 git_host = self._canonical_git_googlesource_host(host)
3329 prefix = git_host.split('.', 1)[0]
3330 return prefix + '-review.' + self._GOOGLESOURCE
3331
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003332 def _get_counterpart_host(self, host):
3333 assert host.endswith(self._GOOGLESOURCE)
3334 git = self._canonical_git_googlesource_host(host)
3335 gerrit = self._canonical_gerrit_googlesource_host(git)
3336 return git if gerrit == host else gerrit
3337
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003338 def has_generic_host(self):
3339 """Returns whether generic .googlesource.com has been configured.
3340
3341 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3342 """
3343 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3344 if host == '.' + self._GOOGLESOURCE:
3345 return True
3346 return False
3347
3348 def _get_git_gerrit_identity_pairs(self):
3349 """Returns map from canonic host to pair of identities (Git, Gerrit).
3350
3351 One of identities might be None, meaning not configured.
3352 """
3353 host_to_identity_pairs = {}
3354 for host, identity, _ in self.get_hosts_with_creds():
3355 canonical = self._canonical_git_googlesource_host(host)
3356 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3357 idx = 0 if canonical == host else 1
3358 pair[idx] = identity
3359 return host_to_identity_pairs
3360
3361 def get_partially_configured_hosts(self):
3362 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003363 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3364 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3365 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003366
3367 def get_conflicting_hosts(self):
3368 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003369 host
3370 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003371 if None not in (i1, i2) and i1 != i2)
3372
3373 def get_duplicated_hosts(self):
3374 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3375 return set(host for host, count in counters.iteritems() if count > 1)
3376
3377 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3378 'chromium.googlesource.com': 'chromium.org',
3379 'chrome-internal.googlesource.com': 'google.com',
3380 }
3381
3382 def get_hosts_with_wrong_identities(self):
3383 """Finds hosts which **likely** reference wrong identities.
3384
3385 Note: skips hosts which have conflicting identities for Git and Gerrit.
3386 """
3387 hosts = set()
3388 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3389 pair = self._get_git_gerrit_identity_pairs().get(host)
3390 if pair and pair[0] == pair[1]:
3391 _, domain = self._parse_identity(pair[0])
3392 if domain != expected:
3393 hosts.add(host)
3394 return hosts
3395
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003396 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003397 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003398 hosts = sorted(hosts)
3399 assert hosts
3400 if extra_column_func is None:
3401 extras = [''] * len(hosts)
3402 else:
3403 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003404 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3405 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003406 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003407 lines.append(tmpl % he)
3408 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003409
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003410 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003411 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003412 yield ('.googlesource.com wildcard record detected',
3413 ['Chrome Infrastructure team recommends to list full host names '
3414 'explicitly.'],
3415 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003416
3417 dups = self.get_duplicated_hosts()
3418 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003419 yield ('The following hosts were defined twice',
3420 self._format_hosts(dups),
3421 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003422
3423 partial = self.get_partially_configured_hosts()
3424 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003425 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3426 'These hosts are missing',
3427 self._format_hosts(partial, lambda host: 'but %s defined' %
3428 self._get_counterpart_host(host)),
3429 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003430
3431 conflicting = self.get_conflicting_hosts()
3432 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003433 yield ('The following Git hosts have differing credentials from their '
3434 'Gerrit counterparts',
3435 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3436 tuple(self._get_git_gerrit_identity_pairs()[host])),
3437 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003438
3439 wrong = self.get_hosts_with_wrong_identities()
3440 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003441 yield ('These hosts likely use wrong identity',
3442 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3443 (self._get_git_gerrit_identity_pairs()[host][0],
3444 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3445 wrong)
3446
3447 def find_and_report_problems(self):
3448 """Returns True if there was at least one problem, else False."""
3449 found = False
3450 bad_hosts = set()
3451 for title, sublines, hosts in self._find_problems():
3452 if not found:
3453 found = True
3454 print('\n\n.gitcookies problem report:\n')
3455 bad_hosts.update(hosts or [])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003456 print(' %s%s' % (title, (':' if sublines else '')))
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003457 if sublines:
3458 print()
3459 print(' %s' % '\n '.join(sublines))
3460 print()
3461
3462 if bad_hosts:
3463 assert found
3464 print(' You can manually remove corresponding lines in your %s file and '
3465 'visit the following URLs with correct account to generate '
3466 'correct credential lines:\n' %
3467 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3468 print(' %s' % '\n '.join(sorted(set(
3469 gerrit_util.CookiesAuthenticator().get_new_password_url(
3470 self._canonical_git_googlesource_host(host))
3471 for host in bad_hosts
3472 ))))
3473 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003474
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003475
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003476@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003477def CMDcreds_check(parser, args):
3478 """Checks credentials and suggests changes."""
3479 _, _ = parser.parse_args(args)
3480
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003481 # Code below checks .gitcookies. Abort if using something else.
3482 authn = gerrit_util.Authenticator.get()
3483 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3484 if isinstance(authn, gerrit_util.GceAuthenticator):
3485 DieWithError(
3486 'This command is not designed for GCE, are you on a bot?\n'
3487 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3488 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003489 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003490 'This command is not designed for bot environment. It checks '
3491 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003492
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003493 checker = _GitCookiesChecker()
3494 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003495
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003496 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003497 checker.print_current_creds(include_netrc=True)
3498
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003499 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003500 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003501 return 0
3502 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003503
3504
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003505@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003506def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003507 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003508 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3509 branch = ShortBranchName(branchref)
3510 _, args = parser.parse_args(args)
3511 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003512 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003513 return RunGit(['config', 'branch.%s.base-url' % branch],
3514 error_ok=False).strip()
3515 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003516 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003517 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3518 error_ok=False).strip()
3519
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003520
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003521def color_for_status(status):
3522 """Maps a Changelist status to color, for CMDstatus and other tools."""
3523 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003524 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003525 'waiting': Fore.BLUE,
3526 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003527 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003528 'lgtm': Fore.GREEN,
3529 'commit': Fore.MAGENTA,
3530 'closed': Fore.CYAN,
3531 'error': Fore.WHITE,
3532 }.get(status, Fore.WHITE)
3533
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003534
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003535def get_cl_statuses(changes, fine_grained, max_processes=None):
3536 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003537
3538 If fine_grained is true, this will fetch CL statuses from the server.
3539 Otherwise, simply indicate if there's a matching url for the given branches.
3540
3541 If max_processes is specified, it is used as the maximum number of processes
3542 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3543 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003544
3545 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003546 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003547 if not changes:
3548 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003549
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003550 if not fine_grained:
3551 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003552 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003553 for cl in changes:
3554 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003555 return
3556
3557 # First, sort out authentication issues.
3558 logging.debug('ensuring credentials exist')
3559 for cl in changes:
3560 cl.EnsureAuthenticated(force=False, refresh=True)
3561
3562 def fetch(cl):
3563 try:
3564 return (cl, cl.GetStatus())
3565 except:
3566 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003567 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003568 raise
3569
3570 threads_count = len(changes)
3571 if max_processes:
3572 threads_count = max(1, min(threads_count, max_processes))
3573 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3574
3575 pool = ThreadPool(threads_count)
3576 fetched_cls = set()
3577 try:
3578 it = pool.imap_unordered(fetch, changes).__iter__()
3579 while True:
3580 try:
3581 cl, status = it.next(timeout=5)
3582 except multiprocessing.TimeoutError:
3583 break
3584 fetched_cls.add(cl)
3585 yield cl, status
3586 finally:
3587 pool.close()
3588
3589 # Add any branches that failed to fetch.
3590 for cl in set(changes) - fetched_cls:
3591 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003592
rmistry@google.com2dd99862015-06-22 12:22:18 +00003593
3594def upload_branch_deps(cl, args):
3595 """Uploads CLs of local branches that are dependents of the current branch.
3596
3597 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003598
3599 test1 -> test2.1 -> test3.1
3600 -> test3.2
3601 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003602
3603 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3604 run on the dependent branches in this order:
3605 test2.1, test3.1, test3.2, test2.2, test3.3
3606
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003607 Note: This function does not rebase your local dependent branches. Use it
3608 when you make a change to the parent branch that will not conflict
3609 with its dependent branches, and you would like their dependencies
3610 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003611 """
3612 if git_common.is_dirty_git_tree('upload-branch-deps'):
3613 return 1
3614
3615 root_branch = cl.GetBranch()
3616 if root_branch is None:
3617 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3618 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003619 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003620 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3621 'patchset dependencies without an uploaded CL.')
3622
3623 branches = RunGit(['for-each-ref',
3624 '--format=%(refname:short) %(upstream:short)',
3625 'refs/heads'])
3626 if not branches:
3627 print('No local branches found.')
3628 return 0
3629
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003630 # Create a dictionary of all local branches to the branches that are
3631 # dependent on it.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003632 tracked_to_dependents = collections.defaultdict(list)
3633 for b in branches.splitlines():
3634 tokens = b.split()
3635 if len(tokens) == 2:
3636 branch_name, tracked = tokens
3637 tracked_to_dependents[tracked].append(branch_name)
3638
vapiera7fbd5a2016-06-16 09:17:49 -07003639 print()
3640 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003641 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003642
rmistry@google.com2dd99862015-06-22 12:22:18 +00003643 def traverse_dependents_preorder(branch, padding=''):
3644 dependents_to_process = tracked_to_dependents.get(branch, [])
3645 padding += ' '
3646 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003647 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003648 dependents.append(dependent)
3649 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003650
rmistry@google.com2dd99862015-06-22 12:22:18 +00003651 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003652 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003653
3654 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003655 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003656 return 0
3657
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003658 confirm_or_exit('This command will checkout all dependent branches and run '
3659 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003660
rmistry@google.com2dd99862015-06-22 12:22:18 +00003661 # Record all dependents that failed to upload.
3662 failures = {}
3663 # Go through all dependents, checkout the branch and upload.
3664 try:
3665 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003666 print()
3667 print('--------------------------------------')
3668 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003669 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003670 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003671 try:
3672 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003673 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003674 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003675 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003676 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003677 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003678 finally:
3679 # Swap back to the original root branch.
3680 RunGit(['checkout', '-q', root_branch])
3681
vapiera7fbd5a2016-06-16 09:17:49 -07003682 print()
3683 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003684 for dependent_branch in dependents:
3685 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003686 print(' %s : %s' % (dependent_branch, upload_status))
3687 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003688
3689 return 0
3690
3691
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003692@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003693def CMDarchive(parser, args):
3694 """Archives and deletes branches associated with closed changelists."""
3695 parser.add_option(
3696 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003697 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003698 parser.add_option(
3699 '-f', '--force', action='store_true',
3700 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003701 parser.add_option(
3702 '-d', '--dry-run', action='store_true',
3703 help='Skip the branch tagging and removal steps.')
3704 parser.add_option(
3705 '-t', '--notags', action='store_true',
3706 help='Do not tag archived branches. '
3707 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003708
kmarshall3bff56b2016-06-06 18:31:47 -07003709 options, args = parser.parse_args(args)
3710 if args:
3711 parser.error('Unsupported args: %s' % ' '.join(args))
kmarshall3bff56b2016-06-06 18:31:47 -07003712
3713 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3714 if not branches:
3715 return 0
3716
vapiera7fbd5a2016-06-16 09:17:49 -07003717 print('Finding all branches associated with closed issues...')
Edward Lemur934836a2019-09-09 20:16:54 +00003718 changes = [Changelist(branchref=b)
3719 for b in branches.splitlines()]
kmarshall3bff56b2016-06-06 18:31:47 -07003720 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3721 statuses = get_cl_statuses(changes,
3722 fine_grained=True,
3723 max_processes=options.maxjobs)
3724 proposal = [(cl.GetBranch(),
3725 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3726 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00003727 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07003728 proposal.sort()
3729
3730 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003731 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003732 return 0
3733
3734 current_branch = GetCurrentBranch()
3735
vapiera7fbd5a2016-06-16 09:17:49 -07003736 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003737 if options.notags:
3738 for next_item in proposal:
3739 print(' ' + next_item[0])
3740 else:
3741 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3742 for next_item in proposal:
3743 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003744
kmarshall9249e012016-08-23 12:02:16 -07003745 # Quit now on precondition failure or if instructed by the user, either
3746 # via an interactive prompt or by command line flags.
3747 if options.dry_run:
3748 print('\nNo changes were made (dry run).\n')
3749 return 0
3750 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003751 print('You are currently on a branch \'%s\' which is associated with a '
3752 'closed codereview issue, so archive cannot proceed. Please '
3753 'checkout another branch and run this command again.' %
3754 current_branch)
3755 return 1
kmarshall9249e012016-08-23 12:02:16 -07003756 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003757 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3758 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003759 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003760 return 1
3761
3762 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003763 if not options.notags:
3764 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003765 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003766
vapiera7fbd5a2016-06-16 09:17:49 -07003767 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003768
3769 return 0
3770
3771
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003772@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003773def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003774 """Show status of changelists.
3775
3776 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003777 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07003778 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003779 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07003780 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00003781 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003782 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07003783 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003784
3785 Also see 'git cl comments'.
3786 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00003787 parser.add_option(
3788 '--no-branch-color',
3789 action='store_true',
3790 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003791 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003792 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003793 parser.add_option('-f', '--fast', action='store_true',
3794 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003795 parser.add_option(
3796 '-j', '--maxjobs', action='store', type=int,
3797 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003798
iannuccie53c9352016-08-17 14:40:40 -07003799 _add_codereview_issue_select_options(
3800 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003801 options, args = parser.parse_args(args)
Edward Lemurf38bc172019-09-03 21:02:13 +00003802 _process_codereview_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003803 if args:
3804 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003805
iannuccie53c9352016-08-17 14:40:40 -07003806 if options.issue is not None and not options.field:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00003807 parser.error('--field must be specified with --issue.')
iannucci3c972b92016-08-17 13:24:10 -07003808
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003809 if options.field:
Edward Lemur934836a2019-09-09 20:16:54 +00003810 cl = Changelist(issue=options.issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003811 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003812 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003813 elif options.field == 'id':
3814 issueid = cl.GetIssue()
3815 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003816 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003817 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08003818 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003819 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003820 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003821 elif options.field == 'status':
3822 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003823 elif options.field == 'url':
3824 url = cl.GetIssueURL()
3825 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003826 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003827 return 0
3828
3829 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3830 if not branches:
3831 print('No local branch found.')
3832 return 0
3833
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003834 changes = [
Edward Lemur934836a2019-09-09 20:16:54 +00003835 Changelist(branchref=b)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003836 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003837 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003838 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003839 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003840 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003841
Daniel McArdlea23bf592019-02-12 00:25:12 +00003842 current_branch = GetCurrentBranch()
3843
3844 def FormatBranchName(branch, colorize=False):
3845 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
3846 an asterisk when it is the current branch."""
3847
3848 asterisk = ""
3849 color = Fore.RESET
3850 if branch == current_branch:
3851 asterisk = "* "
3852 color = Fore.GREEN
3853 branch_name = ShortBranchName(branch)
3854
3855 if colorize:
3856 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00003857 return asterisk + branch_name
3858
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003859 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00003860
3861 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003862 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3863 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003864 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003865 c, status = output.next()
3866 branch_statuses[c.GetBranch()] = status
3867 status = branch_statuses.pop(branch)
3868 url = cl.GetIssueURL()
3869 if url and (not status or status == 'error'):
3870 # The issue probably doesn't exist anymore.
3871 url += ' (broken)'
3872
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003873 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003874 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003875 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003876 color = ''
3877 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003878 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00003879
Alan Cuttera3be9a52019-03-04 18:50:33 +00003880 branch_display = FormatBranchName(branch)
3881 padding = ' ' * (alignment - len(branch_display))
3882 if not options.no_branch_color:
3883 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00003884
Alan Cuttera3be9a52019-03-04 18:50:33 +00003885 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
3886 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003887
vapiera7fbd5a2016-06-16 09:17:49 -07003888 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00003889 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003890 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00003891 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01003892 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003893 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003894 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003895 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003896 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003897 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003898 print('Issue description:')
3899 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003900 return 0
3901
3902
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003903def colorize_CMDstatus_doc():
3904 """To be called once in main() to add colors to git cl status help."""
3905 colors = [i for i in dir(Fore) if i[0].isupper()]
3906
3907 def colorize_line(line):
3908 for color in colors:
3909 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003910 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003911 indent = len(line) - len(line.lstrip(' ')) + 1
3912 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3913 return line
3914
3915 lines = CMDstatus.__doc__.splitlines()
3916 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3917
3918
phajdan.jre328cf92016-08-22 04:12:17 -07003919def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07003920 if path == '-':
3921 json.dump(contents, sys.stdout)
3922 else:
3923 with open(path, 'w') as f:
3924 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07003925
3926
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003927@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003928@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003929def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003930 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003931
3932 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003933 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003934 parser.add_option('-r', '--reverse', action='store_true',
3935 help='Lookup the branch(es) for the specified issues. If '
3936 'no issues are specified, all branches with mapped '
3937 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07003938 parser.add_option('--json',
3939 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003940 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003941 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003942 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003943
dnj@chromium.org406c4402015-03-03 17:22:28 +00003944 if options.reverse:
3945 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08003946 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00003947 # Reverse issue lookup.
3948 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00003949
3950 git_config = {}
3951 for config in RunGit(['config', '--get-regexp',
3952 r'branch\..*issue']).splitlines():
3953 name, _space, val = config.partition(' ')
3954 git_config[name] = val
3955
dnj@chromium.org406c4402015-03-03 17:22:28 +00003956 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00003957 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
3958 config_key = _git_branch_config_key(ShortBranchName(branch),
3959 cls.IssueConfigKey())
3960 issue = git_config.get(config_key)
3961 if issue:
3962 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003963 if not args:
3964 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003965 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003966 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00003967 try:
3968 issue_num = int(issue)
3969 except ValueError:
3970 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003971 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00003972 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07003973 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00003974 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003975 if options.json:
3976 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07003977 return 0
3978
3979 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00003980 issue = ParseIssueNumberArgument(args[0])
Aaron Gable78753da2017-06-15 10:35:49 -07003981 if not issue.valid:
3982 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
3983 'or no argument to list it.\n'
3984 'Maybe you want to run git cl status?')
Edward Lemurf38bc172019-09-03 21:02:13 +00003985 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07003986 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003987 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00003988 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07003989 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
3990 if options.json:
3991 write_json(options.json, {
3992 'issue': cl.GetIssue(),
3993 'issue_url': cl.GetIssueURL(),
3994 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003995 return 0
3996
3997
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003998@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003999def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004000 """Shows or posts review comments for any changelist."""
4001 parser.add_option('-a', '--add-comment', dest='comment',
4002 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004003 parser.add_option('-p', '--publish', action='store_true',
4004 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004005 parser.add_option('-i', '--issue', dest='issue',
Edward Lemurf38bc172019-09-03 21:02:13 +00004006 help='review issue id (defaults to current issue).')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004007 parser.add_option('-m', '--machine-readable', dest='readable',
4008 action='store_false', default=True,
4009 help='output comments in a format compatible with '
4010 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004011 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004012 help='File to write JSON summary to, or "-" for stdout')
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004013 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004014 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004015 _process_codereview_select_options(parser, options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004016
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004017 issue = None
4018 if options.issue:
4019 try:
4020 issue = int(options.issue)
4021 except ValueError:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004022 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004023
Edward Lemur934836a2019-09-09 20:16:54 +00004024 cl = Changelist(issue=issue)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004025
4026 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004027 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004028 return 0
4029
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004030 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4031 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004032 for comment in summary:
4033 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004034 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004035 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004036 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004037 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004038 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00004039 elif comment.autogenerated:
4040 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004041 else:
4042 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004043 print('\n%s%s %s%s\n%s' % (
4044 color,
4045 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4046 comment.sender,
4047 Fore.RESET,
4048 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4049
smut@google.comc85ac942015-09-15 16:34:43 +00004050 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004051 def pre_serialize(c):
4052 dct = c.__dict__.copy()
4053 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4054 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004055 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004056 return 0
4057
4058
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004059@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004060@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004061def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004062 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004063 parser.add_option('-d', '--display', action='store_true',
4064 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004065 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004066 help='New description to set for this issue (- for stdin, '
4067 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004068 parser.add_option('-f', '--force', action='store_true',
4069 help='Delete any unpublished Gerrit edits for this issue '
4070 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004071
4072 _add_codereview_select_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004073 options, args = parser.parse_args(args)
4074 _process_codereview_select_options(parser, options)
4075
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004076 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004077 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00004078 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004079 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004080 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004081
Edward Lemur934836a2019-09-09 20:16:54 +00004082 kwargs = {}
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004083 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004084 if target_issue_arg:
4085 kwargs['issue'] = target_issue_arg.issue
4086 kwargs['codereview_host'] = target_issue_arg.hostname
Edward Lemurf38bc172019-09-03 21:02:13 +00004087 if not args[0].isdigit() and not options.forced_codereview:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004088 detected_codereview_from_url = True
martiniss6eda05f2016-06-30 10:18:35 -07004089
4090 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004091 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004092 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004093 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004094
4095 if detected_codereview_from_url:
Edward Lemurf38bc172019-09-03 21:02:13 +00004096 logging.info('canonical issue/change URL: %s\n', cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004097
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004098 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004099
smut@google.com34fb6b12015-07-13 20:03:26 +00004100 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004101 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004102 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004103
4104 if options.new_description:
4105 text = options.new_description
4106 if text == '-':
4107 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004108 elif text == '+':
4109 base_branch = cl.GetCommonAncestorWithUpstream()
4110 change = cl.GetChange(base_branch, None, local_description=True)
4111 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004112
4113 description.set_description(text)
4114 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00004115 description.prompt()
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004116 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004117 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004118 return 0
4119
4120
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004121@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004122def CMDlint(parser, args):
4123 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004124 parser.add_option('--filter', action='append', metavar='-x,+y',
4125 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004126 options, args = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004127
4128 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004129 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004130 try:
4131 import cpplint
4132 import cpplint_chromium
4133 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004134 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004135 return 1
4136
4137 # Change the current working directory before calling lint so that it
4138 # shows the correct base.
4139 previous_cwd = os.getcwd()
4140 os.chdir(settings.GetRoot())
4141 try:
Edward Lemur934836a2019-09-09 20:16:54 +00004142 cl = Changelist()
thestig@chromium.org44202a22014-03-11 19:22:18 +00004143 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4144 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004145 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004146 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004147 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004148
4149 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004150 command = args + files
4151 if options.filter:
4152 command = ['--filter=' + ','.join(options.filter)] + command
4153 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004154
4155 white_regex = re.compile(settings.GetLintRegex())
4156 black_regex = re.compile(settings.GetLintIgnoreRegex())
4157 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4158 for filename in filenames:
4159 if white_regex.match(filename):
4160 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004161 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004162 else:
4163 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4164 extra_check_functions)
4165 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004166 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004167 finally:
4168 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004169 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004170 if cpplint._cpplint_state.error_count != 0:
4171 return 1
4172 return 0
4173
4174
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004175@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004176def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004177 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004178 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004179 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004180 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004181 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004182 parser.add_option('--all', action='store_true',
4183 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004184 parser.add_option('--parallel', action='store_true',
4185 help='Run all tests specified by input_api.RunTests in all '
4186 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004187 options, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004188
sbc@chromium.org71437c02015-04-09 19:29:40 +00004189 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004190 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004191 return 1
4192
Edward Lemur934836a2019-09-09 20:16:54 +00004193 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004194 if args:
4195 base_branch = args[0]
4196 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004197 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004198 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004199
Aaron Gable8076c282017-11-29 14:39:41 -08004200 if options.all:
4201 base_change = cl.GetChange(base_branch, None)
4202 files = [('M', f) for f in base_change.AllFiles()]
4203 change = presubmit_support.GitChange(
4204 base_change.Name(),
4205 base_change.FullDescriptionText(),
4206 base_change.RepositoryRoot(),
4207 files,
4208 base_change.issue,
4209 base_change.patchset,
4210 base_change.author_email,
4211 base_change._upstream)
4212 else:
4213 change = cl.GetChange(base_branch, None)
4214
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004215 cl.RunHook(
4216 committing=not options.upload,
4217 may_prompt=False,
4218 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004219 change=change,
4220 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004221 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004222
4223
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004224def GenerateGerritChangeId(message):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004225 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004226
4227 Works the same way as
4228 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4229 but can be called on demand on all platforms.
4230
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004231 The basic idea is to generate git hash of a state of the tree, original
4232 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004233 """
4234 lines = []
4235 tree_hash = RunGitSilent(['write-tree'])
4236 lines.append('tree %s' % tree_hash.strip())
4237 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4238 if code == 0:
4239 lines.append('parent %s' % parent.strip())
4240 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4241 lines.append('author %s' % author.strip())
4242 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4243 lines.append('committer %s' % committer.strip())
4244 lines.append('')
4245 # Note: Gerrit's commit-hook actually cleans message of some lines and
4246 # whitespace. This code is not doing this, but it clearly won't decrease
4247 # entropy.
4248 lines.append(message)
4249 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00004250 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004251 return 'I%s' % change_hash.strip()
4252
4253
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004254def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004255 """Computes the remote branch ref to use for the CL.
4256
4257 Args:
4258 remote (str): The git remote for the CL.
4259 remote_branch (str): The git remote branch for the CL.
4260 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004261 """
4262 if not (remote and remote_branch):
4263 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004264
wittman@chromium.org455dc922015-01-26 20:15:50 +00004265 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004266 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004267 # refs, which are then translated into the remote full symbolic refs
4268 # below.
4269 if '/' not in target_branch:
4270 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4271 else:
4272 prefix_replacements = (
4273 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4274 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4275 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4276 )
4277 match = None
4278 for regex, replacement in prefix_replacements:
4279 match = re.search(regex, target_branch)
4280 if match:
4281 remote_branch = target_branch.replace(match.group(0), replacement)
4282 break
4283 if not match:
4284 # This is a branch path but not one we recognize; use as-is.
4285 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004286 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4287 # Handle the refs that need to land in different refs.
4288 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004289
wittman@chromium.org455dc922015-01-26 20:15:50 +00004290 # Create the true path to the remote branch.
4291 # Does the following translation:
4292 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4293 # * refs/remotes/origin/master -> refs/heads/master
4294 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4295 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4296 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4297 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4298 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4299 'refs/heads/')
4300 elif remote_branch.startswith('refs/remotes/branch-heads'):
4301 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004302
wittman@chromium.org455dc922015-01-26 20:15:50 +00004303 return remote_branch
4304
4305
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004306def cleanup_list(l):
4307 """Fixes a list so that comma separated items are put as individual items.
4308
4309 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4310 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4311 """
4312 items = sum((i.split(',') for i in l), [])
4313 stripped_items = (i.strip() for i in items)
4314 return sorted(filter(None, stripped_items))
4315
4316
Aaron Gable4db38df2017-11-03 14:59:07 -07004317@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004318@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004319def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004320 """Uploads the current changelist to codereview.
4321
4322 Can skip dependency patchset uploads for a branch by running:
4323 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004324 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00004325 git config --unset branch.branch_name.skip-deps-uploads
4326 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004327
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004328 If the name of the checked out branch starts with "bug-" or "fix-" followed
4329 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02004330 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004331
4332 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004333 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004334 [git-cl] add support for hashtags
4335 Foo bar: implement foo
4336 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004337 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004338 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4339 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004340 parser.add_option('--bypass-watchlists', action='store_true',
4341 dest='bypass_watchlists',
4342 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004343 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004344 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004345 parser.add_option('--message', '-m', dest='message',
4346 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004347 parser.add_option('-b', '--bug',
4348 help='pre-populate the bug number(s) for this issue. '
4349 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004350 parser.add_option('--message-file', dest='message_file',
4351 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004352 parser.add_option('--title', '-t', dest='title',
4353 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004354 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004355 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004356 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004357 parser.add_option('--tbrs',
4358 action='append', default=[],
4359 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004360 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004361 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004362 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004363 parser.add_option('--hashtag', dest='hashtags',
4364 action='append', default=[],
4365 help=('Gerrit hashtag for new CL; '
4366 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004367 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004368 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004369 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004370 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004371 metavar='TARGET',
4372 help='Apply CL to remote ref TARGET. ' +
4373 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004374 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004375 help='Squash multiple commits into one')
Mike Frysingera989d552019-08-14 20:51:23 +00004376 parser.add_option('--no-squash', action='store_false', dest='squash',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004377 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004378 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004379 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004380 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4381 const='TBR', help='add a set of OWNERS to TBR')
4382 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4383 const='R', help='add a set of OWNERS to R')
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004384 parser.add_option('-c', '--use-commit-queue', action='store_true',
4385 help='tell the CQ to commit this patchset; '
4386 'implies --send-mail')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004387 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4388 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004389 help='Send the patchset to do a CQ dry run right after '
4390 'upload.')
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00004391 parser.add_option('--preserve-tryjobs', action='store_true',
4392 help='instruct the CQ to let tryjobs running even after '
4393 'new patchsets are uploaded instead of canceling '
4394 'prior patchset\' tryjobs')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004395 parser.add_option('--dependencies', action='store_true',
4396 help='Uploads CLs of all the local branches that depend on '
4397 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004398 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4399 help='Sends your change to the CQ after an approval. Only '
4400 'works on repos that have the Auto-Submit label '
4401 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004402 parser.add_option('--parallel', action='store_true',
4403 help='Run all tests specified by input_api.RunTests in all '
4404 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004405
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004406 parser.add_option('--no-autocc', action='store_true',
4407 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004408 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004409 help='Set the review private. This implies --no-autocc.')
4410
rmistry@google.com2dd99862015-06-22 12:22:18 +00004411 orig_args = args
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004412 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004413 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004414 _process_codereview_select_options(parser, options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004415
sbc@chromium.org71437c02015-04-09 19:29:40 +00004416 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004417 return 1
4418
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004419 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004420 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004421 options.cc = cleanup_list(options.cc)
4422
tandriib80458a2016-06-23 12:20:07 -07004423 if options.message_file:
4424 if options.message:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004425 parser.error('Only one of --message and --message-file allowed.')
tandriib80458a2016-06-23 12:20:07 -07004426 options.message = gclient_utils.FileRead(options.message_file)
4427 options.message_file = None
4428
tandrii4d0545a2016-07-06 03:56:49 -07004429 if options.cq_dry_run and options.use_commit_queue:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004430 parser.error('Only one of --use-commit-queue and --cq-dry-run allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07004431
Aaron Gableedbc4132017-09-11 13:22:28 -07004432 if options.use_commit_queue:
4433 options.send_mail = True
4434
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004435 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4436 settings.GetIsGerrit()
4437
Edward Lemur934836a2019-09-09 20:16:54 +00004438 cl = Changelist()
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004439
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004440 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004441
4442
Francois Dorayd42c6812017-05-30 15:10:20 -04004443@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004444@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004445def CMDsplit(parser, args):
4446 """Splits a branch into smaller branches and uploads CLs.
4447
4448 Creates a branch and uploads a CL for each group of files modified in the
4449 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004450 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004451 the shared OWNERS file.
4452 """
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004453 parser.add_option('-d', '--description', dest='description_file',
4454 help='A text file containing a CL description in which '
4455 '$directory will be replaced by each CL\'s directory.')
4456 parser.add_option('-c', '--comment', dest='comment_file',
4457 help='A text file containing a CL comment.')
4458 parser.add_option('-n', '--dry-run', dest='dry_run', action='store_true',
Chris Watkinsba28e462017-12-13 11:22:17 +11004459 default=False,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004460 help='List the files and reviewers for each CL that would '
4461 'be created, but don\'t create branches or CLs.')
4462 parser.add_option('--cq-dry-run', action='store_true',
4463 help='If set, will do a cq dry run for each uploaded CL. '
4464 'Please be careful when doing this; more than ~10 CLs '
4465 'has the potential to overload our build '
4466 'infrastructure. Try to upload these not during high '
4467 'load times (usually 11-3 Mountain View time). Email '
4468 'infra-dev@chromium.org with any questions.')
Takuto Ikuta51eca592019-02-14 19:40:52 +00004469 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4470 default=True,
4471 help='Sends your change to the CQ after an approval. Only '
4472 'works on repos that have the Auto-Submit label '
4473 'enabled')
Francois Dorayd42c6812017-05-30 15:10:20 -04004474 options, _ = parser.parse_args(args)
4475
4476 if not options.description_file:
4477 parser.error('No --description flag specified.')
4478
4479 def WrappedCMDupload(args):
4480 return CMDupload(OptionParser(), args)
4481
4482 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004483 Changelist, WrappedCMDupload, options.dry_run,
Takuto Ikuta51eca592019-02-14 19:40:52 +00004484 options.cq_dry_run, options.enable_auto_submit)
Francois Dorayd42c6812017-05-30 15:10:20 -04004485
4486
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004487@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004488@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004489def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004490 """DEPRECATED: Used to commit the current changelist via git-svn."""
4491 message = ('git-cl no longer supports committing to SVN repositories via '
4492 'git-svn. You probably want to use `git cl land` instead.')
4493 print(message)
4494 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004495
4496
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004497# Two special branches used by git cl land.
4498MERGE_BRANCH = 'git-cl-commit'
4499CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4500
4501
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004502@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004503@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004504def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004505 """Commits the current changelist via git.
4506
4507 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4508 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004509 """
4510 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4511 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004512 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004513 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004514 parser.add_option('--parallel', action='store_true',
4515 help='Run all tests specified by input_api.RunTests in all '
4516 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004517 (options, args) = parser.parse_args(args)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004518
Edward Lemur934836a2019-09-09 20:16:54 +00004519 cl = Changelist()
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004520
Robert Iannucci2e73d432018-03-14 01:10:47 -07004521 if not cl.GetIssue():
4522 DieWithError('You must upload the change first to Gerrit.\n'
4523 ' If you would rather have `git cl land` upload '
4524 'automatically for you, see http://crbug.com/642759')
Edward Lemur125d60a2019-09-13 18:25:41 +00004525 return cl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004526 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004527
4528
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004529@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004530@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004531def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004532 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004533 parser.add_option('-b', dest='newbranch',
4534 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004535 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004536 help='overwrite state on the current or chosen branch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004537 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
Edward Lemurf38bc172019-09-03 21:02:13 +00004538 help='don\'t commit after patch applies.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004539
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004540 group = optparse.OptionGroup(
4541 parser,
4542 'Options for continuing work on the current issue uploaded from a '
4543 'different clone (e.g. different machine). Must be used independently '
4544 'from the other options. No issue number should be specified, and the '
4545 'branch must have an issue number associated with it')
4546 group.add_option('--reapply', action='store_true', dest='reapply',
4547 help='Reset the branch and reapply the issue.\n'
4548 'CAUTION: This will undo any local changes in this '
4549 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004550
4551 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004552 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004553 parser.add_option_group(group)
4554
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004555 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004556 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004557 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004558
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004559 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004560 if options.newbranch:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004561 parser.error('--reapply works on the current branch only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004562 if len(args) > 0:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004563 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004564
Edward Lemur934836a2019-09-09 20:16:54 +00004565 cl = Changelist()
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004566 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004567 parser.error('Current branch must have an associated issue.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004568
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004569 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004570 if upstream is None:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004571 parser.error('No upstream branch specified. Cannot reset branch.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004572
4573 RunGit(['reset', '--hard', upstream])
4574 if options.pull:
4575 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004576
Edward Lemurf38bc172019-09-03 21:02:13 +00004577 return cl.CMDPatchIssue(cl.GetIssue(), options.nocommit)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004578
4579 if len(args) != 1 or not args[0]:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004580 parser.error('Must specify issue number or URL.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004581
Edward Lemurf38bc172019-09-03 21:02:13 +00004582 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004583 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004584 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004585
4586 cl_kwargs = {
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004587 'codereview_host': target_issue_arg.hostname,
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004588 }
4589 detected_codereview_from_url = False
Edward Lemurf38bc172019-09-03 21:02:13 +00004590 if not args[0].isdigit() and not options.forced_codereview:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004591 detected_codereview_from_url = True
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004592 cl_kwargs['issue'] = target_issue_arg.issue
4593
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004594 # We don't want uncommitted changes mixed up with the patch.
4595 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004596 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004597
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004598 if options.newbranch:
4599 if options.force:
4600 RunGit(['branch', '-D', options.newbranch],
4601 stderr=subprocess2.PIPE, error_ok=True)
4602 RunGit(['new-branch', options.newbranch])
4603
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004604 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004605
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004606 if detected_codereview_from_url:
Edward Lemurf38bc172019-09-03 21:02:13 +00004607 print('canonical issue/change URL: %s\n' % cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004608
Edward Lemurf38bc172019-09-03 21:02:13 +00004609 return cl.CMDPatchWithParsedIssue(
4610 target_issue_arg, options.nocommit, options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004611
4612
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004613def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004614 """Fetches the tree status and returns either 'open', 'closed',
4615 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004616 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004617 if url:
4618 status = urllib2.urlopen(url).read().lower()
4619 if status.find('closed') != -1 or status == '0':
4620 return 'closed'
4621 elif status.find('open') != -1 or status == '1':
4622 return 'open'
4623 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004624 return 'unset'
4625
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004626
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004627def GetTreeStatusReason():
4628 """Fetches the tree status from a json url and returns the message
4629 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004630 url = settings.GetTreeStatusUrl()
4631 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004632 connection = urllib2.urlopen(json_url)
4633 status = json.loads(connection.read())
4634 connection.close()
4635 return status['message']
4636
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004637
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004638@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004639def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004640 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004641 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004642 status = GetTreeStatus()
4643 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004644 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004645 return 2
4646
vapiera7fbd5a2016-06-16 09:17:49 -07004647 print('The tree is %s' % status)
4648 print()
4649 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004650 if status != 'open':
4651 return 1
4652 return 0
4653
4654
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004655@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00004656def CMDtry(parser, args):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004657 """Triggers tryjobs using either Buildbucket or CQ dry run."""
4658 group = optparse.OptionGroup(parser, 'Tryjob options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004659 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004660 '-b', '--bot', action='append',
4661 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4662 'times to specify multiple builders. ex: '
4663 '"-b win_rel -b win_layout". See '
4664 'the try server waterfall for the builders name and the tests '
4665 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004666 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004667 '-B', '--bucket', default='',
4668 help=('Buildbucket bucket to send the try requests.'))
4669 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004670 '-r', '--revision',
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004671 help='Revision to use for the tryjob; default: the revision will '
tandriif7b29d42016-10-07 08:45:41 -07004672 'be determined by the try recipe that builder runs, which usually '
4673 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004674 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004675 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004676 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004677 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004678 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004679 '--category', default='git_cl_try', help='Specify custom build category.')
4680 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004681 '--project',
4682 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004683 'in recipe to determine to which repository or directory to '
4684 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004685 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004686 '-p', '--property', dest='properties', action='append', default=[],
4687 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004688 'key2=value2 etc. The value will be treated as '
4689 'json if decodable, or as string otherwise. '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004690 'NOTE: using this may make your tryjob not usable for CQ, '
4691 'which will then schedule another tryjob with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004692 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004693 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4694 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004695 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004696 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09004697 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004698 options, args = parser.parse_args(args)
Edward Lemurf38bc172019-09-03 21:02:13 +00004699 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004700 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004701
machenbach@chromium.org45453142015-09-15 08:45:22 +00004702 # Make sure that all properties are prop=value pairs.
4703 bad_params = [x for x in options.properties if '=' not in x]
4704 if bad_params:
4705 parser.error('Got properties with missing "=": %s' % bad_params)
4706
maruel@chromium.org15192402012-09-06 12:38:29 +00004707 if args:
4708 parser.error('Unknown arguments: %s' % args)
4709
Edward Lemur934836a2019-09-09 20:16:54 +00004710 cl = Changelist(issue=options.issue)
maruel@chromium.org15192402012-09-06 12:38:29 +00004711 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004712 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004713
Edward Lemurf38bc172019-09-03 21:02:13 +00004714 # HACK: warm up Gerrit change detail cache to save on RPCs.
Edward Lemur125d60a2019-09-13 18:25:41 +00004715 cl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004716
tandriie113dfd2016-10-11 10:20:12 -07004717 error_message = cl.CannotTriggerTryJobReason()
4718 if error_message:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004719 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004720
qyearsley1fdfcb62016-10-24 13:22:03 -07004721 buckets = _get_bucket_map(cl, options, parser)
Edward Lemurc8b67ed2019-09-12 20:28:58 +00004722 if buckets and any(b.startswith('master.') for b in buckets):
4723 print('ERROR: Buildbot masters are not supported.')
4724 return 1
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004725
qyearsleydd49f942016-10-28 11:57:22 -07004726 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4727 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004728 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004729 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07004730 print('git cl try with no bots now defaults to CQ dry run.')
4731 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
4732 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00004733
borenet6c0efe62016-10-19 08:13:29 -07004734 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004735 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004736 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004737 'of bot requires an initial job from a parent (usually a builder). '
4738 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004739 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004740 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004741
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004742 patchset = cl.GetMostRecentPatchset()
Edward Lemur2c210a42019-09-16 23:58:35 +00004743 try:
4744 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
4745 except BuildbucketResponseException as ex:
4746 print('ERROR: %s' % ex)
4747 return 1
4748 return 0
maruel@chromium.org15192402012-09-06 12:38:29 +00004749
4750
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004751@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004752def CMDtry_results(parser, args):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00004753 """Prints info about results for tryjobs associated with the current CL."""
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004754 group = optparse.OptionGroup(parser, 'Tryjob results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004755 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004756 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004757 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004758 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004759 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004760 '--color', action='store_true', default=setup_color.IS_TTY,
4761 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004762 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004763 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4764 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004765 group.add_option(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004766 '--json', help=('Path of JSON output file to write tryjob results to,'
Stefan Zager1306bd02017-06-22 19:26:46 -07004767 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004768 parser.add_option_group(group)
4769 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07004770 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004771 options, args = parser.parse_args(args)
Edward Lemurf38bc172019-09-03 21:02:13 +00004772 _process_codereview_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004773 if args:
4774 parser.error('Unrecognized args: %s' % ' '.join(args))
4775
4776 auth_config = auth.extract_auth_config_from_options(options)
Edward Lemur934836a2019-09-09 20:16:54 +00004777 cl = Changelist(issue=options.issue)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004778 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004779 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004780
tandrii221ab252016-10-06 08:12:04 -07004781 patchset = options.patchset
4782 if not patchset:
4783 patchset = cl.GetMostRecentPatchset()
4784 if not patchset:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004785 parser.error('Code review host doesn\'t know about issue %s. '
tandrii221ab252016-10-06 08:12:04 -07004786 'No access to issue or wrong issue number?\n'
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004787 'Either upload first, or pass --patchset explicitly.' %
tandrii221ab252016-10-06 08:12:04 -07004788 cl.GetIssue())
4789
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004790 try:
tandrii221ab252016-10-06 08:12:04 -07004791 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004792 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004793 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004794 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004795 if options.json:
4796 write_try_results_json(options.json, jobs)
4797 else:
4798 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004799 return 0
4800
4801
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004802@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004803@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004804def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004805 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004806 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004807 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004808 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004809
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004810 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004811 if args:
4812 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004813 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004814 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004815 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004816 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004817
4818 # Clear configured merge-base, if there is one.
4819 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004820 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004821 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004822 return 0
4823
4824
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004825@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00004826def CMDweb(parser, args):
4827 """Opens the current CL in the web browser."""
4828 _, args = parser.parse_args(args)
4829 if args:
4830 parser.error('Unrecognized args: %s' % ' '.join(args))
4831
4832 issue_url = Changelist().GetIssueURL()
4833 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07004834 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004835 return 1
4836
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004837 # Redirect I/O before invoking browser to hide its output. For example, this
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004838 # allows us to hide the "Created new window in existing browser session."
4839 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004840 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004841 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004842 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004843 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00004844 os.open(os.devnull, os.O_RDWR)
4845 try:
4846 webbrowser.open(issue_url)
4847 finally:
4848 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00004849 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00004850 return 0
4851
4852
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004853@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004854def CMDset_commit(parser, args):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004855 """Sets the commit bit to trigger the CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004856 parser.add_option('-d', '--dry-run', action='store_true',
4857 help='trigger in dry run mode')
4858 parser.add_option('-c', '--clear', action='store_true',
4859 help='stop CQ run, if any')
iannuccie53c9352016-08-17 14:40:40 -07004860 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004861 options, args = parser.parse_args(args)
Edward Lemurf38bc172019-09-03 21:02:13 +00004862 _process_codereview_select_options(parser, options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004863 if args:
4864 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004865 if options.dry_run and options.clear:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004866 parser.error('Only one of --dry-run and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004867
Edward Lemur934836a2019-09-09 20:16:54 +00004868 cl = Changelist(issue=options.issue)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004869 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07004870 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00004871 elif options.dry_run:
4872 state = _CQState.DRY_RUN
4873 else:
4874 state = _CQState.COMMIT
4875 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004876 parser.error('Must upload the issue first.')
tandrii9de9ec62016-07-13 03:01:59 -07004877 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004878 return 0
4879
4880
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004881@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00004882def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004883 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07004884 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004885 options, args = parser.parse_args(args)
Edward Lemurf38bc172019-09-03 21:02:13 +00004886 _process_codereview_select_options(parser, options)
groby@chromium.org411034a2013-02-26 15:12:01 +00004887 if args:
4888 parser.error('Unrecognized args: %s' % ' '.join(args))
Edward Lemur934836a2019-09-09 20:16:54 +00004889 cl = Changelist(issue=options.issue)
groby@chromium.org411034a2013-02-26 15:12:01 +00004890 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07004891 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004892 DieWithError('ERROR: No issue to close.')
groby@chromium.org411034a2013-02-26 15:12:01 +00004893 cl.CloseIssue()
4894 return 0
4895
4896
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004897@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004898def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00004899 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07004900 parser.add_option(
4901 '--stat',
4902 action='store_true',
4903 dest='stat',
4904 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004905 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004906 if args:
4907 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00004908
Edward Lemur934836a2019-09-09 20:16:54 +00004909 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004910 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004911 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00004912 if not issue:
4913 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004914
Aaron Gablea718c3e2017-08-28 17:47:28 -07004915 base = cl._GitGetBranchConfigValue('last-upload-hash')
4916 if not base:
4917 base = cl._GitGetBranchConfigValue('gerritsquashhash')
4918 if not base:
4919 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
4920 revision_info = detail['revisions'][detail['current_revision']]
4921 fetch_info = revision_info['fetch']['http']
4922 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
4923 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004924
Aaron Gablea718c3e2017-08-28 17:47:28 -07004925 cmd = ['git', 'diff']
4926 if options.stat:
4927 cmd.append('--stat')
4928 cmd.append(base)
4929 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00004930
4931 return 0
4932
4933
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004934@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004935def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07004936 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004937 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00004938 '--ignore-current',
4939 action='store_true',
4940 help='Ignore the CL\'s current reviewers and start from scratch.')
4941 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00004942 '--ignore-self',
4943 action='store_true',
4944 help='Do not consider CL\'s author as an owners.')
4945 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004946 '--no-color',
4947 action='store_true',
4948 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07004949 parser.add_option(
4950 '--batch',
4951 action='store_true',
4952 help='Do not run interactively, just suggest some')
Yang Guo6e269a02019-06-26 11:17:02 +00004953 # TODO: Consider moving this to another command, since other
4954 # git-cl owners commands deal with owners for a given CL.
4955 parser.add_option(
4956 '--show-all',
4957 action='store_true',
4958 help='Show all owners for a particular file')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004959 options, args = parser.parse_args(args)
4960
4961 author = RunGit(['config', 'user.email']).strip() or None
4962
Edward Lemur934836a2019-09-09 20:16:54 +00004963 cl = Changelist()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004964
Yang Guo6e269a02019-06-26 11:17:02 +00004965 if options.show_all:
4966 for arg in args:
4967 base_branch = cl.GetCommonAncestorWithUpstream()
4968 change = cl.GetChange(base_branch, None)
4969 database = owners.Database(change.RepositoryRoot(), file, os.path)
4970 database.load_data_needed_for([arg])
4971 print('Owners for %s:' % arg)
4972 for owner in sorted(database.all_possible_owners([arg], None)):
4973 print(' - %s' % owner)
4974 return 0
4975
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004976 if args:
4977 if len(args) > 1:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004978 parser.error('Unknown args.')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004979 base_branch = args[0]
4980 else:
4981 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004982 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004983
4984 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07004985 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
4986
4987 if options.batch:
4988 db = owners.Database(change.RepositoryRoot(), file, os.path)
4989 print('\n'.join(db.reviewers_for(affected_files, author)))
4990 return 0
4991
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00004992 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07004993 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02004994 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01004995 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00004996 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01004997 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02004998 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00004999 override_files=change.OriginalOwnersFiles(),
5000 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005001
5002
Aiden Bennerc08566e2018-10-03 17:52:42 +00005003def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005004 """Generates a diff command."""
5005 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005006 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5007
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005008 if allow_prefix:
5009 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5010 # case that diff.noprefix is set in the user's git config.
5011 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5012 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005013 diff_cmd += ['--no-prefix']
5014
5015 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005016
5017 if args:
5018 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005019 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005020 diff_cmd.append(arg)
5021 else:
5022 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005023
5024 return diff_cmd
5025
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005026
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005027def MatchingFileType(file_name, extensions):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005028 """Returns True if the file name ends with one of the given extensions."""
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005029 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005030
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005031
enne@chromium.org555cfe42014-01-29 18:21:39 +00005032@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005033@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005034def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005035 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005036 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005037 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005038 parser.add_option('--full', action='store_true',
5039 help='Reformat the full content of all touched files')
5040 parser.add_option('--dry-run', action='store_true',
5041 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005042 parser.add_option(
5043 '--python',
5044 action='store_true',
5045 default=None,
5046 help='Enables python formatting on all python files.')
5047 parser.add_option(
5048 '--no-python',
5049 action='store_true',
5050 dest='python',
5051 help='Disables python formatting on all python files. '
5052 'Takes precedence over --python. '
5053 'If neither --python or --no-python are set, python '
5054 'files that have a .style.yapf file in an ancestor '
5055 'directory will be formatted.')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005056 parser.add_option('--js', action='store_true',
5057 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005058 parser.add_option('--diff', action='store_true',
5059 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005060 parser.add_option('--presubmit', action='store_true',
5061 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005062 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005063
Daniel Chengc55eecf2016-12-30 03:11:02 -08005064 # Normalize any remaining args against the current path, so paths relative to
5065 # the current directory are still resolved as expected.
5066 args = [os.path.join(os.getcwd(), arg) for arg in args]
5067
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005068 # git diff generates paths against the root of the repository. Change
5069 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005070 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005071 if rel_base_path:
5072 os.chdir(rel_base_path)
5073
digit@chromium.org29e47272013-05-17 17:01:46 +00005074 # Grab the merge-base commit, i.e. the upstream commit of the current
5075 # branch when it was created or the last time it was rebased. This is
5076 # to cover the case where the user may have called "git fetch origin",
5077 # moving the origin branch to a newer commit, but hasn't rebased yet.
5078 upstream_commit = None
5079 cl = Changelist()
5080 upstream_branch = cl.GetUpstreamBranch()
5081 if upstream_branch:
5082 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5083 upstream_commit = upstream_commit.strip()
5084
5085 if not upstream_commit:
5086 DieWithError('Could not find base commit for this branch. '
5087 'Are you in detached state?')
5088
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005089 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5090 diff_output = RunGit(changed_files_cmd)
5091 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005092 # Filter out files deleted by this CL
5093 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005094
Christopher Lamc5ba6922017-01-24 11:19:14 +11005095 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005096 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005097
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005098 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5099 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5100 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005101 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005102
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005103 top_dir = os.path.normpath(
5104 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5105
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005106 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5107 # formatted. This is used to block during the presubmit.
5108 return_value = 0
5109
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005110 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005111 # Locate the clang-format binary in the checkout
5112 try:
5113 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005114 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005115 DieWithError(e)
5116
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005117 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005118 cmd = [clang_format_tool]
5119 if not opts.dry_run and not opts.diff:
5120 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005121 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005122 if opts.diff:
5123 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005124 else:
5125 env = os.environ.copy()
5126 env['PATH'] = str(os.path.dirname(clang_format_tool))
5127 try:
5128 script = clang_format.FindClangFormatScriptInChromiumTree(
5129 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005130 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005131 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005132
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005133 cmd = [sys.executable, script, '-p0']
5134 if not opts.dry_run and not opts.diff:
5135 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005136
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005137 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5138 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005139
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005140 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5141 if opts.diff:
5142 sys.stdout.write(stdout)
5143 if opts.dry_run and len(stdout) > 0:
5144 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005145
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005146 # Similar code to above, but using yapf on .py files rather than clang-format
5147 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005148 py_explicitly_disabled = opts.python is not None and not opts.python
5149 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005150 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5151 yapf_tool = os.path.join(depot_tools_path, 'yapf')
5152 if sys.platform.startswith('win'):
5153 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005154
Aiden Bennerc08566e2018-10-03 17:52:42 +00005155 # If we couldn't find a yapf file we'll default to the chromium style
5156 # specified in depot_tools.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005157 chromium_default_yapf_style = os.path.join(depot_tools_path,
5158 YAPF_CONFIG_FILENAME)
Aiden Bennerc08566e2018-10-03 17:52:42 +00005159 # Used for caching.
5160 yapf_configs = {}
5161 for f in python_diff_files:
5162 # Find the yapf style config for the current file, defaults to depot
5163 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005164 _FindYapfConfigFile(f, yapf_configs, top_dir)
5165
5166 # Turn on python formatting by default if a yapf config is specified.
5167 # This breaks in the case of this repo though since the specified
5168 # style file is also the global default.
5169 if opts.python is None:
5170 filtered_py_files = []
5171 for f in python_diff_files:
5172 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5173 filtered_py_files.append(f)
5174 else:
5175 filtered_py_files = python_diff_files
5176
5177 # Note: yapf still seems to fix indentation of the entire file
5178 # even if line ranges are specified.
5179 # See https://github.com/google/yapf/issues/499
5180 if not opts.full and filtered_py_files:
5181 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5182
5183 for f in filtered_py_files:
5184 yapf_config = _FindYapfConfigFile(f, yapf_configs, top_dir)
5185 if yapf_config is None:
5186 yapf_config = chromium_default_yapf_style
Aiden Bennerc08566e2018-10-03 17:52:42 +00005187
5188 cmd = [yapf_tool, '--style', yapf_config, f]
5189
5190 has_formattable_lines = False
5191 if not opts.full:
5192 # Only run yapf over changed line ranges.
5193 for diff_start, diff_len in py_line_diffs[f]:
5194 diff_end = diff_start + diff_len - 1
5195 # Yapf errors out if diff_end < diff_start but this
5196 # is a valid line range diff for a removal.
5197 if diff_end >= diff_start:
5198 has_formattable_lines = True
5199 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5200 # If all line diffs were removals we have nothing to format.
5201 if not has_formattable_lines:
5202 continue
5203
5204 if opts.diff or opts.dry_run:
5205 cmd += ['--diff']
5206 # Will return non-zero exit code if non-empty diff.
5207 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5208 if opts.diff:
5209 sys.stdout.write(stdout)
5210 elif len(stdout) > 0:
5211 return_value = 2
5212 else:
5213 cmd += ['-i']
5214 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005215
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005216 # Dart's formatter does not have the nice property of only operating on
5217 # modified chunks, so hard code full.
5218 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005219 try:
5220 command = [dart_format.FindDartFmtToolInChromiumTree()]
5221 if not opts.dry_run and not opts.diff:
5222 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005223 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005224
ppi@chromium.org6593d932016-03-03 15:41:15 +00005225 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005226 if opts.dry_run and stdout:
5227 return_value = 2
5228 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005229 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5230 'found in this checkout. Files in other languages are still '
5231 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005232
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005233 # Format GN build files. Always run on full build files for canonical form.
5234 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005235 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005236 if opts.dry_run or opts.diff:
5237 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005238 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005239 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5240 shell=sys.platform == 'win32',
5241 cwd=top_dir)
5242 if opts.dry_run and gn_ret == 2:
5243 return_value = 2 # Not formatted.
5244 elif opts.diff and gn_ret == 2:
5245 # TODO this should compute and print the actual diff.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005246 print('This change has GN build file diff for ' + gn_diff_file)
brettw4b8ed592016-08-05 16:19:12 -07005247 elif gn_ret != 0:
5248 # For non-dry run cases (and non-2 return values for dry-run), a
5249 # nonzero error code indicates a failure, probably because the file
5250 # doesn't parse.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005251 DieWithError('gn format failed on ' + gn_diff_file +
5252 '\nTry running `gn format` on this file manually.')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005253
Ilya Shermane081cbe2017-08-15 17:51:04 -07005254 # Skip the metrics formatting from the global presubmit hook. These files have
5255 # a separate presubmit hook that issues an error if the files need formatting,
5256 # whereas the top-level presubmit script merely issues a warning. Formatting
5257 # these files is somewhat slow, so it's important not to duplicate the work.
5258 if not opts.presubmit:
5259 for xml_dir in GetDirtyMetricsDirs(diff_files):
5260 tool_dir = os.path.join(top_dir, xml_dir)
5261 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5262 if opts.dry_run or opts.diff:
5263 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005264 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005265 if opts.diff:
5266 sys.stdout.write(stdout)
5267 if opts.dry_run and stdout:
5268 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005269
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005270 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005271
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005272
Steven Holte2e664bf2017-04-21 13:10:47 -07005273def GetDirtyMetricsDirs(diff_files):
5274 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5275 metrics_xml_dirs = [
5276 os.path.join('tools', 'metrics', 'actions'),
5277 os.path.join('tools', 'metrics', 'histograms'),
5278 os.path.join('tools', 'metrics', 'rappor'),
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005279 os.path.join('tools', 'metrics', 'ukm'),
5280 ]
Steven Holte2e664bf2017-04-21 13:10:47 -07005281 for xml_dir in metrics_xml_dirs:
5282 if any(file.startswith(xml_dir) for file in xml_diff_files):
5283 yield xml_dir
5284
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005285
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005286@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005287@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005288def CMDcheckout(parser, args):
Edward Lemurf38bc172019-09-03 21:02:13 +00005289 """Checks out a branch associated with a given Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005290 _, args = parser.parse_args(args)
5291
5292 if len(args) != 1:
5293 parser.print_help()
5294 return 1
5295
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005296 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005297 if not issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005298 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005299
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005300 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005301
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005302 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005303 output = RunGit(['config', '--local', '--get-regexp',
5304 r'branch\..*\.%s' % issueprefix],
5305 error_ok=True)
5306 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005307 if issue == target_issue:
5308 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005309
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005310 branches = []
5311 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005312 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005313 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005314 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005315 return 1
5316 if len(branches) == 1:
5317 RunGit(['checkout', branches[0]])
5318 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005319 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005320 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005321 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005322 which = raw_input('Choose by index: ')
5323 try:
5324 RunGit(['checkout', branches[int(which)]])
5325 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005326 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005327 return 1
5328
5329 return 0
5330
5331
maruel@chromium.org29404b52014-09-08 22:58:00 +00005332def CMDlol(parser, args):
5333 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005334 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005335 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5336 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5337 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005338 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005339 return 0
5340
5341
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005342class OptionParser(optparse.OptionParser):
5343 """Creates the option parse and add --verbose support."""
5344 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005345 optparse.OptionParser.__init__(
5346 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005347 self.add_option(
5348 '-v', '--verbose', action='count', default=0,
5349 help='Use 2 times for more debugging info')
5350
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005351 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005352 try:
5353 return self._parse_args(args)
5354 finally:
5355 # Regardless of success or failure of args parsing, we want to report
5356 # metrics, but only after logging has been initialized (if parsing
5357 # succeeded).
5358 global settings
5359 settings = Settings()
5360
5361 if not metrics.DISABLE_METRICS_COLLECTION:
5362 # GetViewVCUrl ultimately calls logging method.
5363 project_url = settings.GetViewVCUrl().strip('/+')
5364 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5365 metrics.collector.add('project_urls', [project_url])
5366
5367 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005368 # Create an optparse.Values object that will store only the actual passed
5369 # options, without the defaults.
5370 actual_options = optparse.Values()
5371 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5372 # Create an optparse.Values object with the default options.
5373 options = optparse.Values(self.get_default_values().__dict__)
5374 # Update it with the options passed by the user.
5375 options._update_careful(actual_options.__dict__)
5376 # Store the options passed by the user in an _actual_options attribute.
5377 # We store only the keys, and not the values, since the values can contain
5378 # arbitrary information, which might be PII.
5379 metrics.collector.add('arguments', actual_options.__dict__.keys())
5380
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005381 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005382 logging.basicConfig(
5383 level=levels[min(options.verbose, len(levels) - 1)],
5384 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5385 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005386
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005387 return options, args
5388
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005389
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005390def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005391 if sys.hexversion < 0x02060000:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005392 print('\nYour Python version %s is unsupported, please upgrade.\n' %
vapiera7fbd5a2016-06-16 09:17:49 -07005393 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005394 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005395
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005396 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005397 dispatcher = subcommand.CommandDispatcher(__name__)
5398 try:
5399 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005400 except auth.AuthenticationError as e:
5401 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005402 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005403 if e.code != 500:
5404 raise
5405 DieWithError(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005406 ('App Engine is misbehaving and returned HTTP %d, again. Keep faith '
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005407 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005408 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005409
5410
5411if __name__ == '__main__':
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005412 # These affect sys.stdout, so do it outside of main() to simplify mocks in
5413 # the unit tests.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005414 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005415 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005416 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005417 sys.exit(main(sys.argv[1:]))