blob: a52b0ae989688277dae6222aea8f81002fb06653 [file] [log] [blame]
James Darpinianf994d872019-08-06 18:57:40 +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
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010018import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000019import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010020import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000021import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000023import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import optparse
25import os
26import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010027import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000028import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070030import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000032import time
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000033import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000035import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000036import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000037import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000038import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000039
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000040from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000041from third_party import httplib2
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000042import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000043import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000044import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000045import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000046import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000047import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000048import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000049import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000050import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000051import git_footers
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000052import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000053import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
57import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040058import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
tandrii7400cf02016-06-21 08:48:07 -070063__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
Edward Lemur0f58ae42019-04-30 17:24:12 +000065# Traces for git push will be stored in a traces directory inside the
66# depot_tools checkout.
67DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
68TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
69
70# When collecting traces, Git hashes will be reduced to 6 characters to reduce
71# the size after compression.
72GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
73# Used to redact the cookies from the gitcookies file.
74GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
75
Edward Lemur1b52d872019-05-09 21:12:12 +000076# The maximum number of traces we will keep. Multiplied by 3 since we store
77# 3 files per trace.
78MAX_TRACES = 3 * 10
Edward Lemur5737f022019-05-17 01:24:00 +000079# Message to be displayed to the user to inform where to find the traces for a
80# git-cl upload execution.
Edward Lemur0f58ae42019-04-30 17:24:12 +000081TRACES_MESSAGE = (
Edward Lemur1b52d872019-05-09 21:12:12 +000082'\n'
Edward Lemur5737f022019-05-17 01:24:00 +000083'The traces of this git-cl execution have been recorded at:\n'
Edward Lemur1b52d872019-05-09 21:12:12 +000084' %(trace_name)s-traces.zip\n'
Edward Lemur5737f022019-05-17 01:24:00 +000085'Copies of your gitcookies file and git config have been recorded at:\n'
86' %(trace_name)s-git-info.zip\n')
Edward Lemur1b52d872019-05-09 21:12:12 +000087# Format of the message to be stored as part of the traces to give developers a
88# better context when they go through traces.
89TRACES_README_FORMAT = (
90'Date: %(now)s\n'
91'\n'
92'Change: https://%(gerrit_host)s/q/%(change_id)s\n'
93'Title: %(title)s\n'
94'\n'
95'%(description)s\n'
96'\n'
97'Execution time: %(execution_time)s\n'
98'Exit code: %(exit_code)s\n') + TRACES_MESSAGE
Edward Lemur0f58ae42019-04-30 17:24:12 +000099
tandrii9d2c7a32016-06-22 03:42:45 -0700100COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -0800101POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +0000103REFS_THAT_ALIAS_TO_OTHER_REFS = {
104 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
105 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
106}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000107
thestig@chromium.org44202a22014-03-11 19:22:18 +0000108# Valid extensions for files we want to lint.
109DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
110DEFAULT_LINT_IGNORE_REGEX = r"$^"
111
Aiden Bennerc08566e2018-10-03 17:52:42 +0000112# File name for yapf style config files.
113YAPF_CONFIG_FILENAME = '.style.yapf'
114
borenet6c0efe62016-10-19 08:13:29 -0700115# Buildbucket master name prefix.
116MASTER_PREFIX = 'master.'
117
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000118# Shortcut since it quickly becomes redundant.
119Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000120
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000121# Initialized in main()
122settings = None
123
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100124# Used by tests/git_cl_test.py to add extra logging.
125# Inside the weirdly failing test, add this:
126# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700127# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100128_IS_BEING_TESTED = False
129
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000130
Christopher Lamf732cd52017-01-24 12:40:11 +1100131def DieWithError(message, change_desc=None):
132 if change_desc:
133 SaveDescriptionBackup(change_desc)
134
vapiera7fbd5a2016-06-16 09:17:49 -0700135 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000136 sys.exit(1)
137
138
Christopher Lamf732cd52017-01-24 12:40:11 +1100139def SaveDescriptionBackup(change_desc):
140 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000141 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100142 backup_file = open(backup_path, 'w')
143 backup_file.write(change_desc.description)
144 backup_file.close()
145
146
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000147def GetNoGitPagerEnv():
148 env = os.environ.copy()
149 # 'cat' is a magical git string that disables pagers on all platforms.
150 env['GIT_PAGER'] = 'cat'
151 return env
152
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000153
bsep@chromium.org627d9002016-04-29 00:00:52 +0000154def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000155 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000156 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000157 except subprocess2.CalledProcessError as e:
158 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000159 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000160 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000161 'Command "%s" failed.\n%s' % (
162 ' '.join(args), error_message or e.stdout or ''))
163 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000164
165
166def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000167 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000168 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000169
170
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000171def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000172 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700173 if suppress_stderr:
174 stderr = subprocess2.VOID
175 else:
176 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000177 try:
tandrii5d48c322016-08-18 16:19:37 -0700178 (out, _), code = subprocess2.communicate(['git'] + args,
179 env=GetNoGitPagerEnv(),
180 stdout=subprocess2.PIPE,
181 stderr=stderr)
182 return code, out
183 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900184 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700185 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000186
187
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000188def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000189 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000190 return RunGitWithCode(args, suppress_stderr=True)[1]
191
192
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000193def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000194 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000195 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000196 return (version.startswith(prefix) and
197 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000198
199
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000200def BranchExists(branch):
201 """Return True if specified branch exists."""
202 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
203 suppress_stderr=True)
204 return not code
205
206
tandrii2a16b952016-10-19 07:09:44 -0700207def time_sleep(seconds):
208 # Use this so that it can be mocked in tests without interfering with python
209 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700210 return time.sleep(seconds)
211
212
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000213def time_time():
214 # Use this so that it can be mocked in tests without interfering with python
215 # system machinery.
216 return time.time()
217
218
Edward Lemur1b52d872019-05-09 21:12:12 +0000219def datetime_now():
220 # Use this so that it can be mocked in tests without interfering with python
221 # system machinery.
222 return datetime.datetime.now()
223
224
maruel@chromium.org90541732011-04-01 17:54:18 +0000225def ask_for_data(prompt):
226 try:
227 return raw_input(prompt)
228 except KeyboardInterrupt:
229 # Hide the exception.
230 sys.exit(1)
231
232
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100233def confirm_or_exit(prefix='', action='confirm'):
234 """Asks user to press enter to continue or press Ctrl+C to abort."""
235 if not prefix or prefix.endswith('\n'):
236 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100237 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100238 mid = ' Press'
239 elif prefix.endswith(' '):
240 mid = 'press'
241 else:
242 mid = ' press'
243 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
244
245
246def ask_for_explicit_yes(prompt):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000247 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100248 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
249 while True:
250 if 'yes'.startswith(result):
251 return True
252 if 'no'.startswith(result):
253 return False
254 result = ask_for_data('Please, type yes or no: ').lower()
255
256
tandrii5d48c322016-08-18 16:19:37 -0700257def _git_branch_config_key(branch, key):
258 """Helper method to return Git config key for a branch."""
259 assert branch, 'branch name is required to set git config for it'
260 return 'branch.%s.%s' % (branch, key)
261
262
263def _git_get_branch_config_value(key, default=None, value_type=str,
264 branch=False):
265 """Returns git config value of given or current branch if any.
266
267 Returns default in all other cases.
268 """
269 assert value_type in (int, str, bool)
270 if branch is False: # Distinguishing default arg value from None.
271 branch = GetCurrentBranch()
272
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000273 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700274 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000275
tandrii5d48c322016-08-18 16:19:37 -0700276 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700277 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700278 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700279 # git config also has --int, but apparently git config suffers from integer
280 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700281 args.append(_git_branch_config_key(branch, key))
282 code, out = RunGitWithCode(args)
283 if code == 0:
284 value = out.strip()
285 if value_type == int:
286 return int(value)
287 if value_type == bool:
288 return bool(value.lower() == 'true')
289 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000290 return default
291
292
tandrii5d48c322016-08-18 16:19:37 -0700293def _git_set_branch_config_value(key, value, branch=None, **kwargs):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000294 """Sets or unsets the git branch config value.
tandrii5d48c322016-08-18 16:19:37 -0700295
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000296 If value is None, the key will be unset, otherwise it will be set.
297 If no branch is given, the currently checked out branch is used.
tandrii5d48c322016-08-18 16:19:37 -0700298 """
299 if not branch:
300 branch = GetCurrentBranch()
301 assert branch, 'a branch name OR currently checked out branch is required'
302 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700303 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700304 if value is None:
305 args.append('--unset')
306 elif isinstance(value, bool):
307 args.append('--bool')
308 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700309 else:
tandrii33a46ff2016-08-23 05:53:40 -0700310 # git config also has --int, but apparently git config suffers from integer
311 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700312 value = str(value)
313 args.append(_git_branch_config_key(branch, key))
314 if value is not None:
315 args.append(value)
316 RunGit(args, **kwargs)
317
318
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100319def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700320 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100321
322 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
323 """
324 # Git also stores timezone offset, but it only affects visual display,
325 # actual point in time is defined by this timestamp only.
326 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
327
328
329def _git_amend_head(message, committer_timestamp):
330 """Amends commit with new message and desired committer_timestamp.
331
332 Sets committer timezone to UTC.
333 """
334 env = os.environ.copy()
335 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
336 return RunGit(['commit', '--amend', '-m', message], env=env)
337
338
machenbach@chromium.org45453142015-09-15 08:45:22 +0000339def _get_properties_from_options(options):
340 properties = dict(x.split('=', 1) for x in options.properties)
341 for key, val in properties.iteritems():
342 try:
343 properties[key] = json.loads(val)
344 except ValueError:
345 pass # If a value couldn't be evaluated, treat it as a string.
346 return properties
347
348
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000349def _prefix_master(master):
350 """Convert user-specified master name to full master name.
351
352 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
353 name, while the developers always use shortened master name
354 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
355 function does the conversion for buildbucket migration.
356 """
borenet6c0efe62016-10-19 08:13:29 -0700357 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000358 return master
borenet6c0efe62016-10-19 08:13:29 -0700359 return '%s%s' % (MASTER_PREFIX, master)
360
361
362def _unprefix_master(bucket):
363 """Convert bucket name to shortened master name.
364
365 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
366 name, while the developers always use shortened master name
367 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
368 function does the conversion for buildbucket migration.
369 """
370 if bucket.startswith(MASTER_PREFIX):
371 return bucket[len(MASTER_PREFIX):]
372 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000373
374
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000375def _buildbucket_retry(operation_name, http, *args, **kwargs):
376 """Retries requests to buildbucket service and returns parsed json content."""
377 try_count = 0
378 while True:
379 response, content = http.request(*args, **kwargs)
380 try:
381 content_json = json.loads(content)
382 except ValueError:
383 content_json = None
384
385 # Buildbucket could return an error even if status==200.
386 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000387 error = content_json.get('error')
388 if error.get('code') == 403:
389 raise BuildbucketResponseException(
390 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000391 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000392 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000393 raise BuildbucketResponseException(msg)
394
395 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700396 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000397 raise BuildbucketResponseException(
398 'Buildbucket returns invalid json content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700399 'Please file bugs at http://crbug.com, '
400 'component "Infra>Platform>BuildBucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000401 content)
402 return content_json
403 if response.status < 500 or try_count >= 2:
404 raise httplib2.HttpLib2Error(content)
405
406 # status >= 500 means transient failures.
407 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700408 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000409 try_count += 1
410 assert False, 'unreachable'
411
412
qyearsley1fdfcb62016-10-24 13:22:03 -0700413def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700414 """Returns a dict mapping bucket names to builders and tests,
415 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700416 """
qyearsleydd49f942016-10-28 11:57:22 -0700417 # If no bots are listed, we try to get a set of builders and tests based
418 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700419 if not options.bot:
420 change = changelist.GetChange(
421 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700422 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700423 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700424 change=change,
425 changed_files=change.LocalPaths(),
426 repository_root=settings.GetRoot(),
427 default_presubmit=None,
428 project=None,
429 verbose=options.verbose,
430 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700431 if masters is None:
432 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100433 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700434
qyearsley1fdfcb62016-10-24 13:22:03 -0700435 if options.bucket:
436 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700437 if options.master:
438 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700439
qyearsleydd49f942016-10-28 11:57:22 -0700440 # If bots are listed but no master or bucket, then we need to find out
441 # the corresponding master for each bot.
442 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
443 if error_message:
444 option_parser.error(
445 'Tryserver master cannot be found because: %s\n'
446 'Please manually specify the tryserver master, e.g. '
447 '"-m tryserver.chromium.linux".' % error_message)
448 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700449
450
qyearsley123a4682016-10-26 09:12:17 -0700451def _get_bucket_map_for_builders(builders):
452 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700453 map_url = 'https://builders-map.appspot.com/'
454 try:
qyearsley123a4682016-10-26 09:12:17 -0700455 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700456 except urllib2.URLError as e:
457 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
458 (map_url, e))
459 except ValueError as e:
460 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700461 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700462 return None, 'Failed to build master map.'
463
qyearsley123a4682016-10-26 09:12:17 -0700464 bucket_map = {}
465 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800466 bucket = builders_map.get(builder, {}).get('bucket')
467 if bucket:
468 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700469 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700470
471
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800472def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700473 """Sends a request to Buildbucket to trigger try jobs for a changelist.
474
475 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700476 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700477 changelist: Changelist that the try jobs are associated with.
478 buckets: A nested dict mapping bucket names to builders to tests.
479 options: Command-line options.
480 """
tandriide281ae2016-10-12 06:02:30 -0700481 assert changelist.GetIssue(), 'CL must be uploaded first'
482 codereview_url = changelist.GetCodereviewServer()
483 assert codereview_url, 'CL must be uploaded first'
484 patchset = patchset or changelist.GetMostRecentPatchset()
485 assert patchset, 'CL must be uploaded first'
486
487 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700488 # Cache the buildbucket credentials under the codereview host key, so that
489 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700490 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000491 http = authenticator.authorize(httplib2.Http())
492 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700493
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000494 buildbucket_put_url = (
495 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000496 hostname=options.buildbucket_host))
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000497 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandriide281ae2016-10-12 06:02:30 -0700498 hostname=codereview_host,
499 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000500 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700501
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700502 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800503 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700504 if options.clobber:
505 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700506 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700507 if extra_properties:
508 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000509
510 batch_req_body = {'builds': []}
511 print_text = []
512 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700513 for bucket, builders_and_tests in sorted(buckets.iteritems()):
514 print_text.append('Bucket: %s' % bucket)
515 master = None
516 if bucket.startswith(MASTER_PREFIX):
517 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000518 for builder, tests in sorted(builders_and_tests.iteritems()):
519 print_text.append(' %s: %s' % (builder, tests))
520 parameters = {
521 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000522 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100523 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000524 'revision': options.revision,
525 }],
tandrii8c5a3532016-11-04 07:52:02 -0700526 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000527 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000528 if 'presubmit' in builder.lower():
529 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000530 if tests:
531 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700532
533 tags = [
534 'builder:%s' % builder,
535 'buildset:%s' % buildset,
536 'user_agent:git_cl_try',
537 ]
538 if master:
539 parameters['properties']['master'] = master
540 tags.append('master:%s' % master)
541
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000542 batch_req_body['builds'].append(
543 {
544 'bucket': bucket,
545 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000546 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700547 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000548 }
549 )
550
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000551 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700552 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000553 http,
554 buildbucket_put_url,
555 'PUT',
556 body=json.dumps(batch_req_body),
557 headers={'Content-Type': 'application/json'}
558 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000559 print_text.append('To see results here, run: git cl try-results')
560 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700561 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000562
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000563
tandrii221ab252016-10-06 08:12:04 -0700564def fetch_try_jobs(auth_config, changelist, buildbucket_host,
565 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700566 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000567
qyearsley53f48a12016-09-01 10:45:13 -0700568 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000569 """
tandrii221ab252016-10-06 08:12:04 -0700570 assert buildbucket_host
571 assert changelist.GetIssue(), 'CL must be uploaded first'
572 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
573 patchset = patchset or changelist.GetMostRecentPatchset()
574 assert patchset, 'CL must be uploaded first'
575
576 codereview_url = changelist.GetCodereviewServer()
577 codereview_host = urlparse.urlparse(codereview_url).hostname
578 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000579 if authenticator.has_cached_credentials():
580 http = authenticator.authorize(httplib2.Http())
581 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700582 print('Warning: Some results might be missing because %s' %
583 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700584 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000585 http = httplib2.Http()
586
587 http.force_exception_to_status_code = True
588
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000589 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandrii221ab252016-10-06 08:12:04 -0700590 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000591 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700592 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000593 params = {'tag': 'buildset:%s' % buildset}
594
595 builds = {}
596 while True:
597 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700598 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000599 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700600 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000601 for build in content.get('builds', []):
602 builds[build['id']] = build
603 if 'next_cursor' in content:
604 params['start_cursor'] = content['next_cursor']
605 else:
606 break
607 return builds
608
609
qyearsleyeab3c042016-08-24 09:18:28 -0700610def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000611 """Prints nicely result of fetch_try_jobs."""
612 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700613 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000614 return
615
616 # Make a copy, because we'll be modifying builds dictionary.
617 builds = builds.copy()
618 builder_names_cache = {}
619
620 def get_builder(b):
621 try:
622 return builder_names_cache[b['id']]
623 except KeyError:
624 try:
625 parameters = json.loads(b['parameters_json'])
626 name = parameters['builder_name']
627 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700628 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700629 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000630 name = None
631 builder_names_cache[b['id']] = name
632 return name
633
634 def get_bucket(b):
635 bucket = b['bucket']
636 if bucket.startswith('master.'):
637 return bucket[len('master.'):]
638 return bucket
639
640 if options.print_master:
641 name_fmt = '%%-%ds %%-%ds' % (
642 max(len(str(get_bucket(b))) for b in builds.itervalues()),
643 max(len(str(get_builder(b))) for b in builds.itervalues()))
644 def get_name(b):
645 return name_fmt % (get_bucket(b), get_builder(b))
646 else:
647 name_fmt = '%%-%ds' % (
648 max(len(str(get_builder(b))) for b in builds.itervalues()))
649 def get_name(b):
650 return name_fmt % get_builder(b)
651
652 def sort_key(b):
653 return b['status'], b.get('result'), get_name(b), b.get('url')
654
655 def pop(title, f, color=None, **kwargs):
656 """Pop matching builds from `builds` dict and print them."""
657
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000658 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000659 colorize = str
660 else:
661 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
662
663 result = []
664 for b in builds.values():
665 if all(b.get(k) == v for k, v in kwargs.iteritems()):
666 builds.pop(b['id'])
667 result.append(b)
668 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700669 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000670 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700671 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000672
673 total = len(builds)
674 pop(status='COMPLETED', result='SUCCESS',
675 title='Successes:', color=Fore.GREEN,
676 f=lambda b: (get_name(b), b.get('url')))
677 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
678 title='Infra Failures:', color=Fore.MAGENTA,
679 f=lambda b: (get_name(b), b.get('url')))
680 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
681 title='Failures:', color=Fore.RED,
682 f=lambda b: (get_name(b), b.get('url')))
683 pop(status='COMPLETED', result='CANCELED',
684 title='Canceled:', color=Fore.MAGENTA,
685 f=lambda b: (get_name(b),))
686 pop(status='COMPLETED', result='FAILURE',
687 failure_reason='INVALID_BUILD_DEFINITION',
688 title='Wrong master/builder name:', color=Fore.MAGENTA,
689 f=lambda b: (get_name(b),))
690 pop(status='COMPLETED', result='FAILURE',
691 title='Other failures:',
692 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
693 pop(status='COMPLETED',
694 title='Other finished:',
695 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
696 pop(status='STARTED',
697 title='Started:', color=Fore.YELLOW,
698 f=lambda b: (get_name(b), b.get('url')))
699 pop(status='SCHEDULED',
700 title='Scheduled:',
701 f=lambda b: (get_name(b), 'id=%s' % b['id']))
702 # The last section is just in case buildbucket API changes OR there is a bug.
703 pop(title='Other:',
704 f=lambda b: (get_name(b), 'id=%s' % b['id']))
705 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700706 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000707
708
Aiden Bennerc08566e2018-10-03 17:52:42 +0000709def _ComputeDiffLineRanges(files, upstream_commit):
710 """Gets the changed line ranges for each file since upstream_commit.
711
712 Parses a git diff on provided files and returns a dict that maps a file name
713 to an ordered list of range tuples in the form (start_line, count).
714 Ranges are in the same format as a git diff.
715 """
716 # If files is empty then diff_output will be a full diff.
717 if len(files) == 0:
718 return {}
719
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000720 # Take the git diff and find the line ranges where there are changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000721 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
722 diff_output = RunGit(diff_cmd)
723
724 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
725 # 2 capture groups
726 # 0 == fname of diff file
727 # 1 == 'diff_start,diff_count' or 'diff_start'
728 # will match each of
729 # diff --git a/foo.foo b/foo.py
730 # @@ -12,2 +14,3 @@
731 # @@ -12,2 +17 @@
732 # running re.findall on the above string with pattern will give
733 # [('foo.py', ''), ('', '14,3'), ('', '17')]
734
735 curr_file = None
736 line_diffs = {}
737 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
738 if match[0] != '':
739 # Will match the second filename in diff --git a/a.py b/b.py.
740 curr_file = match[0]
741 line_diffs[curr_file] = []
742 else:
743 # Matches +14,3
744 if ',' in match[1]:
745 diff_start, diff_count = match[1].split(',')
746 else:
747 # Single line changes are of the form +12 instead of +12,1.
748 diff_start = match[1]
749 diff_count = 1
750
751 diff_start = int(diff_start)
752 diff_count = int(diff_count)
753
754 # If diff_count == 0 this is a removal we can ignore.
755 line_diffs[curr_file].append((diff_start, diff_count))
756
757 return line_diffs
758
759
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000760def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000761 """Checks if a yapf file is in any parent directory of fpath until top_dir.
762
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000763 Recursively checks parent directories to find yapf file and if no yapf file
764 is found returns None. Uses yapf_config_cache as a cache for
765 previously found configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000766 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000767 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000768 # Return result if we've already computed it.
769 if fpath in yapf_config_cache:
770 return yapf_config_cache[fpath]
771
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000772 parent_dir = os.path.dirname(fpath)
773 if os.path.isfile(fpath):
774 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000775 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000776 # Otherwise fpath is a directory
777 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
778 if os.path.isfile(yapf_file):
779 ret = yapf_file
780 elif fpath == top_dir or parent_dir == fpath:
781 # If we're at the top level directory, or if we're at root
782 # there is no provided style.
783 ret = None
784 else:
785 # Otherwise recurse on the current directory.
786 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000787 yapf_config_cache[fpath] = ret
788 return ret
789
790
qyearsley53f48a12016-09-01 10:45:13 -0700791def write_try_results_json(output_file, builds):
792 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
793
794 The input |builds| dict is assumed to be generated by Buildbucket.
795 Buildbucket documentation: http://goo.gl/G0s101
796 """
797
798 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800799 """Extracts some of the information from one build dict."""
800 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700801 return {
802 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700803 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800804 'builder_name': parameters.get('builder_name'),
805 'created_ts': build.get('created_ts'),
806 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700807 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800808 'result': build.get('result'),
809 'status': build.get('status'),
810 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700811 'url': build.get('url'),
812 }
813
814 converted = []
815 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000816 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 10:45:13 -0700817 write_json(output_file, converted)
818
819
Aaron Gable13101a62018-02-09 13:20:41 -0800820def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000821 """Prints statistics about the change to the user."""
822 # --no-ext-diff is broken in some versions of Git, so try to work around
823 # this by overriding the environment (but there is still a problem if the
824 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000825 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000826 if 'GIT_EXTERNAL_DIFF' in env:
827 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000828
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000829 try:
830 stdout = sys.stdout.fileno()
831 except AttributeError:
832 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000833 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800834 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000835 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000836
837
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000838class BuildbucketResponseException(Exception):
839 pass
840
841
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000842class Settings(object):
843 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000844 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000845 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000846 self.tree_status_url = None
847 self.viewvc_url = None
848 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000849 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000850 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000851 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000852 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000853
854 def LazyUpdateIfNeeded(self):
855 """Updates the settings from a codereview.settings file, if available."""
856 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000857 # The only value that actually changes the behavior is
858 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000859 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000860 error_ok=True
861 ).strip().lower()
862
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000863 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000864 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000865 LoadCodereviewSettingsFromFile(cr_settings_file)
866 self.updated = True
867
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000868 @staticmethod
869 def GetRelativeRoot():
870 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000871
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000872 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000873 if self.root is None:
874 self.root = os.path.abspath(self.GetRelativeRoot())
875 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000876
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000877 def GetTreeStatusUrl(self, error_ok=False):
878 if not self.tree_status_url:
879 error_message = ('You must configure your tree status URL by running '
880 '"git cl config".')
Edward Lemur61ea3072018-12-01 00:34:36 +0000881 self.tree_status_url = self._GetConfig(
882 'rietveld.tree-status-url', error_ok=error_ok,
883 error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000884 return self.tree_status_url
885
886 def GetViewVCUrl(self):
887 if not self.viewvc_url:
Edward Lemur61ea3072018-12-01 00:34:36 +0000888 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000889 return self.viewvc_url
890
rmistry@google.com90752582014-01-14 21:04:50 +0000891 def GetBugPrefix(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000892 return self._GetConfig('rietveld.bug-prefix', error_ok=True)
rmistry@google.com78948ed2015-07-08 23:09:57 +0000893
rmistry@google.com5626a922015-02-26 14:03:30 +0000894 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000895 run_post_upload_hook = self._GetConfig(
896 'rietveld.run-post-upload-hook', error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +0000897 return run_post_upload_hook == "True"
898
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000899 def GetDefaultCCList(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000900 return self._GetConfig('rietveld.cc', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000901
ukai@chromium.orge8077812012-02-03 03:41:46 +0000902 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700903 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000904 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700905 self.is_gerrit = (
906 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000907 return self.is_gerrit
908
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000909 def GetSquashGerritUploads(self):
910 """Return true if uploads to Gerrit should be squashed by default."""
911 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700912 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
913 if self.squash_gerrit_uploads is None:
914 # Default is squash now (http://crbug.com/611892#c23).
915 self.squash_gerrit_uploads = not (
916 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
917 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000918 return self.squash_gerrit_uploads
919
tandriia60502f2016-06-20 02:01:53 -0700920 def GetSquashGerritUploadsOverride(self):
921 """Return True or False if codereview.settings should be overridden.
922
923 Returns None if no override has been defined.
924 """
925 # See also http://crbug.com/611892#c23
926 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
927 error_ok=True).strip()
928 if result == 'true':
929 return True
930 if result == 'false':
931 return False
932 return None
933
tandrii@chromium.org28253532016-04-14 13:46:56 +0000934 def GetGerritSkipEnsureAuthenticated(self):
935 """Return True if EnsureAuthenticated should not be done for Gerrit
936 uploads."""
937 if self.gerrit_skip_ensure_authenticated is None:
938 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000939 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000940 error_ok=True).strip() == 'true')
941 return self.gerrit_skip_ensure_authenticated
942
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000943 def GetGitEditor(self):
944 """Return the editor specified in the git config, or None if none is."""
945 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000946 # Git requires single quotes for paths with spaces. We need to replace
947 # them with double quotes for Windows to treat such paths as a single
948 # path.
949 self.git_editor = self._GetConfig(
950 'core.editor', error_ok=True).replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000951 return self.git_editor or None
952
thestig@chromium.org44202a22014-03-11 19:22:18 +0000953 def GetLintRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000954 return (self._GetConfig('rietveld.cpplint-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000955 DEFAULT_LINT_REGEX)
956
957 def GetLintIgnoreRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000958 return (self._GetConfig('rietveld.cpplint-ignore-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000959 DEFAULT_LINT_IGNORE_REGEX)
960
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000961 def _GetConfig(self, param, **kwargs):
962 self.LazyUpdateIfNeeded()
963 return RunGit(['config', param], **kwargs).strip()
964
965
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100966@contextlib.contextmanager
967def _get_gerrit_project_config_file(remote_url):
968 """Context manager to fetch and store Gerrit's project.config from
969 refs/meta/config branch and store it in temp file.
970
971 Provides a temporary filename or None if there was error.
972 """
973 error, _ = RunGitWithCode([
974 'fetch', remote_url,
975 '+refs/meta/config:refs/git_cl/meta/config'])
976 if error:
977 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700978 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100979 (remote_url, error))
980 yield None
981 return
982
983 error, project_config_data = RunGitWithCode(
984 ['show', 'refs/git_cl/meta/config:project.config'])
985 if error:
986 print('WARNING: project.config file not found')
987 yield None
988 return
989
990 with gclient_utils.temporary_directory() as tempdir:
991 project_config_file = os.path.join(tempdir, 'project.config')
992 gclient_utils.FileWrite(project_config_file, project_config_data)
993 yield project_config_file
994
995
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000996def ShortBranchName(branch):
997 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000998 return branch.replace('refs/heads/', '', 1)
999
1000
1001def GetCurrentBranchRef():
1002 """Returns branch ref (e.g., refs/heads/master) or None."""
1003 return RunGit(['symbolic-ref', 'HEAD'],
1004 stderr=subprocess2.VOID, error_ok=True).strip() or None
1005
1006
1007def GetCurrentBranch():
1008 """Returns current branch or None.
1009
1010 For refs/heads/* branches, returns just last part. For others, full ref.
1011 """
1012 branchref = GetCurrentBranchRef()
1013 if branchref:
1014 return ShortBranchName(branchref)
1015 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001016
1017
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001018class _CQState(object):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00001019 """Enum for states of CL with respect to CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001020 NONE = 'none'
1021 DRY_RUN = 'dry_run'
1022 COMMIT = 'commit'
1023
1024 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1025
1026
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001027class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001028 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001029 self.issue = issue
1030 self.patchset = patchset
1031 self.hostname = hostname
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +00001032 assert codereview in (None, 'gerrit', 'rietveld')
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001033 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001034
1035 @property
1036 def valid(self):
1037 return self.issue is not None
1038
1039
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001040def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001041 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1042 fail_result = _ParsedIssueNumberArgument()
1043
1044 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001045 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001046 if not arg.startswith('http'):
1047 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001048
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001049 url = gclient_utils.UpgradeToHttps(arg)
1050 try:
1051 parsed_url = urlparse.urlparse(url)
1052 except ValueError:
1053 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001054
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001055 if codereview is not None:
1056 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1057 return parsed or fail_result
1058
Andrii Shyshkalov0a264d82018-11-21 00:36:16 +00001059 return _GerritChangelistImpl.ParseIssueURL(parsed_url) or fail_result
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001060
1061
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001062def _create_description_from_log(args):
1063 """Pulls out the commit log to use as a base for the CL description."""
1064 log_args = []
1065 if len(args) == 1 and not args[0].endswith('.'):
1066 log_args = [args[0] + '..']
1067 elif len(args) == 1 and args[0].endswith('...'):
1068 log_args = [args[0][:-1]]
1069 elif len(args) == 2:
1070 log_args = [args[0] + '..' + args[1]]
1071 else:
1072 log_args = args[:] # Hope for the best!
1073 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
1074
1075
Aaron Gablea45ee112016-11-22 15:14:38 -08001076class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001077 def __init__(self, issue, url):
1078 self.issue = issue
1079 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001080 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001081
1082 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001083 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001084 self.issue, self.url)
1085
1086
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001087_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001088 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001089 # TODO(tandrii): these two aren't known in Gerrit.
1090 'approval', 'disapproval'])
1091
1092
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001093class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001094 """Changelist works with one changelist in local branch.
1095
1096 Supports two codereview backends: Rietveld or Gerrit, selected at object
1097 creation.
1098
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001099 Notes:
1100 * Not safe for concurrent multi-{thread,process} use.
1101 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001102 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001103 """
1104
1105 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1106 """Create a new ChangeList instance.
1107
1108 If issue is given, the codereview must be given too.
1109
1110 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1111 Otherwise, it's decided based on current configuration of the local branch,
1112 with default being 'rietveld' for backwards compatibility.
1113 See _load_codereview_impl for more details.
1114
1115 **kwargs will be passed directly to codereview implementation.
1116 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001117 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001118 global settings
1119 if not settings:
1120 # Happens when git_cl.py is used as a utility library.
1121 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001122
1123 if issue:
1124 assert codereview, 'codereview must be known, if issue is known'
1125
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001126 self.branchref = branchref
1127 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001128 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129 self.branch = ShortBranchName(self.branchref)
1130 else:
1131 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001133 self.lookedup_issue = False
1134 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001135 self.has_description = False
1136 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001137 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001139 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001140 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001141 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001142 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001143
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001144 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001145 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001146 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001147 assert self._codereview_impl
1148 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001149
1150 def _load_codereview_impl(self, codereview=None, **kwargs):
1151 if codereview:
Joe Masond87b0962018-12-03 21:04:46 +00001152 assert codereview in _CODEREVIEW_IMPLEMENTATIONS, (
1153 'codereview {} not in {}'.format(codereview,
1154 _CODEREVIEW_IMPLEMENTATIONS))
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001155 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1156 self._codereview = codereview
1157 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001158 return
1159
1160 # Automatic selection based on issue number set for a current branch.
1161 # Rietveld takes precedence over Gerrit.
1162 assert not self.issue
1163 # Whether we find issue or not, we are doing the lookup.
1164 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001165 if self.GetBranch():
1166 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1167 issue = _git_get_branch_config_value(
1168 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1169 if issue:
1170 self._codereview = codereview
1171 self._codereview_impl = cls(self, **kwargs)
1172 self.issue = int(issue)
1173 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001174
Bryce Thomascfc97122018-12-13 20:21:47 +00001175 # No issue is set for this branch, so default to gerrit.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001176 return self._load_codereview_impl(
Bryce Thomascfc97122018-12-13 20:21:47 +00001177 codereview='gerrit',
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001178 **kwargs)
1179
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001180 def IsGerrit(self):
1181 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001182
1183 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001184 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001185
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001186 The return value is a string suitable for passing to git cl with the --cc
1187 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001188 """
1189 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001190 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001191 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001192 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1193 return self.cc
1194
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001195 def GetCCListWithoutDefault(self):
1196 """Return the users cc'd on this CL excluding default ones."""
1197 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001198 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001199 return self.cc
1200
Daniel Cheng7227d212017-11-17 08:12:37 -08001201 def ExtendCC(self, more_cc):
1202 """Extends the list of users to cc on this CL based on the changed files."""
1203 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001204
1205 def GetBranch(self):
1206 """Returns the short branch name, e.g. 'master'."""
1207 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001208 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001209 if not branchref:
1210 return None
1211 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212 self.branch = ShortBranchName(self.branchref)
1213 return self.branch
1214
1215 def GetBranchRef(self):
1216 """Returns the full branch name, e.g. 'refs/heads/master'."""
1217 self.GetBranch() # Poke the lazy loader.
1218 return self.branchref
1219
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001220 def ClearBranch(self):
1221 """Clears cached branch data of this object."""
1222 self.branch = self.branchref = None
1223
tandrii5d48c322016-08-18 16:19:37 -07001224 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1225 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1226 kwargs['branch'] = self.GetBranch()
1227 return _git_get_branch_config_value(key, default, **kwargs)
1228
1229 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1230 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1231 assert self.GetBranch(), (
1232 'this CL must have an associated branch to %sset %s%s' %
1233 ('un' if value is None else '',
1234 key,
1235 '' if value is None else ' to %r' % value))
1236 kwargs['branch'] = self.GetBranch()
1237 return _git_set_branch_config_value(key, value, **kwargs)
1238
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001239 @staticmethod
1240 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001241 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001242 e.g. 'origin', 'refs/heads/master'
1243 """
1244 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001245 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1246
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001248 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001250 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1251 error_ok=True).strip()
1252 if upstream_branch:
1253 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001255 # Else, try to guess the origin remote.
1256 remote_branches = RunGit(['branch', '-r']).split()
1257 if 'origin/master' in remote_branches:
1258 # Fall back on origin/master if it exits.
1259 remote = 'origin'
1260 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001262 DieWithError(
1263 'Unable to determine default branch to diff against.\n'
1264 'Either pass complete "git diff"-style arguments, like\n'
1265 ' git cl upload origin/master\n'
1266 'or verify this branch is set up to track another \n'
1267 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001268
1269 return remote, upstream_branch
1270
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001271 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001272 upstream_branch = self.GetUpstreamBranch()
1273 if not BranchExists(upstream_branch):
1274 DieWithError('The upstream for the current branch (%s) does not exist '
1275 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001276 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001277 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001278
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279 def GetUpstreamBranch(self):
1280 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001281 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Raul Tambrefe1dbe12019-05-02 04:43:57 +00001282 if remote != '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001283 upstream_branch = upstream_branch.replace('refs/heads/',
1284 'refs/remotes/%s/' % remote)
1285 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1286 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001287 self.upstream_branch = upstream_branch
1288 return self.upstream_branch
1289
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001290 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001291 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001292 remote, branch = None, self.GetBranch()
1293 seen_branches = set()
1294 while branch not in seen_branches:
1295 seen_branches.add(branch)
1296 remote, branch = self.FetchUpstreamTuple(branch)
1297 branch = ShortBranchName(branch)
1298 if remote != '.' or branch.startswith('refs/remotes'):
1299 break
1300 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001301 remotes = RunGit(['remote'], error_ok=True).split()
1302 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001303 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001304 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001305 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001306 logging.warn('Could not determine which remote this change is '
1307 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001308 else:
1309 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001310 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001311 branch = 'HEAD'
1312 if branch.startswith('refs/remotes'):
1313 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001314 elif branch.startswith('refs/branch-heads/'):
1315 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001316 else:
1317 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001318 return self._remote
1319
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001320 def GitSanityChecks(self, upstream_git_obj):
1321 """Checks git repo status and ensures diff is from local commits."""
1322
sbc@chromium.org79706062015-01-14 21:18:12 +00001323 if upstream_git_obj is None:
1324 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001325 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001326 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001327 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001328 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001329 return False
1330
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001331 # Verify the commit we're diffing against is in our current branch.
1332 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1333 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1334 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001335 print('ERROR: %s is not in the current branch. You may need to rebase '
1336 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001337 return False
1338
1339 # List the commits inside the diff, and verify they are all local.
1340 commits_in_diff = RunGit(
1341 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1342 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1343 remote_branch = remote_branch.strip()
1344 if code != 0:
1345 _, remote_branch = self.GetRemoteBranch()
1346
1347 commits_in_remote = RunGit(
1348 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1349
1350 common_commits = set(commits_in_diff) & set(commits_in_remote)
1351 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001352 print('ERROR: Your diff contains %d commits already in %s.\n'
1353 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1354 'the diff. If you are using a custom git flow, you can override'
1355 ' the reference used for this check with "git config '
1356 'gitcl.remotebranch <git-ref>".' % (
1357 len(common_commits), remote_branch, upstream_git_obj),
1358 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001359 return False
1360 return True
1361
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001362 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001363 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001364
1365 Returns None if it is not set.
1366 """
tandrii5d48c322016-08-18 16:19:37 -07001367 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001368
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001369 def GetRemoteUrl(self):
1370 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1371
1372 Returns None if there is no remote.
1373 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001374 is_cached, value = self._cached_remote_url
1375 if is_cached:
1376 return value
1377
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001378 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001379 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1380
Edward Lemur298f2cf2019-02-22 21:40:39 +00001381 # Check if the remote url can be parsed as an URL.
1382 host = urlparse.urlparse(url).netloc
1383 if host:
1384 self._cached_remote_url = (True, url)
1385 return url
1386
1387 # If it cannot be parsed as an url, assume it is a local directory, probably
1388 # a git cache.
1389 logging.warning('"%s" doesn\'t appear to point to a git host. '
1390 'Interpreting it as a local directory.', url)
1391 if not os.path.isdir(url):
1392 logging.error(
1393 'Remote "%s" for branch "%s" points to "%s", but it doesn\'t exist.',
1394 remote, url, self.GetBranch())
1395 return None
1396
1397 cache_path = url
1398 url = RunGit(['config', 'remote.%s.url' % remote],
1399 error_ok=True,
1400 cwd=url).strip()
1401
1402 host = urlparse.urlparse(url).netloc
1403 if not host:
1404 logging.error(
1405 'Remote "%(remote)s" for branch "%(branch)s" points to '
1406 '"%(cache_path)s", but it is misconfigured.\n'
1407 '"%(cache_path)s" must be a git repo and must have a remote named '
1408 '"%(remote)s" pointing to the git host.', {
1409 'remote': remote,
1410 'cache_path': cache_path,
1411 'branch': self.GetBranch()})
1412 return None
1413
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001414 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001415 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001417 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001418 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001419 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001420 self.issue = self._GitGetBranchConfigValue(
1421 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001422 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001423 return self.issue
1424
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001425 def GetIssueURL(self):
1426 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001427 issue = self.GetIssue()
1428 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001429 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001430 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001432 def GetDescription(self, pretty=False, force=False):
1433 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001434 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001435 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001436 self.has_description = True
1437 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001438 # Set width to 72 columns + 2 space indent.
1439 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001441 lines = self.description.splitlines()
1442 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443 return self.description
1444
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001445 def GetDescriptionFooters(self):
1446 """Returns (non_footer_lines, footers) for the commit message.
1447
1448 Returns:
1449 non_footer_lines (list(str)) - Simple list of description lines without
1450 any footer. The lines do not contain newlines, nor does the list contain
1451 the empty line between the message and the footers.
1452 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1453 [("Change-Id", "Ideadbeef...."), ...]
1454 """
1455 raw_description = self.GetDescription()
1456 msg_lines, _, footers = git_footers.split_footers(raw_description)
1457 if footers:
1458 msg_lines = msg_lines[:len(msg_lines)-1]
1459 return msg_lines, footers
1460
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001461 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001462 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001463 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001464 self.patchset = self._GitGetBranchConfigValue(
1465 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001466 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001467 return self.patchset
1468
1469 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001470 """Set this branch's patchset. If patchset=0, clears the patchset."""
1471 assert self.GetBranch()
1472 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001473 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001474 else:
1475 self.patchset = int(patchset)
1476 self._GitSetBranchConfigValue(
1477 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001478
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001479 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001480 """Set this branch's issue. If issue isn't given, clears the issue."""
1481 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001482 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001483 issue = int(issue)
1484 self._GitSetBranchConfigValue(
1485 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001486 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001487 codereview_server = self._codereview_impl.GetCodereviewServer()
1488 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001489 self._GitSetBranchConfigValue(
1490 self._codereview_impl.CodereviewServerConfigKey(),
1491 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492 else:
tandrii5d48c322016-08-18 16:19:37 -07001493 # Reset all of these just to be clean.
1494 reset_suffixes = [
1495 'last-upload-hash',
1496 self._codereview_impl.IssueConfigKey(),
1497 self._codereview_impl.PatchsetConfigKey(),
1498 self._codereview_impl.CodereviewServerConfigKey(),
1499 ] + self._PostUnsetIssueProperties()
1500 for prop in reset_suffixes:
1501 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001502 msg = RunGit(['log', '-1', '--format=%B']).strip()
1503 if msg and git_footers.get_footer_change_id(msg):
1504 print('WARNING: The change patched into this branch has a Change-Id. '
1505 'Removing it.')
1506 RunGit(['commit', '--amend', '-m',
1507 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001508 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001509 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001510
dnjba1b0f32016-09-02 12:37:42 -07001511 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001512 if not self.GitSanityChecks(upstream_branch):
1513 DieWithError('\nGit sanity check failure')
1514
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001515 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001516 if not root:
1517 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001518 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001519
1520 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001521 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001522 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001523 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001524 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001525 except subprocess2.CalledProcessError:
1526 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001527 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001528 'This branch probably doesn\'t exist anymore. To reset the\n'
1529 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001530 ' git branch --set-upstream-to origin/master %s\n'
1531 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001532 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001533
maruel@chromium.org52424302012-08-29 15:14:30 +00001534 issue = self.GetIssue()
1535 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001536 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001537 description = self.GetDescription()
1538 else:
1539 # If the change was never uploaded, use the log messages of all commits
1540 # up to the branch point, as git cl upload will prefill the description
1541 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001542 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1543 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001544
1545 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001546 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001547 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001548 name,
1549 description,
1550 absroot,
1551 files,
1552 issue,
1553 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001554 author,
1555 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001556
dsansomee2d6fd92016-09-08 00:10:47 -07001557 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001558 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001559 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001560 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001561
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001562 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1563 """Sets the description for this CL remotely.
1564
1565 You can get description_lines and footers with GetDescriptionFooters.
1566
1567 Args:
1568 description_lines (list(str)) - List of CL description lines without
1569 newline characters.
1570 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1571 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1572 `List-Of-Tokens`). It will be case-normalized so that each token is
1573 title-cased.
1574 """
1575 new_description = '\n'.join(description_lines)
1576 if footers:
1577 new_description += '\n'
1578 for k, v in footers:
1579 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1580 if not git_footers.FOOTER_PATTERN.match(foot):
1581 raise ValueError('Invalid footer %r' % foot)
1582 new_description += foot + '\n'
1583 self.UpdateDescription(new_description, force)
1584
Edward Lesmes8e282792018-04-03 18:50:29 -04001585 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001586 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1587 try:
Edward Lemur2c48f242019-06-04 16:14:09 +00001588 start = time_time()
1589 result = presubmit_support.DoPresubmitChecks(change, committing,
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001590 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1591 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001592 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1593 parallel=parallel)
Edward Lemur2c48f242019-06-04 16:14:09 +00001594 metrics.collector.add_repeated('sub_commands', {
1595 'command': 'presubmit',
1596 'execution_time': time_time() - start,
1597 'exit_code': 0 if result.should_continue() else 1,
1598 })
1599 return result
vapierfd77ac72016-06-16 08:33:57 -07001600 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001601 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001602
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001603 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1604 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001605 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1606 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001607 else:
1608 # Assume url.
1609 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1610 urlparse.urlparse(issue_arg))
1611 if not parsed_issue_arg or not parsed_issue_arg.valid:
1612 DieWithError('Failed to parse issue argument "%s". '
1613 'Must be an issue number or a valid URL.' % issue_arg)
1614 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001615 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001616
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001617 def CMDUpload(self, options, git_diff_args, orig_args):
1618 """Uploads a change to codereview."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00001619 assert self.IsGerrit()
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001620 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001621 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001622 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001623 else:
1624 if self.GetBranch() is None:
1625 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1626
1627 # Default to diffing against common ancestor of upstream branch
1628 base_branch = self.GetCommonAncestorWithUpstream()
1629 git_diff_args = [base_branch, 'HEAD']
1630
Aaron Gablec4c40d12017-05-22 11:49:53 -07001631
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001632 # Fast best-effort checks to abort before running potentially
1633 # expensive hooks if uploading is likely to fail anyway. Passing these
1634 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001635 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001636 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001637
1638 # Apply watchlists on upload.
1639 change = self.GetChange(base_branch, None)
1640 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1641 files = [f.LocalPath() for f in change.AffectedFiles()]
1642 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001643 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001644
1645 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001646 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001647 # Set the reviewer list now so that presubmit checks can access it.
1648 change_description = ChangeDescription(change.FullDescriptionText())
1649 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001650 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001651 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001652 change)
1653 change.SetDescriptionText(change_description.description)
1654 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001655 may_prompt=not options.force,
1656 verbose=options.verbose,
1657 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001658 if not hook_results.should_continue():
1659 return 1
1660 if not options.reviewers and hook_results.reviewers:
1661 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001662 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001663
Aaron Gable13101a62018-02-09 13:20:41 -08001664 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001665 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001666 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001667 _git_set_branch_config_value('last-upload-hash',
1668 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001669 # Run post upload hooks, if specified.
1670 if settings.GetRunPostUploadHook():
1671 presubmit_support.DoPostUploadExecuter(
1672 change,
1673 self,
1674 settings.GetRoot(),
1675 options.verbose,
1676 sys.stdout)
1677
1678 # Upload all dependencies if specified.
1679 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001680 print()
1681 print('--dependencies has been specified.')
1682 print('All dependent local branches will be re-uploaded.')
1683 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001684 # Remove the dependencies flag from args so that we do not end up in a
1685 # loop.
1686 orig_args.remove('--dependencies')
1687 ret = upload_branch_deps(self, orig_args)
1688 return ret
1689
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001690 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001691 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001692
1693 Issue must have been already uploaded and known.
1694 """
1695 assert new_state in _CQState.ALL_STATES
1696 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001697 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001698 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001699 return 0
1700 except KeyboardInterrupt:
1701 raise
1702 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001703 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001704 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001705 ' * Your project has no CQ,\n'
1706 ' * You don\'t have permission to change the CQ state,\n'
1707 ' * There\'s a bug in this code (see stack trace below).\n'
1708 'Consider specifying which bots to trigger manually or asking your '
1709 'project owners for permissions or contacting Chrome Infra at:\n'
1710 'https://www.chromium.org/infra\n\n' %
1711 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001712 # Still raise exception so that stack trace is printed.
1713 raise
1714
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001715 # Forward methods to codereview specific implementation.
1716
Aaron Gable636b13f2017-07-14 10:42:48 -07001717 def AddComment(self, message, publish=None):
1718 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001719
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001720 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001721 """Returns list of _CommentSummary for each comment.
1722
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001723 args:
1724 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001725 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001726 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001727
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001728 def CloseIssue(self):
1729 return self._codereview_impl.CloseIssue()
1730
1731 def GetStatus(self):
1732 return self._codereview_impl.GetStatus()
1733
1734 def GetCodereviewServer(self):
1735 return self._codereview_impl.GetCodereviewServer()
1736
tandriide281ae2016-10-12 06:02:30 -07001737 def GetIssueOwner(self):
1738 """Get owner from codereview, which may differ from this checkout."""
1739 return self._codereview_impl.GetIssueOwner()
1740
Edward Lemur707d70b2018-02-07 00:50:14 +01001741 def GetReviewers(self):
1742 return self._codereview_impl.GetReviewers()
1743
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001744 def GetMostRecentPatchset(self):
1745 return self._codereview_impl.GetMostRecentPatchset()
1746
tandriide281ae2016-10-12 06:02:30 -07001747 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001748 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001749 return self._codereview_impl.CannotTriggerTryJobReason()
1750
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001751 def GetTryJobProperties(self, patchset=None):
1752 """Returns dictionary of properties to launch try job."""
1753 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001754
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001755 def __getattr__(self, attr):
1756 # This is because lots of untested code accesses Rietveld-specific stuff
1757 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001758 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001759 # Note that child method defines __getattr__ as well, and forwards it here,
1760 # because _RietveldChangelistImpl is not cleaned up yet, and given
1761 # deprecation of Rietveld, it should probably be just removed.
1762 # Until that time, avoid infinite recursion by bypassing __getattr__
1763 # of implementation class.
1764 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001765
1766
1767class _ChangelistCodereviewBase(object):
1768 """Abstract base class encapsulating codereview specifics of a changelist."""
1769 def __init__(self, changelist):
1770 self._changelist = changelist # instance of Changelist
1771
1772 def __getattr__(self, attr):
1773 # Forward methods to changelist.
1774 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1775 # _RietveldChangelistImpl to avoid this hack?
1776 return getattr(self._changelist, attr)
1777
1778 def GetStatus(self):
1779 """Apply a rough heuristic to give a simple summary of an issue's review
1780 or CQ status, assuming adherence to a common workflow.
1781
1782 Returns None if no issue for this branch, or specific string keywords.
1783 """
1784 raise NotImplementedError()
1785
1786 def GetCodereviewServer(self):
1787 """Returns server URL without end slash, like "https://codereview.com"."""
1788 raise NotImplementedError()
1789
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001790 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001791 """Fetches and returns description from the codereview server."""
1792 raise NotImplementedError()
1793
tandrii5d48c322016-08-18 16:19:37 -07001794 @classmethod
1795 def IssueConfigKey(cls):
1796 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001797 raise NotImplementedError()
1798
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001799 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001800 def PatchsetConfigKey(cls):
1801 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001802 raise NotImplementedError()
1803
tandrii5d48c322016-08-18 16:19:37 -07001804 @classmethod
1805 def CodereviewServerConfigKey(cls):
1806 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001807 raise NotImplementedError()
1808
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001809 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001810 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001811 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001812
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001813 def GetGerritObjForPresubmit(self):
1814 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1815 return None
1816
dsansomee2d6fd92016-09-08 00:10:47 -07001817 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001818 """Update the description on codereview site."""
1819 raise NotImplementedError()
1820
Aaron Gable636b13f2017-07-14 10:42:48 -07001821 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001822 """Posts a comment to the codereview site."""
1823 raise NotImplementedError()
1824
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001825 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001826 raise NotImplementedError()
1827
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001828 def CloseIssue(self):
1829 """Closes the issue."""
1830 raise NotImplementedError()
1831
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001832 def GetMostRecentPatchset(self):
1833 """Returns the most recent patchset number from the codereview site."""
1834 raise NotImplementedError()
1835
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001836 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001837 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001838 """Fetches and applies the issue.
1839
1840 Arguments:
1841 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1842 reject: if True, reject the failed patch instead of switching to 3-way
1843 merge. Rietveld only.
1844 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1845 only.
1846 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001847 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001848 """
1849 raise NotImplementedError()
1850
1851 @staticmethod
1852 def ParseIssueURL(parsed_url):
1853 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1854 failed."""
1855 raise NotImplementedError()
1856
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001857 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001858 """Best effort check that user is authenticated with codereview server.
1859
1860 Arguments:
1861 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001862 refresh: whether to attempt to refresh credentials. Ignored if not
1863 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001864 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001865 raise NotImplementedError()
1866
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001867 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001868 """Best effort check that uploading isn't supposed to fail for predictable
1869 reasons.
1870
1871 This method should raise informative exception if uploading shouldn't
1872 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001873
1874 Arguments:
1875 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001876 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001877 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001878
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001879 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001880 """Uploads a change to codereview."""
1881 raise NotImplementedError()
1882
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001883 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001884 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001885
1886 Issue must have been already uploaded and known.
1887 """
1888 raise NotImplementedError()
1889
tandriie113dfd2016-10-11 10:20:12 -07001890 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001891 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001892 raise NotImplementedError()
1893
tandriide281ae2016-10-12 06:02:30 -07001894 def GetIssueOwner(self):
1895 raise NotImplementedError()
1896
Edward Lemur707d70b2018-02-07 00:50:14 +01001897 def GetReviewers(self):
1898 raise NotImplementedError()
1899
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001900 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001901 raise NotImplementedError()
1902
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001903
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001904class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001905 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001906 # auth_config is Rietveld thing, kept here to preserve interface only.
1907 super(_GerritChangelistImpl, self).__init__(changelist)
1908 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001909 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001910 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001911 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001912 # Map from change number (issue) to its detail cache.
1913 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001914
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001915 if codereview_host is not None:
1916 assert not codereview_host.startswith('https://'), codereview_host
1917 self._gerrit_host = codereview_host
1918 self._gerrit_server = 'https://%s' % codereview_host
1919
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001920 def _GetGerritHost(self):
1921 # Lazy load of configs.
1922 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001923 if self._gerrit_host and '.' not in self._gerrit_host:
1924 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1925 # This happens for internal stuff http://crbug.com/614312.
1926 parsed = urlparse.urlparse(self.GetRemoteUrl())
1927 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001928 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07001929 ' Your current remote is: %s' % self.GetRemoteUrl())
1930 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1931 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001932 return self._gerrit_host
1933
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001934 def _GetGitHost(self):
1935 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001936 remote_url = self.GetRemoteUrl()
1937 if not remote_url:
1938 return None
1939 return urlparse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001940
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001941 def GetCodereviewServer(self):
1942 if not self._gerrit_server:
1943 # If we're on a branch then get the server potentially associated
1944 # with that branch.
1945 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001946 self._gerrit_server = self._GitGetBranchConfigValue(
1947 self.CodereviewServerConfigKey())
1948 if self._gerrit_server:
1949 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001950 if not self._gerrit_server:
1951 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1952 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001953 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001954 parts[0] = parts[0] + '-review'
1955 self._gerrit_host = '.'.join(parts)
1956 self._gerrit_server = 'https://%s' % self._gerrit_host
1957 return self._gerrit_server
1958
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001959 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001960 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001961 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001962 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001963 logging.warn('can\'t detect Gerrit project.')
1964 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001965 project = urlparse.urlparse(remote_url).path.strip('/')
1966 if project.endswith('.git'):
1967 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001968 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1969 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1970 # gitiles/git-over-https protocol. E.g.,
1971 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1972 # as
1973 # https://chromium.googlesource.com/v8/v8
1974 if project.startswith('a/'):
1975 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001976 return project
1977
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001978 def _GerritChangeIdentifier(self):
1979 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1980
1981 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001982 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001983 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001984 project = self._GetGerritProject()
1985 if project:
1986 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1987 # Fall back on still unique, but less efficient change number.
1988 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001989
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001990 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001991 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001992 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001993
tandrii5d48c322016-08-18 16:19:37 -07001994 @classmethod
1995 def PatchsetConfigKey(cls):
1996 return 'gerritpatchset'
1997
1998 @classmethod
1999 def CodereviewServerConfigKey(cls):
2000 return 'gerritserver'
2001
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002002 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002003 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002004 if settings.GetGerritSkipEnsureAuthenticated():
2005 # For projects with unusual authentication schemes.
2006 # See http://crbug.com/603378.
2007 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002008
2009 # Check presence of cookies only if using cookies-based auth method.
2010 cookie_auth = gerrit_util.Authenticator.get()
2011 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002012 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002013
Daniel Chengcf6269b2019-05-18 01:02:12 +00002014 if urlparse.urlparse(self.GetRemoteUrl()).scheme != 'https':
2015 print('WARNING: Ignoring branch %s with non-https remote %s' %
2016 (self._changelist.branch, self.GetRemoteUrl()))
2017 return
2018
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002019 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002020 self.GetCodereviewServer()
2021 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00002022 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002023
2024 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2025 git_auth = cookie_auth.get_auth_header(git_host)
2026 if gerrit_auth and git_auth:
2027 if gerrit_auth == git_auth:
2028 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002029 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
Raul Tambre80ee78e2019-05-06 22:41:05 +00002030 print(
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002031 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002032 ' %s\n'
2033 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002034 ' Consider running the following command:\n'
2035 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002036 ' %s\n'
Raul Tambre80ee78e2019-05-06 22:41:05 +00002037 ' %s' %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002038 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002039 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002040 cookie_auth.get_new_password_message(git_host)))
2041 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002042 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002043 return
2044 else:
2045 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002046 ([] if gerrit_auth else [self._gerrit_host]) +
2047 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002048 DieWithError('Credentials for the following hosts are required:\n'
2049 ' %s\n'
2050 'These are read from %s (or legacy %s)\n'
2051 '%s' % (
2052 '\n '.join(missing),
2053 cookie_auth.get_gitcookies_path(),
2054 cookie_auth.get_netrc_path(),
2055 cookie_auth.get_new_password_message(git_host)))
2056
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002057 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002058 if not self.GetIssue():
2059 return
2060
2061 # Warm change details cache now to avoid RPCs later, reducing latency for
2062 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002063 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002064 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002065
2066 status = self._GetChangeDetail()['status']
2067 if status in ('MERGED', 'ABANDONED'):
2068 DieWithError('Change %s has been %s, new uploads are not allowed' %
2069 (self.GetIssueURL(),
2070 'submitted' if status == 'MERGED' else 'abandoned'))
2071
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002072 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2073 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2074 # Apparently this check is not very important? Otherwise get_auth_email
2075 # could have been added to other implementations of Authenticator.
2076 cookies_auth = gerrit_util.Authenticator.get()
2077 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002078 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002079
2080 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002081 if self.GetIssueOwner() == cookies_user:
2082 return
2083 logging.debug('change %s owner is %s, cookies user is %s',
2084 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002085 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002086 # so ask what Gerrit thinks of this user.
2087 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2088 if details['email'] == self.GetIssueOwner():
2089 return
2090 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002091 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002092 'as %s.\n'
2093 'Uploading may fail due to lack of permissions.' %
2094 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2095 confirm_or_exit(action='upload')
2096
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002097 def _PostUnsetIssueProperties(self):
2098 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002099 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002100
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002101 def GetGerritObjForPresubmit(self):
2102 return presubmit_support.GerritAccessor(self._GetGerritHost())
2103
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002104 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002105 """Apply a rough heuristic to give a simple summary of an issue's review
2106 or CQ status, assuming adherence to a common workflow.
2107
2108 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002109 * 'error' - error from review tool (including deleted issues)
2110 * 'unsent' - no reviewers added
2111 * 'waiting' - waiting for review
2112 * 'reply' - waiting for uploader to reply to review
2113 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002114 * 'dry-run' - dry-running in the CQ
2115 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07002116 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002117 """
2118 if not self.GetIssue():
2119 return None
2120
2121 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002122 data = self._GetChangeDetail([
2123 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002124 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002125 return 'error'
2126
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002127 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002128 return 'closed'
2129
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002130 cq_label = data['labels'].get('Commit-Queue', {})
2131 max_cq_vote = 0
2132 for vote in cq_label.get('all', []):
2133 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
2134 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002135 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002136 if max_cq_vote == 1:
2137 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002138
Aaron Gable9ab38c62017-04-06 14:36:33 -07002139 if data['labels'].get('Code-Review', {}).get('approved'):
2140 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002141
2142 if not data.get('reviewers', {}).get('REVIEWER', []):
2143 return 'unsent'
2144
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002145 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002146 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2147 last_message_author = messages.pop().get('author', {})
2148 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002149 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2150 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002151 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002152 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002153 if last_message_author.get('_account_id') == owner:
2154 # Most recent message was by owner.
2155 return 'waiting'
2156 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002157 # Some reply from non-owner.
2158 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002159
2160 # Somehow there are no messages even though there are reviewers.
2161 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002162
2163 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002164 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002165 patchset = data['revisions'][data['current_revision']]['_number']
2166 self.SetPatchset(patchset)
2167 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002168
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002169 def FetchDescription(self, force=False):
2170 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2171 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002172 current_rev = data['current_revision']
Dan Beamcf6df902018-11-08 01:48:37 +00002173 return data['revisions'][current_rev]['commit']['message'].encode(
2174 'utf-8', 'ignore')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002175
dsansomee2d6fd92016-09-08 00:10:47 -07002176 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002177 if gerrit_util.HasPendingChangeEdit(
2178 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002179 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002180 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002181 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002182 'unpublished edit. Either publish the edit in the Gerrit web UI '
2183 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002184
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002185 gerrit_util.DeletePendingChangeEdit(
2186 self._GetGerritHost(), self._GerritChangeIdentifier())
2187 gerrit_util.SetCommitMessage(
2188 self._GetGerritHost(), self._GerritChangeIdentifier(),
2189 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002190
Aaron Gable636b13f2017-07-14 10:42:48 -07002191 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002192 gerrit_util.SetReview(
2193 self._GetGerritHost(), self._GerritChangeIdentifier(),
2194 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002195
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002196 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002197 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002198 # CURRENT_REVISION is included to get the latest patchset so that
2199 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002200 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002201 options=['MESSAGES', 'DETAILED_ACCOUNTS',
2202 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002203 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002204 self._GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002205 robot_file_comments = gerrit_util.GetChangeRobotComments(
2206 self._GetGerritHost(), self._GerritChangeIdentifier())
2207
2208 # Add the robot comments onto the list of comments, but only
2209 # keep those that are from the latest pachset.
2210 latest_patch_set = self.GetMostRecentPatchset()
2211 for path, robot_comments in robot_file_comments.iteritems():
2212 line_comments = file_comments.setdefault(path, [])
2213 line_comments.extend(
2214 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002215
2216 # Build dictionary of file comments for easy access and sorting later.
2217 # {author+date: {path: {patchset: {line: url+message}}}}
2218 comments = collections.defaultdict(
2219 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2220 for path, line_comments in file_comments.iteritems():
2221 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002222 tag = comment.get('tag', '')
2223 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002224 continue
2225 key = (comment['author']['email'], comment['updated'])
2226 if comment.get('side', 'REVISION') == 'PARENT':
2227 patchset = 'Base'
2228 else:
2229 patchset = 'PS%d' % comment['patch_set']
2230 line = comment.get('line', 0)
2231 url = ('https://%s/c/%s/%s/%s#%s%s' %
2232 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2233 'b' if comment.get('side') == 'PARENT' else '',
2234 str(line) if line else ''))
2235 comments[key][path][patchset][line] = (url, comment['message'])
2236
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002237 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002238 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002239 summary = self._BuildCommentSummary(msg, comments, readable)
2240 if summary:
2241 summaries.append(summary)
2242 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002243
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002244 @staticmethod
2245 def _BuildCommentSummary(msg, comments, readable):
2246 key = (msg['author']['email'], msg['date'])
2247 # Don't bother showing autogenerated messages that don't have associated
2248 # file or line comments. this will filter out most autogenerated
2249 # messages, but will keep robot comments like those from Tricium.
2250 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
2251 if is_autogenerated and not comments.get(key):
2252 return None
2253 message = msg['message']
2254 # Gerrit spits out nanoseconds.
2255 assert len(msg['date'].split('.')[-1]) == 9
2256 date = datetime.datetime.strptime(msg['date'][:-3],
2257 '%Y-%m-%d %H:%M:%S.%f')
2258 if key in comments:
2259 message += '\n'
2260 for path, patchsets in sorted(comments.get(key, {}).items()):
2261 if readable:
2262 message += '\n%s' % path
2263 for patchset, lines in sorted(patchsets.items()):
2264 for line, (url, content) in sorted(lines.items()):
2265 if line:
2266 line_str = 'Line %d' % line
2267 path_str = '%s:%d:' % (path, line)
2268 else:
2269 line_str = 'File comment'
2270 path_str = '%s:0:' % path
2271 if readable:
2272 message += '\n %s, %s: %s' % (patchset, line_str, url)
2273 message += '\n %s\n' % content
2274 else:
2275 message += '\n%s ' % path_str
2276 message += '\n%s\n' % content
2277
2278 return _CommentSummary(
2279 date=date,
2280 message=message,
2281 sender=msg['author']['email'],
2282 autogenerated=is_autogenerated,
2283 # These could be inferred from the text messages and correlated with
2284 # Code-Review label maximum, however this is not reliable.
2285 # Leaving as is until the need arises.
2286 approval=False,
2287 disapproval=False,
2288 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002289
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002290 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002291 gerrit_util.AbandonChange(
2292 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002293
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002294 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002295 gerrit_util.SubmitChange(
2296 self._GetGerritHost(), self._GerritChangeIdentifier(),
2297 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002298
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002299 def _GetChangeDetail(self, options=None, no_cache=False):
2300 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002301
2302 If fresh data is needed, set no_cache=True which will clear cache and
2303 thus new data will be fetched from Gerrit.
2304 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002305 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002306 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002307
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002308 # Optimization to avoid multiple RPCs:
2309 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2310 'CURRENT_COMMIT' not in options):
2311 options.append('CURRENT_COMMIT')
2312
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002313 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002314 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002315 options = [o.upper() for o in options]
2316
2317 # Check in cache first unless no_cache is True.
2318 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002319 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002320 else:
2321 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002322 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002323 # Assumption: data fetched before with extra options is suitable
2324 # for return for a smaller set of options.
2325 # For example, if we cached data for
2326 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2327 # and request is for options=[CURRENT_REVISION],
2328 # THEN we can return prior cached data.
2329 if options_set.issubset(cached_options_set):
2330 return data
2331
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002332 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002333 data = gerrit_util.GetChangeDetail(
2334 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002335 except gerrit_util.GerritError as e:
2336 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002337 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002338 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002339
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002340 self._detail_cache.setdefault(cache_key, []).append(
2341 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002342 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002343
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002344 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002345 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002346 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002347 data = gerrit_util.GetChangeCommit(
2348 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002349 except gerrit_util.GerritError as e:
2350 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002351 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002352 raise
agable32978d92016-11-01 12:55:02 -07002353 return data
2354
Karen Qian40c19422019-03-13 21:28:29 +00002355 def _IsCqConfigured(self):
2356 detail = self._GetChangeDetail(['LABELS'])
2357 if not u'Commit-Queue' in detail.get('labels', {}):
2358 return False
2359 # TODO(crbug/753213): Remove temporary hack
2360 if ('https://chromium.googlesource.com/chromium/src' ==
2361 self._changelist.GetRemoteUrl() and
2362 detail['branch'].startswith('refs/branch-heads/')):
2363 return False
2364 return True
2365
Olivier Robin75ee7252018-04-13 10:02:56 +02002366 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002367 if git_common.is_dirty_git_tree('land'):
2368 return 1
Karen Qian40c19422019-03-13 21:28:29 +00002369
tandriid60367b2016-06-22 05:25:12 -07002370 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00002371 if not force and self._IsCqConfigured():
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002372 confirm_or_exit('\nIt seems this repository has a CQ, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002373 'which can test and land changes for you. '
2374 'Are you sure you wish to bypass it?\n',
2375 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002376 differs = True
tandriic4344b52016-08-29 06:04:54 -07002377 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002378 # Note: git diff outputs nothing if there is no diff.
2379 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002380 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002381 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002382 if detail['current_revision'] == last_upload:
2383 differs = False
2384 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002385 print('WARNING: Local branch contents differ from latest uploaded '
2386 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002387 if differs:
2388 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002389 confirm_or_exit(
2390 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2391 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002392 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002393 elif not bypass_hooks:
2394 hook_results = self.RunHook(
2395 committing=True,
2396 may_prompt=not force,
2397 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002398 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2399 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002400 if not hook_results.should_continue():
2401 return 1
2402
2403 self.SubmitIssue(wait_for_merge=True)
2404 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002405 links = self._GetChangeCommit().get('web_links', [])
2406 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002407 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002408 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002409 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002410 return 0
2411
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002412 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002413 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002414 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002415 assert not directory
2416 assert parsed_issue_arg.valid
2417
2418 self._changelist.issue = parsed_issue_arg.issue
2419
2420 if parsed_issue_arg.hostname:
2421 self._gerrit_host = parsed_issue_arg.hostname
2422 self._gerrit_server = 'https://%s' % self._gerrit_host
2423
tandriic2405f52016-10-10 08:13:15 -07002424 try:
2425 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002426 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002427 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002428
2429 if not parsed_issue_arg.patchset:
2430 # Use current revision by default.
2431 revision_info = detail['revisions'][detail['current_revision']]
2432 patchset = int(revision_info['_number'])
2433 else:
2434 patchset = parsed_issue_arg.patchset
2435 for revision_info in detail['revisions'].itervalues():
2436 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2437 break
2438 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002439 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002440 (parsed_issue_arg.patchset, self.GetIssue()))
2441
Aaron Gable697a91b2018-01-19 15:20:15 -08002442 remote_url = self._changelist.GetRemoteUrl()
2443 if remote_url.endswith('.git'):
2444 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002445 remote_url = remote_url.rstrip('/')
2446
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002447 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002448 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002449
2450 if remote_url != fetch_info['url']:
2451 DieWithError('Trying to patch a change from %s but this repo appears '
2452 'to be %s.' % (fetch_info['url'], remote_url))
2453
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002454 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002455
Aaron Gable62619a32017-06-16 08:22:09 -07002456 if force:
2457 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2458 print('Checked out commit for change %i patchset %i locally' %
2459 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002460 elif nocommit:
2461 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2462 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002463 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002464 RunGit(['cherry-pick', 'FETCH_HEAD'])
2465 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002466 (parsed_issue_arg.issue, patchset))
2467 print('Note: this created a local commit which does not have '
2468 'the same hash as the one uploaded for review. This will make '
2469 'uploading changes based on top of this branch difficult.\n'
2470 'If you want to do that, use "git cl patch --force" instead.')
2471
Stefan Zagerd08043c2017-10-12 12:07:02 -07002472 if self.GetBranch():
2473 self.SetIssue(parsed_issue_arg.issue)
2474 self.SetPatchset(patchset)
2475 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2476 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2477 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2478 else:
2479 print('WARNING: You are in detached HEAD state.\n'
2480 'The patch has been applied to your checkout, but you will not be '
2481 'able to upload a new patch set to the gerrit issue.\n'
2482 'Try using the \'-b\' option if you would like to work on a '
2483 'branch and/or upload a new patch set.')
2484
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002485 return 0
2486
2487 @staticmethod
2488 def ParseIssueURL(parsed_url):
2489 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2490 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002491 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2492 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002493 # Short urls like https://domain/<issue_number> can be used, but don't allow
2494 # specifying the patchset (you'd 404), but we allow that here.
2495 if parsed_url.path == '/':
2496 part = parsed_url.fragment
2497 else:
2498 part = parsed_url.path
Bruce Dawson9c062012019-05-02 19:20:28 +00002499 match = re.match(r'(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002500 if match:
2501 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002502 issue=int(match.group(3)),
2503 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002504 hostname=parsed_url.netloc,
2505 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002506 return None
2507
tandrii16e0b4e2016-06-07 10:34:28 -07002508 def _GerritCommitMsgHookCheck(self, offer_removal):
2509 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2510 if not os.path.exists(hook):
2511 return
2512 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2513 # custom developer made one.
2514 data = gclient_utils.FileRead(hook)
2515 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2516 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002517 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002518 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002519 'and may interfere with it in subtle ways.\n'
2520 'We recommend you remove the commit-msg hook.')
2521 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002522 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002523 gclient_utils.rm_file_or_tree(hook)
2524 print('Gerrit commit-msg hook removed.')
2525 else:
2526 print('OK, will keep Gerrit commit-msg hook in place.')
2527
Edward Lemur1b52d872019-05-09 21:12:12 +00002528 def _CleanUpOldTraces(self):
2529 """Keep only the last |MAX_TRACES| traces."""
2530 try:
2531 traces = sorted([
2532 os.path.join(TRACES_DIR, f)
2533 for f in os.listdir(TRACES_DIR)
2534 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2535 and not f.startswith('tmp'))
2536 ])
2537 traces_to_delete = traces[:-MAX_TRACES]
2538 for trace in traces_to_delete:
Daniel Chengcf6269b2019-05-18 01:02:12 +00002539 os.remove(trace)
Edward Lemur1b52d872019-05-09 21:12:12 +00002540 except OSError:
2541 print('WARNING: Failed to remove old git traces from\n'
2542 ' %s'
2543 'Consider removing them manually.' % TRACES_DIR)
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002544
Edward Lemur5737f022019-05-17 01:24:00 +00002545 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
Edward Lemur1b52d872019-05-09 21:12:12 +00002546 """Zip and write the git push traces stored in traces_dir."""
2547 gclient_utils.safe_makedirs(TRACES_DIR)
Edward Lemur1b52d872019-05-09 21:12:12 +00002548 traces_zip = trace_name + '-traces'
2549 traces_readme = trace_name + '-README'
Michael Mosse7f0b4c2019-05-08 04:36:24 +00002550 # Create a temporary dir to store git config and gitcookies in. It will be
2551 # compressed and stored next to the traces.
2552 git_info_dir = tempfile.mkdtemp()
Edward Lemur1b52d872019-05-09 21:12:12 +00002553 git_info_zip = trace_name + '-git-info'
2554
Edward Lemur5737f022019-05-17 01:24:00 +00002555 git_push_metadata['now'] = datetime_now().strftime('%c')
Eric Boren67c48202019-05-30 16:52:51 +00002556 if sys.stdin.encoding and sys.stdin.encoding != 'utf-8':
sangwoo.ko7a614332019-05-22 02:46:19 +00002557 git_push_metadata['now'] = git_push_metadata['now'].decode(
2558 sys.stdin.encoding)
2559
Edward Lemur1b52d872019-05-09 21:12:12 +00002560 git_push_metadata['trace_name'] = trace_name
2561 gclient_utils.FileWrite(
2562 traces_readme, TRACES_README_FORMAT % git_push_metadata)
2563
2564 # Keep only the first 6 characters of the git hashes on the packet
2565 # trace. This greatly decreases size after compression.
2566 packet_traces = os.path.join(traces_dir, 'trace-packet')
2567 if os.path.isfile(packet_traces):
2568 contents = gclient_utils.FileRead(packet_traces)
2569 gclient_utils.FileWrite(
2570 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2571 shutil.make_archive(traces_zip, 'zip', traces_dir)
2572
2573 # Collect and compress the git config and gitcookies.
2574 git_config = RunGit(['config', '-l'])
2575 gclient_utils.FileWrite(
2576 os.path.join(git_info_dir, 'git-config'),
2577 git_config)
2578
2579 cookie_auth = gerrit_util.Authenticator.get()
2580 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2581 gitcookies_path = cookie_auth.get_gitcookies_path()
2582 if os.path.isfile(gitcookies_path):
2583 gitcookies = gclient_utils.FileRead(gitcookies_path)
2584 gclient_utils.FileWrite(
2585 os.path.join(git_info_dir, 'gitcookies'),
2586 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2587 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2588
Edward Lemur1b52d872019-05-09 21:12:12 +00002589 gclient_utils.rmtree(git_info_dir)
2590
2591 def _RunGitPushWithTraces(
2592 self, change_desc, refspec, refspec_opts, git_push_metadata):
2593 """Run git push and collect the traces resulting from the execution."""
2594 # Create a temporary directory to store traces in. Traces will be compressed
2595 # and stored in a 'traces' dir inside depot_tools.
2596 traces_dir = tempfile.mkdtemp()
Edward Lemur5737f022019-05-17 01:24:00 +00002597 trace_name = os.path.join(
2598 TRACES_DIR, datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
Edward Lemur0f58ae42019-04-30 17:24:12 +00002599
2600 env = os.environ.copy()
2601 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2602 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Jonathan Nieder9779b142019-05-29 23:19:29 +00002603 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002604 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2605 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2606 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2607
2608 try:
2609 push_returncode = 0
Edward Lemur1b52d872019-05-09 21:12:12 +00002610 remote_url = self.GetRemoteUrl()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002611 before_push = time_time()
2612 push_stdout = gclient_utils.CheckCallAndFilter(
Edward Lemur1b52d872019-05-09 21:12:12 +00002613 ['git', 'push', remote_url, refspec],
Edward Lemur0f58ae42019-04-30 17:24:12 +00002614 env=env,
2615 print_stdout=True,
2616 # Flush after every line: useful for seeing progress when running as
2617 # recipe.
2618 filter_fn=lambda _: sys.stdout.flush())
2619 except subprocess2.CalledProcessError as e:
2620 push_returncode = e.returncode
2621 DieWithError('Failed to create a change. Please examine output above '
2622 'for the reason of the failure.\n'
2623 'Hint: run command below to diagnose common Git/Gerrit '
2624 'credential problems:\n'
Edward Lemur5737f022019-05-17 01:24:00 +00002625 ' git cl creds-check\n'
2626 '\n'
2627 'If git-cl is not working correctly, file a bug under the '
2628 'Infra>SDK component including the files below.\n'
2629 'Review the files before upload, since they might contain '
2630 'sensitive information.\n'
2631 'Set the Restrict-View-Google label so that they are not '
2632 'publicly accessible.\n'
2633 + TRACES_MESSAGE % {'trace_name': trace_name},
Edward Lemur0f58ae42019-04-30 17:24:12 +00002634 change_desc)
2635 finally:
2636 execution_time = time_time() - before_push
2637 metrics.collector.add_repeated('sub_commands', {
2638 'command': 'git push',
2639 'execution_time': execution_time,
2640 'exit_code': push_returncode,
2641 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2642 })
2643
Edward Lemur1b52d872019-05-09 21:12:12 +00002644 git_push_metadata['execution_time'] = execution_time
2645 git_push_metadata['exit_code'] = push_returncode
Edward Lemur5737f022019-05-17 01:24:00 +00002646 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002647
Edward Lemur1b52d872019-05-09 21:12:12 +00002648 self._CleanUpOldTraces()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002649 gclient_utils.rmtree(traces_dir)
2650
2651 return push_stdout
2652
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002653 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002654 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002655 if options.squash and options.no_squash:
2656 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002657
2658 if not options.squash and not options.no_squash:
2659 # Load default for user, repo, squash=true, in this order.
2660 options.squash = settings.GetSquashGerritUploads()
2661 elif options.no_squash:
2662 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002663
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002664 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002665 branch = GetTargetRef(remote, remote_branch, options.target_branch)
Aaron Gableb56ad332017-01-06 15:24:31 -08002666 # This may be None; default fallback value is determined in logic below.
2667 title = options.title
2668
Dominic Battre7d1c4842017-10-27 09:17:28 +02002669 # Extract bug number from branch name.
2670 bug = options.bug
2671 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2672 if not bug and match:
2673 bug = match.group(1)
2674
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002675 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002676 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002677 if self.GetIssue():
2678 # Try to get the message from a previous upload.
2679 message = self.GetDescription()
2680 if not message:
2681 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002682 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002683 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002684 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002685 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002686 # When uploading a subsequent patchset, -m|--message is taken
2687 # as the patchset title if --title was not provided.
2688 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002689 else:
2690 default_title = RunGit(
2691 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002692 if options.force:
2693 title = default_title
2694 else:
2695 title = ask_for_data(
2696 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002697 change_id = self._GetChangeDetail()['change_id']
2698 while True:
2699 footer_change_ids = git_footers.get_footer_change_id(message)
2700 if footer_change_ids == [change_id]:
2701 break
2702 if not footer_change_ids:
2703 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002704 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002705 continue
2706 # There is already a valid footer but with different or several ids.
2707 # Doing this automatically is non-trivial as we don't want to lose
2708 # existing other footers, yet we want to append just 1 desired
2709 # Change-Id. Thus, just create a new footer, but let user verify the
2710 # new description.
2711 message = '%s\n\nChange-Id: %s' % (message, change_id)
2712 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002713 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002714 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002715 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002716 'Please, check the proposed correction to the description, '
2717 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2718 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2719 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002720 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002721 if not options.force:
2722 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002723 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002724 message = change_desc.description
2725 if not message:
2726 DieWithError("Description is empty. Aborting...")
2727 # Continue the while loop.
2728 # Sanity check of this code - we should end up with proper message
2729 # footer.
2730 assert [change_id] == git_footers.get_footer_change_id(message)
2731 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002732 else: # if not self.GetIssue()
2733 if options.message:
2734 message = options.message
2735 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002736 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002737 if options.title:
2738 message = options.title + '\n\n' + message
2739 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002740
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002741 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002742 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002743 # On first upload, patchset title is always this string, while
2744 # --title flag gets converted to first line of message.
2745 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002746 if not change_desc.description:
2747 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002748 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002749 if len(change_ids) > 1:
2750 DieWithError('too many Change-Id footers, at most 1 allowed.')
2751 if not change_ids:
2752 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002753 change_desc.set_description(git_footers.add_footer_change_id(
2754 change_desc.description,
2755 GenerateGerritChangeId(change_desc.description)))
2756 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002757 assert len(change_ids) == 1
2758 change_id = change_ids[0]
2759
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002760 if options.reviewers or options.tbrs or options.add_owners_to:
2761 change_desc.update_reviewers(options.reviewers, options.tbrs,
2762 options.add_owners_to, change)
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002763 if options.preserve_tryjobs:
2764 change_desc.set_preserve_tryjobs()
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002765
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002766 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002767 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2768 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002769 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002770 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2771 desc_tempfile.write(change_desc.description)
2772 desc_tempfile.close()
2773 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2774 '-F', desc_tempfile.name]).strip()
2775 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002776 else:
2777 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002778 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002779 if not change_desc.description:
2780 DieWithError("Description is empty. Aborting...")
2781
2782 if not git_footers.get_footer_change_id(change_desc.description):
2783 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002784 change_desc.set_description(
2785 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002786 if options.reviewers or options.tbrs or options.add_owners_to:
2787 change_desc.update_reviewers(options.reviewers, options.tbrs,
2788 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002789 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002790 # For no-squash mode, we assume the remote called "origin" is the one we
2791 # want. It is not worthwhile to support different workflows for
2792 # no-squash mode.
2793 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002794 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2795
2796 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002797 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002798 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2799 ref_to_push)]).splitlines()
2800 if len(commits) > 1:
2801 print('WARNING: This will upload %d commits. Run the following command '
2802 'to see which commits will be uploaded: ' % len(commits))
2803 print('git log %s..%s' % (parent, ref_to_push))
2804 print('You can also use `git squash-branch` to squash these into a '
2805 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002806 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002807
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002808 if options.reviewers or options.tbrs or options.add_owners_to:
2809 change_desc.update_reviewers(options.reviewers, options.tbrs,
2810 options.add_owners_to, change)
2811
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002812 reviewers = sorted(change_desc.get_reviewers())
2813 # Add cc's from the CC_LIST and --cc flag (if any).
2814 if not options.private and not options.no_autocc:
2815 cc = self.GetCCList().split(',')
2816 else:
2817 cc = []
2818 if options.cc:
2819 cc.extend(options.cc)
2820 cc = filter(None, [email.strip() for email in cc])
2821 if change_desc.get_cced():
2822 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002823 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2824 valid_accounts = set(reviewers + cc)
2825 # TODO(crbug/877717): relax this for all hosts.
2826 else:
2827 valid_accounts = gerrit_util.ValidAccounts(
2828 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002829 logging.info('accounts %s are recognized, %s invalid',
2830 sorted(valid_accounts),
2831 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002832
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002833 # Extra options that can be specified at push time. Doc:
2834 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002835 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002836
Aaron Gable844cf292017-06-28 11:32:59 -07002837 # By default, new changes are started in WIP mode, and subsequent patchsets
2838 # don't send email. At any time, passing --send-mail will mark the change
2839 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002840 if options.send_mail:
2841 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002842 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002843 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002844 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002845 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002846 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002847
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002848 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002849 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002850
Aaron Gable9b713dd2016-12-14 16:04:21 -08002851 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002852 # Punctuation and whitespace in |title| must be percent-encoded.
2853 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002854
agablec6787972016-09-09 16:13:34 -07002855 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002856 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002857
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002858 for r in sorted(reviewers):
2859 if r in valid_accounts:
2860 refspec_opts.append('r=%s' % r)
2861 reviewers.remove(r)
2862 else:
2863 # TODO(tandrii): this should probably be a hard failure.
2864 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2865 % r)
2866 for c in sorted(cc):
2867 # refspec option will be rejected if cc doesn't correspond to an
2868 # account, even though REST call to add such arbitrary cc may succeed.
2869 if c in valid_accounts:
2870 refspec_opts.append('cc=%s' % c)
2871 cc.remove(c)
2872
rmistry9eadede2016-09-19 11:22:43 -07002873 if options.topic:
2874 # Documentation on Gerrit topics is here:
2875 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002876 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002877
Edward Lemur687ca902018-12-05 02:30:30 +00002878 if options.enable_auto_submit:
2879 refspec_opts.append('l=Auto-Submit+1')
2880 if options.use_commit_queue:
2881 refspec_opts.append('l=Commit-Queue+2')
2882 elif options.cq_dry_run:
2883 refspec_opts.append('l=Commit-Queue+1')
2884
2885 if change_desc.get_reviewers(tbr_only=True):
2886 score = gerrit_util.GetCodeReviewTbrScore(
2887 self._GetGerritHost(),
2888 self._GetGerritProject())
2889 refspec_opts.append('l=Code-Review+%s' % score)
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002890
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002891 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002892 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002893 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002894 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002895 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2896
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002897 refspec_suffix = ''
2898 if refspec_opts:
2899 refspec_suffix = '%' + ','.join(refspec_opts)
2900 assert ' ' not in refspec_suffix, (
2901 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2902 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2903
Edward Lemur1b52d872019-05-09 21:12:12 +00002904 git_push_metadata = {
2905 'gerrit_host': self._GetGerritHost(),
2906 'title': title or '<untitled>',
2907 'change_id': change_id,
2908 'description': change_desc.description,
2909 }
2910 push_stdout = self._RunGitPushWithTraces(
2911 change_desc, refspec, refspec_opts, git_push_metadata)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002912
2913 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002914 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002915 change_numbers = [m.group(1)
2916 for m in map(regex.match, push_stdout.splitlines())
2917 if m]
2918 if len(change_numbers) != 1:
2919 DieWithError(
2920 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002921 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002922 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002923 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002924
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002925 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002926 # GetIssue() is not set in case of non-squash uploads according to tests.
2927 # TODO(agable): non-squash uploads in git cl should be removed.
2928 gerrit_util.AddReviewers(
2929 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002930 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002931 reviewers, cc,
2932 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002933
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002934 return 0
2935
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002936 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2937 change_desc):
2938 """Computes parent of the generated commit to be uploaded to Gerrit.
2939
2940 Returns revision or a ref name.
2941 """
2942 if custom_cl_base:
2943 # Try to avoid creating additional unintended CLs when uploading, unless
2944 # user wants to take this risk.
2945 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2946 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2947 local_ref_of_target_remote])
2948 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002949 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002950 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2951 'If you proceed with upload, more than 1 CL may be created by '
2952 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2953 'If you are certain that specified base `%s` has already been '
2954 'uploaded to Gerrit as another CL, you may proceed.\n' %
2955 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2956 if not force:
2957 confirm_or_exit(
2958 'Do you take responsibility for cleaning up potential mess '
2959 'resulting from proceeding with upload?',
2960 action='upload')
2961 return custom_cl_base
2962
Aaron Gablef97e33d2017-03-30 15:44:27 -07002963 if remote != '.':
2964 return self.GetCommonAncestorWithUpstream()
2965
2966 # If our upstream branch is local, we base our squashed commit on its
2967 # squashed version.
2968 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2969
Aaron Gablef97e33d2017-03-30 15:44:27 -07002970 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002971 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002972
2973 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002974 # TODO(tandrii): consider checking parent change in Gerrit and using its
2975 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2976 # the tree hash of the parent branch. The upside is less likely bogus
2977 # requests to reupload parent change just because it's uploadhash is
2978 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07002979 parent = RunGit(['config',
2980 'branch.%s.gerritsquashhash' % upstream_branch_name],
2981 error_ok=True).strip()
2982 # Verify that the upstream branch has been uploaded too, otherwise
2983 # Gerrit will create additional CLs when uploading.
2984 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2985 RunGitSilent(['rev-parse', parent + ':'])):
2986 DieWithError(
2987 '\nUpload upstream branch %s first.\n'
2988 'It is likely that this branch has been rebased since its last '
2989 'upload, so you just need to upload it again.\n'
2990 '(If you uploaded it with --no-squash, then branch dependencies '
2991 'are not supported, and you should reupload with --squash.)'
2992 % upstream_branch_name,
2993 change_desc)
2994 return parent
2995
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002996 def _AddChangeIdToCommitMessage(self, options, args):
2997 """Re-commits using the current message, assumes the commit hook is in
2998 place.
2999 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00003000 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003001 git_command = ['commit', '--amend', '-m', log_desc]
3002 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00003003 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003004 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003005 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003006 return new_log_desc
3007 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003008 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003009
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003010 def SetCQState(self, new_state):
3011 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003012 vote_map = {
3013 _CQState.NONE: 0,
3014 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003015 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003016 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003017 labels = {'Commit-Queue': vote_map[new_state]}
3018 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00003019 gerrit_util.SetReview(
3020 self._GetGerritHost(), self._GerritChangeIdentifier(),
3021 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003022
tandriie113dfd2016-10-11 10:20:12 -07003023 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003024 try:
3025 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003026 except GerritChangeNotExists:
3027 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003028
3029 if data['status'] in ('ABANDONED', 'MERGED'):
3030 return 'CL %s is closed' % self.GetIssue()
3031
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003032 def GetTryJobProperties(self, patchset=None):
3033 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003034 data = self._GetChangeDetail(['ALL_REVISIONS'])
3035 patchset = int(patchset or self.GetPatchset())
3036 assert patchset
3037 revision_data = None # Pylint wants it to be defined.
3038 for revision_data in data['revisions'].itervalues():
3039 if int(revision_data['_number']) == patchset:
3040 break
3041 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003042 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003043 (patchset, self.GetIssue()))
3044 return {
3045 'patch_issue': self.GetIssue(),
3046 'patch_set': patchset or self.GetPatchset(),
3047 'patch_project': data['project'],
3048 'patch_storage': 'gerrit',
3049 'patch_ref': revision_data['fetch']['http']['ref'],
3050 'patch_repository_url': revision_data['fetch']['http']['url'],
3051 'patch_gerrit_url': self.GetCodereviewServer(),
3052 }
tandriie113dfd2016-10-11 10:20:12 -07003053
tandriide281ae2016-10-12 06:02:30 -07003054 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003055 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003056
Edward Lemur707d70b2018-02-07 00:50:14 +01003057 def GetReviewers(self):
3058 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00003059 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01003060
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003061
3062_CODEREVIEW_IMPLEMENTATIONS = {
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003063 'gerrit': _GerritChangelistImpl,
3064}
3065
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003066
iannuccie53c9352016-08-17 14:40:40 -07003067def _add_codereview_issue_select_options(parser, extra=""):
3068 _add_codereview_select_options(parser)
3069
3070 text = ('Operate on this issue number instead of the current branch\'s '
3071 'implicit issue.')
3072 if extra:
3073 text += ' '+extra
3074 parser.add_option('-i', '--issue', type=int, help=text)
3075
3076
3077def _process_codereview_issue_select_options(parser, options):
3078 _process_codereview_select_options(parser, options)
3079 if options.issue is not None and not options.forced_codereview:
3080 parser.error('--issue must be specified with either --rietveld or --gerrit')
3081
3082
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003083def _add_codereview_select_options(parser):
3084 """Appends --gerrit and --rietveld options to force specific codereview."""
3085 parser.codereview_group = optparse.OptionGroup(
3086 parser, 'EXPERIMENTAL! Codereview override options')
3087 parser.add_option_group(parser.codereview_group)
3088 parser.codereview_group.add_option(
3089 '--gerrit', action='store_true',
3090 help='Force the use of Gerrit for codereview')
3091 parser.codereview_group.add_option(
3092 '--rietveld', action='store_true',
3093 help='Force the use of Rietveld for codereview')
3094
3095
3096def _process_codereview_select_options(parser, options):
Andrii Shyshkalovfeec80e2018-10-16 01:00:47 +00003097 if options.rietveld:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00003098 parser.error('--rietveld is no longer supported.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003099 options.forced_codereview = None
3100 if options.gerrit:
3101 options.forced_codereview = 'gerrit'
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003102
3103
tandriif9aefb72016-07-01 09:06:51 -07003104def _get_bug_line_values(default_project, bugs):
3105 """Given default_project and comma separated list of bugs, yields bug line
3106 values.
3107
3108 Each bug can be either:
3109 * a number, which is combined with default_project
3110 * string, which is left as is.
3111
3112 This function may produce more than one line, because bugdroid expects one
3113 project per line.
3114
3115 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3116 ['v8:123', 'chromium:789']
3117 """
3118 default_bugs = []
3119 others = []
3120 for bug in bugs.split(','):
3121 bug = bug.strip()
3122 if bug:
3123 try:
3124 default_bugs.append(int(bug))
3125 except ValueError:
3126 others.append(bug)
3127
3128 if default_bugs:
3129 default_bugs = ','.join(map(str, default_bugs))
3130 if default_project:
3131 yield '%s:%s' % (default_project, default_bugs)
3132 else:
3133 yield default_bugs
3134 for other in sorted(others):
3135 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3136 yield other
3137
3138
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003139class ChangeDescription(object):
3140 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003141 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003142 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003143 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003144 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003145 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3146 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3147 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3148 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003149
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003150 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003151 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003152
agable@chromium.org42c20792013-09-12 17:34:49 +00003153 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003154 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003155 return '\n'.join(self._description_lines)
3156
3157 def set_description(self, desc):
3158 if isinstance(desc, basestring):
3159 lines = desc.splitlines()
3160 else:
3161 lines = [line.rstrip() for line in desc]
3162 while lines and not lines[0]:
3163 lines.pop(0)
3164 while lines and not lines[-1]:
3165 lines.pop(-1)
3166 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003167
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003168 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3169 """Rewrites the R=/TBR= line(s) as a single line each.
3170
3171 Args:
3172 reviewers (list(str)) - list of additional emails to use for reviewers.
3173 tbrs (list(str)) - list of additional emails to use for TBRs.
3174 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3175 the change that are missing OWNER coverage. If this is not None, you
3176 must also pass a value for `change`.
3177 change (Change) - The Change that should be used for OWNERS lookups.
3178 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003179 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003180 assert isinstance(tbrs, list), tbrs
3181
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003182 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003183 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003184
3185 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003186 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003187
3188 reviewers = set(reviewers)
3189 tbrs = set(tbrs)
3190 LOOKUP = {
3191 'TBR': tbrs,
3192 'R': reviewers,
3193 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003194
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003195 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003196 regexp = re.compile(self.R_LINE)
3197 matches = [regexp.match(line) for line in self._description_lines]
3198 new_desc = [l for i, l in enumerate(self._description_lines)
3199 if not matches[i]]
3200 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003201
agable@chromium.org42c20792013-09-12 17:34:49 +00003202 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003203
3204 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003205 for match in matches:
3206 if not match:
3207 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003208 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3209
3210 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003211 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003212 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003213 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003214 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003215 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003216 LOOKUP[add_owners_to].update(
3217 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003218
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003219 # If any folks ended up in both groups, remove them from tbrs.
3220 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003221
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003222 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3223 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003224
3225 # Put the new lines in the description where the old first R= line was.
3226 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3227 if 0 <= line_loc < len(self._description_lines):
3228 if new_tbr_line:
3229 self._description_lines.insert(line_loc, new_tbr_line)
3230 if new_r_line:
3231 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003232 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003233 if new_r_line:
3234 self.append_footer(new_r_line)
3235 if new_tbr_line:
3236 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003237
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00003238 def set_preserve_tryjobs(self):
3239 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
3240 footers = git_footers.parse_footers(self.description)
3241 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
3242 if v.lower() == 'true':
3243 return
3244 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
3245
Aaron Gable3a16ed12017-03-23 10:51:55 -07003246 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003247 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003248 self.set_description([
3249 '# Enter a description of the change.',
3250 '# This will be displayed on the codereview site.',
3251 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003252 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003253 '--------------------',
3254 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003255
agable@chromium.org42c20792013-09-12 17:34:49 +00003256 regexp = re.compile(self.BUG_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00003257 prefix = settings.GetBugPrefix()
agable@chromium.org42c20792013-09-12 17:34:49 +00003258 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003259 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003260 if git_footer:
3261 self.append_footer('Bug: %s' % ', '.join(values))
3262 else:
3263 for value in values:
3264 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003265
agable@chromium.org42c20792013-09-12 17:34:49 +00003266 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003267 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003268 if not content:
3269 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003270 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003271
Bruce Dawson2377b012018-01-11 16:46:49 -08003272 # Strip off comments and default inserted "Bug:" line.
3273 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00003274 (line.startswith('#') or
3275 line.rstrip() == "Bug:" or
3276 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00003277 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003278 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003279 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003280
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003281 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003282 """Adds a footer line to the description.
3283
3284 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3285 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3286 that Gerrit footers are always at the end.
3287 """
3288 parsed_footer_line = git_footers.parse_footer(line)
3289 if parsed_footer_line:
3290 # Line is a gerrit footer in the form: Footer-Key: any value.
3291 # Thus, must be appended observing Gerrit footer rules.
3292 self.set_description(
3293 git_footers.add_footer(self.description,
3294 key=parsed_footer_line[0],
3295 value=parsed_footer_line[1]))
3296 return
3297
3298 if not self._description_lines:
3299 self._description_lines.append(line)
3300 return
3301
3302 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3303 if gerrit_footers:
3304 # git_footers.split_footers ensures that there is an empty line before
3305 # actual (gerrit) footers, if any. We have to keep it that way.
3306 assert top_lines and top_lines[-1] == ''
3307 top_lines, separator = top_lines[:-1], top_lines[-1:]
3308 else:
3309 separator = [] # No need for separator if there are no gerrit_footers.
3310
3311 prev_line = top_lines[-1] if top_lines else ''
3312 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3313 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3314 top_lines.append('')
3315 top_lines.append(line)
3316 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003317
tandrii99a72f22016-08-17 14:33:24 -07003318 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003319 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003320 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003321 reviewers = [match.group(2).strip()
3322 for match in matches
3323 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003324 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003325
bradnelsond975b302016-10-23 12:20:23 -07003326 def get_cced(self):
3327 """Retrieves the list of reviewers."""
3328 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3329 cced = [match.group(2).strip() for match in matches if match]
3330 return cleanup_list(cced)
3331
Nodir Turakulov23b82142017-11-16 11:04:25 -08003332 def get_hash_tags(self):
3333 """Extracts and sanitizes a list of Gerrit hashtags."""
3334 subject = (self._description_lines or ('',))[0]
3335 subject = re.sub(
3336 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3337
3338 tags = []
3339 start = 0
3340 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3341 while True:
3342 m = bracket_exp.match(subject, start)
3343 if not m:
3344 break
3345 tags.append(self.sanitize_hash_tag(m.group(1)))
3346 start = m.end()
3347
3348 if not tags:
3349 # Try "Tag: " prefix.
3350 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3351 if m:
3352 tags.append(self.sanitize_hash_tag(m.group(1)))
3353 return tags
3354
3355 @classmethod
3356 def sanitize_hash_tag(cls, tag):
3357 """Returns a sanitized Gerrit hash tag.
3358
3359 A sanitized hashtag can be used as a git push refspec parameter value.
3360 """
3361 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3362
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003363 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3364 """Updates this commit description given the parent.
3365
3366 This is essentially what Gnumbd used to do.
3367 Consult https://goo.gl/WMmpDe for more details.
3368 """
3369 assert parent_msg # No, orphan branch creation isn't supported.
3370 assert parent_hash
3371 assert dest_ref
3372 parent_footer_map = git_footers.parse_footers(parent_msg)
3373 # This will also happily parse svn-position, which GnumbD is no longer
3374 # supporting. While we'd generate correct footers, the verifier plugin
3375 # installed in Gerrit will block such commit (ie git push below will fail).
3376 parent_position = git_footers.get_position(parent_footer_map)
3377
3378 # Cherry-picks may have last line obscuring their prior footers,
3379 # from git_footers perspective. This is also what Gnumbd did.
3380 cp_line = None
3381 if (self._description_lines and
3382 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3383 cp_line = self._description_lines.pop()
3384
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003385 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003386
3387 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3388 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003389 for i, line in enumerate(footer_lines):
3390 k, v = git_footers.parse_footer(line) or (None, None)
3391 if k and k.startswith('Cr-'):
3392 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003393
3394 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003395 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003396 if parent_position[0] == dest_ref:
3397 # Same branch as parent.
3398 number = int(parent_position[1]) + 1
3399 else:
3400 number = 1 # New branch, and extra lineage.
3401 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3402 int(parent_position[1])))
3403
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003404 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3405 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003406
3407 self._description_lines = top_lines
3408 if cp_line:
3409 self._description_lines.append(cp_line)
3410 if self._description_lines[-1] != '':
3411 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003412 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003413
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003414
Aaron Gablea1bab272017-04-11 16:38:18 -07003415def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003416 """Retrieves the reviewers that approved a CL from the issue properties with
3417 messages.
3418
3419 Note that the list may contain reviewers that are not committer, thus are not
3420 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003421
3422 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003423 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003424 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003425 return sorted(
3426 set(
3427 message['sender']
3428 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003429 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003430 )
3431 )
3432
3433
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003434def FindCodereviewSettingsFile(filename='codereview.settings'):
3435 """Finds the given file starting in the cwd and going up.
3436
3437 Only looks up to the top of the repository unless an
3438 'inherit-review-settings-ok' file exists in the root of the repository.
3439 """
3440 inherit_ok_file = 'inherit-review-settings-ok'
3441 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003442 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003443 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3444 root = '/'
3445 while True:
3446 if filename in os.listdir(cwd):
3447 if os.path.isfile(os.path.join(cwd, filename)):
3448 return open(os.path.join(cwd, filename))
3449 if cwd == root:
3450 break
3451 cwd = os.path.dirname(cwd)
3452
3453
3454def LoadCodereviewSettingsFromFile(fileobj):
3455 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003456 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003457
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003458 def SetProperty(name, setting, unset_error_ok=False):
3459 fullname = 'rietveld.' + name
3460 if setting in keyvals:
3461 RunGit(['config', fullname, keyvals[setting]])
3462 else:
3463 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3464
tandrii48df5812016-10-17 03:55:37 -07003465 if not keyvals.get('GERRIT_HOST', False):
3466 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003467 # Only server setting is required. Other settings can be absent.
3468 # In that case, we ignore errors raised during option deletion attempt.
3469 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3470 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3471 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003472 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003473 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3474 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003475 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3476 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003477
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003478 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003479 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003480
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003481 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003482 RunGit(['config', 'gerrit.squash-uploads',
3483 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003484
tandrii@chromium.org28253532016-04-14 13:46:56 +00003485 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003486 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003487 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3488
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003489 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003490 # should be of the form
3491 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3492 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003493 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3494 keyvals['ORIGIN_URL_CONFIG']])
3495
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003496
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003497def urlretrieve(source, destination):
3498 """urllib is broken for SSL connections via a proxy therefore we
3499 can't use urllib.urlretrieve()."""
3500 with open(destination, 'w') as f:
3501 f.write(urllib2.urlopen(source).read())
3502
3503
ukai@chromium.org712d6102013-11-27 00:52:58 +00003504def hasSheBang(fname):
3505 """Checks fname is a #! script."""
3506 with open(fname) as f:
3507 return f.read(2).startswith('#!')
3508
3509
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003510# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3511def DownloadHooks(*args, **kwargs):
3512 pass
3513
3514
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003515def DownloadGerritHook(force):
3516 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003517
3518 Args:
3519 force: True to update hooks. False to install hooks if not present.
3520 """
3521 if not settings.GetIsGerrit():
3522 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003523 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003524 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3525 if not os.access(dst, os.X_OK):
3526 if os.path.exists(dst):
3527 if not force:
3528 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003529 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003530 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003531 if not hasSheBang(dst):
3532 DieWithError('Not a script: %s\n'
3533 'You need to download from\n%s\n'
3534 'into .git/hooks/commit-msg and '
3535 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003536 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3537 except Exception:
3538 if os.path.exists(dst):
3539 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003540 DieWithError('\nFailed to download hooks.\n'
3541 'You need to download from\n%s\n'
3542 'into .git/hooks/commit-msg and '
3543 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003544
3545
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003546class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003547 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003548
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003549 _GOOGLESOURCE = 'googlesource.com'
3550
3551 def __init__(self):
3552 # Cached list of [host, identity, source], where source is either
3553 # .gitcookies or .netrc.
3554 self._all_hosts = None
3555
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003556 def ensure_configured_gitcookies(self):
3557 """Runs checks and suggests fixes to make git use .gitcookies from default
3558 path."""
3559 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3560 configured_path = RunGitSilent(
3561 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003562 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003563 if configured_path:
3564 self._ensure_default_gitcookies_path(configured_path, default)
3565 else:
3566 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003567
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003568 @staticmethod
3569 def _ensure_default_gitcookies_path(configured_path, default_path):
3570 assert configured_path
3571 if configured_path == default_path:
3572 print('git is already configured to use your .gitcookies from %s' %
3573 configured_path)
3574 return
3575
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003576 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003577 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3578 (configured_path, default_path))
3579
3580 if not os.path.exists(configured_path):
3581 print('However, your configured .gitcookies file is missing.')
3582 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3583 action='reconfigure')
3584 RunGit(['config', '--global', 'http.cookiefile', default_path])
3585 return
3586
3587 if os.path.exists(default_path):
3588 print('WARNING: default .gitcookies file already exists %s' %
3589 default_path)
3590 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3591 default_path)
3592
3593 confirm_or_exit('Move existing .gitcookies to default location?',
3594 action='move')
3595 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003596 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003597 print('Moved and reconfigured git to use .gitcookies from %s' %
3598 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003599
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003600 @staticmethod
3601 def _configure_gitcookies_path(default_path):
3602 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3603 if os.path.exists(netrc_path):
3604 print('You seem to be using outdated .netrc for git credentials: %s' %
3605 netrc_path)
3606 print('This tool will guide you through setting up recommended '
3607 '.gitcookies store for git credentials.\n'
3608 '\n'
3609 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3610 ' git config --global --unset http.cookiefile\n'
3611 ' mv %s %s.backup\n\n' % (default_path, default_path))
3612 confirm_or_exit(action='setup .gitcookies')
3613 RunGit(['config', '--global', 'http.cookiefile', default_path])
3614 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003615
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003616 def get_hosts_with_creds(self, include_netrc=False):
3617 if self._all_hosts is None:
3618 a = gerrit_util.CookiesAuthenticator()
3619 self._all_hosts = [
3620 (h, u, s)
3621 for h, u, s in itertools.chain(
3622 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3623 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3624 )
3625 if h.endswith(self._GOOGLESOURCE)
3626 ]
3627
3628 if include_netrc:
3629 return self._all_hosts
3630 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3631
3632 def print_current_creds(self, include_netrc=False):
3633 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3634 if not hosts:
3635 print('No Git/Gerrit credentials found')
3636 return
3637 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3638 header = [('Host', 'User', 'Which file'),
3639 ['=' * l for l in lengths]]
3640 for row in (header + hosts):
3641 print('\t'.join((('%%+%ds' % l) % s)
3642 for l, s in zip(lengths, row)))
3643
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003644 @staticmethod
3645 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003646 """Parses identity "git-<username>.domain" into <username> and domain."""
3647 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003648 # distinguishable from sub-domains. But we do know typical domains:
3649 if identity.endswith('.chromium.org'):
3650 domain = 'chromium.org'
3651 username = identity[:-len('.chromium.org')]
3652 else:
3653 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003654 if username.startswith('git-'):
3655 username = username[len('git-'):]
3656 return username, domain
3657
3658 def _get_usernames_of_domain(self, domain):
3659 """Returns list of usernames referenced by .gitcookies in a given domain."""
3660 identities_by_domain = {}
3661 for _, identity, _ in self.get_hosts_with_creds():
3662 username, domain = self._parse_identity(identity)
3663 identities_by_domain.setdefault(domain, []).append(username)
3664 return identities_by_domain.get(domain)
3665
3666 def _canonical_git_googlesource_host(self, host):
3667 """Normalizes Gerrit hosts (with '-review') to Git host."""
3668 assert host.endswith(self._GOOGLESOURCE)
3669 # Prefix doesn't include '.' at the end.
3670 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3671 if prefix.endswith('-review'):
3672 prefix = prefix[:-len('-review')]
3673 return prefix + '.' + self._GOOGLESOURCE
3674
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003675 def _canonical_gerrit_googlesource_host(self, host):
3676 git_host = self._canonical_git_googlesource_host(host)
3677 prefix = git_host.split('.', 1)[0]
3678 return prefix + '-review.' + self._GOOGLESOURCE
3679
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003680 def _get_counterpart_host(self, host):
3681 assert host.endswith(self._GOOGLESOURCE)
3682 git = self._canonical_git_googlesource_host(host)
3683 gerrit = self._canonical_gerrit_googlesource_host(git)
3684 return git if gerrit == host else gerrit
3685
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003686 def has_generic_host(self):
3687 """Returns whether generic .googlesource.com has been configured.
3688
3689 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3690 """
3691 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3692 if host == '.' + self._GOOGLESOURCE:
3693 return True
3694 return False
3695
3696 def _get_git_gerrit_identity_pairs(self):
3697 """Returns map from canonic host to pair of identities (Git, Gerrit).
3698
3699 One of identities might be None, meaning not configured.
3700 """
3701 host_to_identity_pairs = {}
3702 for host, identity, _ in self.get_hosts_with_creds():
3703 canonical = self._canonical_git_googlesource_host(host)
3704 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3705 idx = 0 if canonical == host else 1
3706 pair[idx] = identity
3707 return host_to_identity_pairs
3708
3709 def get_partially_configured_hosts(self):
3710 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003711 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3712 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3713 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003714
3715 def get_conflicting_hosts(self):
3716 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003717 host
3718 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003719 if None not in (i1, i2) and i1 != i2)
3720
3721 def get_duplicated_hosts(self):
3722 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3723 return set(host for host, count in counters.iteritems() if count > 1)
3724
3725 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3726 'chromium.googlesource.com': 'chromium.org',
3727 'chrome-internal.googlesource.com': 'google.com',
3728 }
3729
3730 def get_hosts_with_wrong_identities(self):
3731 """Finds hosts which **likely** reference wrong identities.
3732
3733 Note: skips hosts which have conflicting identities for Git and Gerrit.
3734 """
3735 hosts = set()
3736 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3737 pair = self._get_git_gerrit_identity_pairs().get(host)
3738 if pair and pair[0] == pair[1]:
3739 _, domain = self._parse_identity(pair[0])
3740 if domain != expected:
3741 hosts.add(host)
3742 return hosts
3743
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003744 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003745 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003746 hosts = sorted(hosts)
3747 assert hosts
3748 if extra_column_func is None:
3749 extras = [''] * len(hosts)
3750 else:
3751 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003752 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3753 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003754 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003755 lines.append(tmpl % he)
3756 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003757
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003758 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003759 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003760 yield ('.googlesource.com wildcard record detected',
3761 ['Chrome Infrastructure team recommends to list full host names '
3762 'explicitly.'],
3763 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003764
3765 dups = self.get_duplicated_hosts()
3766 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003767 yield ('The following hosts were defined twice',
3768 self._format_hosts(dups),
3769 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003770
3771 partial = self.get_partially_configured_hosts()
3772 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003773 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3774 'These hosts are missing',
3775 self._format_hosts(partial, lambda host: 'but %s defined' %
3776 self._get_counterpart_host(host)),
3777 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003778
3779 conflicting = self.get_conflicting_hosts()
3780 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003781 yield ('The following Git hosts have differing credentials from their '
3782 'Gerrit counterparts',
3783 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3784 tuple(self._get_git_gerrit_identity_pairs()[host])),
3785 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003786
3787 wrong = self.get_hosts_with_wrong_identities()
3788 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003789 yield ('These hosts likely use wrong identity',
3790 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3791 (self._get_git_gerrit_identity_pairs()[host][0],
3792 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3793 wrong)
3794
3795 def find_and_report_problems(self):
3796 """Returns True if there was at least one problem, else False."""
3797 found = False
3798 bad_hosts = set()
3799 for title, sublines, hosts in self._find_problems():
3800 if not found:
3801 found = True
3802 print('\n\n.gitcookies problem report:\n')
3803 bad_hosts.update(hosts or [])
3804 print(' %s%s' % (title , (':' if sublines else '')))
3805 if sublines:
3806 print()
3807 print(' %s' % '\n '.join(sublines))
3808 print()
3809
3810 if bad_hosts:
3811 assert found
3812 print(' You can manually remove corresponding lines in your %s file and '
3813 'visit the following URLs with correct account to generate '
3814 'correct credential lines:\n' %
3815 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3816 print(' %s' % '\n '.join(sorted(set(
3817 gerrit_util.CookiesAuthenticator().get_new_password_url(
3818 self._canonical_git_googlesource_host(host))
3819 for host in bad_hosts
3820 ))))
3821 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003822
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003823
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003824@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003825def CMDcreds_check(parser, args):
3826 """Checks credentials and suggests changes."""
3827 _, _ = parser.parse_args(args)
3828
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003829 # Code below checks .gitcookies. Abort if using something else.
3830 authn = gerrit_util.Authenticator.get()
3831 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3832 if isinstance(authn, gerrit_util.GceAuthenticator):
3833 DieWithError(
3834 'This command is not designed for GCE, are you on a bot?\n'
3835 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3836 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003837 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003838 'This command is not designed for bot environment. It checks '
3839 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003840
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003841 checker = _GitCookiesChecker()
3842 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003843
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003844 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003845 checker.print_current_creds(include_netrc=True)
3846
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003847 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003848 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003849 return 0
3850 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003851
3852
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003853@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003854def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003855 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003856 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3857 branch = ShortBranchName(branchref)
3858 _, args = parser.parse_args(args)
3859 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003860 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003861 return RunGit(['config', 'branch.%s.base-url' % branch],
3862 error_ok=False).strip()
3863 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003864 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003865 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3866 error_ok=False).strip()
3867
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003868def color_for_status(status):
3869 """Maps a Changelist status to color, for CMDstatus and other tools."""
3870 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003871 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003872 'waiting': Fore.BLUE,
3873 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003874 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003875 'lgtm': Fore.GREEN,
3876 'commit': Fore.MAGENTA,
3877 'closed': Fore.CYAN,
3878 'error': Fore.WHITE,
3879 }.get(status, Fore.WHITE)
3880
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003881
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003882def get_cl_statuses(changes, fine_grained, max_processes=None):
3883 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003884
3885 If fine_grained is true, this will fetch CL statuses from the server.
3886 Otherwise, simply indicate if there's a matching url for the given branches.
3887
3888 If max_processes is specified, it is used as the maximum number of processes
3889 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3890 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003891
3892 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003893 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003894 if not changes:
3895 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003896
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003897 if not fine_grained:
3898 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003899 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003900 for cl in changes:
3901 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003902 return
3903
3904 # First, sort out authentication issues.
3905 logging.debug('ensuring credentials exist')
3906 for cl in changes:
3907 cl.EnsureAuthenticated(force=False, refresh=True)
3908
3909 def fetch(cl):
3910 try:
3911 return (cl, cl.GetStatus())
3912 except:
3913 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003914 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003915 raise
3916
3917 threads_count = len(changes)
3918 if max_processes:
3919 threads_count = max(1, min(threads_count, max_processes))
3920 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3921
3922 pool = ThreadPool(threads_count)
3923 fetched_cls = set()
3924 try:
3925 it = pool.imap_unordered(fetch, changes).__iter__()
3926 while True:
3927 try:
3928 cl, status = it.next(timeout=5)
3929 except multiprocessing.TimeoutError:
3930 break
3931 fetched_cls.add(cl)
3932 yield cl, status
3933 finally:
3934 pool.close()
3935
3936 # Add any branches that failed to fetch.
3937 for cl in set(changes) - fetched_cls:
3938 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003939
rmistry@google.com2dd99862015-06-22 12:22:18 +00003940
3941def upload_branch_deps(cl, args):
3942 """Uploads CLs of local branches that are dependents of the current branch.
3943
3944 If the local branch dependency tree looks like:
3945 test1 -> test2.1 -> test3.1
3946 -> test3.2
3947 -> test2.2 -> test3.3
3948
3949 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3950 run on the dependent branches in this order:
3951 test2.1, test3.1, test3.2, test2.2, test3.3
3952
3953 Note: This function does not rebase your local dependent branches. Use it when
3954 you make a change to the parent branch that will not conflict with its
3955 dependent branches, and you would like their dependencies updated in
3956 Rietveld.
3957 """
3958 if git_common.is_dirty_git_tree('upload-branch-deps'):
3959 return 1
3960
3961 root_branch = cl.GetBranch()
3962 if root_branch is None:
3963 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3964 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003965 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003966 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3967 'patchset dependencies without an uploaded CL.')
3968
3969 branches = RunGit(['for-each-ref',
3970 '--format=%(refname:short) %(upstream:short)',
3971 'refs/heads'])
3972 if not branches:
3973 print('No local branches found.')
3974 return 0
3975
3976 # Create a dictionary of all local branches to the branches that are dependent
3977 # on it.
3978 tracked_to_dependents = collections.defaultdict(list)
3979 for b in branches.splitlines():
3980 tokens = b.split()
3981 if len(tokens) == 2:
3982 branch_name, tracked = tokens
3983 tracked_to_dependents[tracked].append(branch_name)
3984
vapiera7fbd5a2016-06-16 09:17:49 -07003985 print()
3986 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003987 dependents = []
3988 def traverse_dependents_preorder(branch, padding=''):
3989 dependents_to_process = tracked_to_dependents.get(branch, [])
3990 padding += ' '
3991 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003992 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003993 dependents.append(dependent)
3994 traverse_dependents_preorder(dependent, padding)
3995 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003996 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003997
3998 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003999 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004000 return 0
4001
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004002 confirm_or_exit('This command will checkout all dependent branches and run '
4003 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004004
rmistry@google.com2dd99862015-06-22 12:22:18 +00004005 # Record all dependents that failed to upload.
4006 failures = {}
4007 # Go through all dependents, checkout the branch and upload.
4008 try:
4009 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004010 print()
4011 print('--------------------------------------')
4012 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004013 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004014 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004015 try:
4016 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004017 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004018 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004019 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004020 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004021 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004022 finally:
4023 # Swap back to the original root branch.
4024 RunGit(['checkout', '-q', root_branch])
4025
vapiera7fbd5a2016-06-16 09:17:49 -07004026 print()
4027 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004028 for dependent_branch in dependents:
4029 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004030 print(' %s : %s' % (dependent_branch, upload_status))
4031 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004032
4033 return 0
4034
4035
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004036@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004037def CMDarchive(parser, args):
4038 """Archives and deletes branches associated with closed changelists."""
4039 parser.add_option(
4040 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004041 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004042 parser.add_option(
4043 '-f', '--force', action='store_true',
4044 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004045 parser.add_option(
4046 '-d', '--dry-run', action='store_true',
4047 help='Skip the branch tagging and removal steps.')
4048 parser.add_option(
4049 '-t', '--notags', action='store_true',
4050 help='Do not tag archived branches. '
4051 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004052
4053 auth.add_auth_options(parser)
4054 options, args = parser.parse_args(args)
4055 if args:
4056 parser.error('Unsupported args: %s' % ' '.join(args))
4057 auth_config = auth.extract_auth_config_from_options(options)
4058
4059 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4060 if not branches:
4061 return 0
4062
vapiera7fbd5a2016-06-16 09:17:49 -07004063 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004064 changes = [Changelist(branchref=b, auth_config=auth_config)
4065 for b in branches.splitlines()]
4066 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4067 statuses = get_cl_statuses(changes,
4068 fine_grained=True,
4069 max_processes=options.maxjobs)
4070 proposal = [(cl.GetBranch(),
4071 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4072 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00004073 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07004074 proposal.sort()
4075
4076 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004077 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004078 return 0
4079
4080 current_branch = GetCurrentBranch()
4081
vapiera7fbd5a2016-06-16 09:17:49 -07004082 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004083 if options.notags:
4084 for next_item in proposal:
4085 print(' ' + next_item[0])
4086 else:
4087 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4088 for next_item in proposal:
4089 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004090
kmarshall9249e012016-08-23 12:02:16 -07004091 # Quit now on precondition failure or if instructed by the user, either
4092 # via an interactive prompt or by command line flags.
4093 if options.dry_run:
4094 print('\nNo changes were made (dry run).\n')
4095 return 0
4096 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004097 print('You are currently on a branch \'%s\' which is associated with a '
4098 'closed codereview issue, so archive cannot proceed. Please '
4099 'checkout another branch and run this command again.' %
4100 current_branch)
4101 return 1
kmarshall9249e012016-08-23 12:02:16 -07004102 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004103 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4104 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004105 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004106 return 1
4107
4108 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004109 if not options.notags:
4110 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004111 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004112
vapiera7fbd5a2016-06-16 09:17:49 -07004113 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004114
4115 return 0
4116
4117
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004118@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004119def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004120 """Show status of changelists.
4121
4122 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004123 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004124 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004125 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004126 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004127 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004128 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004129 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004130
4131 Also see 'git cl comments'.
4132 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00004133 parser.add_option(
4134 '--no-branch-color',
4135 action='store_true',
4136 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004137 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004138 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004139 parser.add_option('-f', '--fast', action='store_true',
4140 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004141 parser.add_option(
4142 '-j', '--maxjobs', action='store', type=int,
4143 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004144
4145 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004146 _add_codereview_issue_select_options(
4147 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004148 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004149 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004150 if args:
4151 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004152 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004153
iannuccie53c9352016-08-17 14:40:40 -07004154 if options.issue is not None and not options.field:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004155 parser.error('--field must be specified with --issue.')
iannucci3c972b92016-08-17 13:24:10 -07004156
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004157 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004158 cl = Changelist(auth_config=auth_config, issue=options.issue,
4159 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004160 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004161 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004162 elif options.field == 'id':
4163 issueid = cl.GetIssue()
4164 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004165 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004166 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004167 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004168 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004169 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004170 elif options.field == 'status':
4171 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004172 elif options.field == 'url':
4173 url = cl.GetIssueURL()
4174 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004175 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004176 return 0
4177
4178 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4179 if not branches:
4180 print('No local branch found.')
4181 return 0
4182
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004183 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004184 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004185 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004186 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004187 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004188 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004189 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004190
Daniel McArdlea23bf592019-02-12 00:25:12 +00004191 current_branch = GetCurrentBranch()
4192
4193 def FormatBranchName(branch, colorize=False):
4194 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
4195 an asterisk when it is the current branch."""
4196
4197 asterisk = ""
4198 color = Fore.RESET
4199 if branch == current_branch:
4200 asterisk = "* "
4201 color = Fore.GREEN
4202 branch_name = ShortBranchName(branch)
4203
4204 if colorize:
4205 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00004206 return asterisk + branch_name
4207
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004208 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00004209
4210 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004211 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4212 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004213 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004214 c, status = output.next()
4215 branch_statuses[c.GetBranch()] = status
4216 status = branch_statuses.pop(branch)
4217 url = cl.GetIssueURL()
4218 if url and (not status or status == 'error'):
4219 # The issue probably doesn't exist anymore.
4220 url += ' (broken)'
4221
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004222 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004223 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004224 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004225 color = ''
4226 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004227 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00004228
Alan Cuttera3be9a52019-03-04 18:50:33 +00004229 branch_display = FormatBranchName(branch)
4230 padding = ' ' * (alignment - len(branch_display))
4231 if not options.no_branch_color:
4232 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00004233
Alan Cuttera3be9a52019-03-04 18:50:33 +00004234 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
4235 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004236
vapiera7fbd5a2016-06-16 09:17:49 -07004237 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00004238 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004239 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00004240 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004241 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004242 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004243 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004244 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004245 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004246 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004247 print('Issue description:')
4248 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004249 return 0
4250
4251
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004252def colorize_CMDstatus_doc():
4253 """To be called once in main() to add colors to git cl status help."""
4254 colors = [i for i in dir(Fore) if i[0].isupper()]
4255
4256 def colorize_line(line):
4257 for color in colors:
4258 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004259 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004260 indent = len(line) - len(line.lstrip(' ')) + 1
4261 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4262 return line
4263
4264 lines = CMDstatus.__doc__.splitlines()
4265 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4266
4267
phajdan.jre328cf92016-08-22 04:12:17 -07004268def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004269 if path == '-':
4270 json.dump(contents, sys.stdout)
4271 else:
4272 with open(path, 'w') as f:
4273 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004274
4275
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004276@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004277@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004278def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004279 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004280
4281 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004282 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004283 parser.add_option('-r', '--reverse', action='store_true',
4284 help='Lookup the branch(es) for the specified issues. If '
4285 'no issues are specified, all branches with mapped '
4286 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004287 parser.add_option('--json',
4288 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004289 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004290 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004291 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004292
dnj@chromium.org406c4402015-03-03 17:22:28 +00004293 if options.reverse:
4294 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004295 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004296 # Reverse issue lookup.
4297 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004298
4299 git_config = {}
4300 for config in RunGit(['config', '--get-regexp',
4301 r'branch\..*issue']).splitlines():
4302 name, _space, val = config.partition(' ')
4303 git_config[name] = val
4304
dnj@chromium.org406c4402015-03-03 17:22:28 +00004305 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004306 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4307 config_key = _git_branch_config_key(ShortBranchName(branch),
4308 cls.IssueConfigKey())
4309 issue = git_config.get(config_key)
4310 if issue:
4311 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004312 if not args:
4313 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004314 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004315 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00004316 try:
4317 issue_num = int(issue)
4318 except ValueError:
4319 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004320 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00004321 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07004322 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00004323 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004324 if options.json:
4325 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004326 return 0
4327
4328 if len(args) > 0:
4329 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4330 if not issue.valid:
4331 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4332 'or no argument to list it.\n'
4333 'Maybe you want to run git cl status?')
4334 cl = Changelist(codereview=issue.codereview)
4335 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004336 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004337 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004338 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4339 if options.json:
4340 write_json(options.json, {
4341 'issue': cl.GetIssue(),
4342 'issue_url': cl.GetIssueURL(),
4343 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004344 return 0
4345
4346
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004347@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004348def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004349 """Shows or posts review comments for any changelist."""
4350 parser.add_option('-a', '--add-comment', dest='comment',
4351 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004352 parser.add_option('-p', '--publish', action='store_true',
4353 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004354 parser.add_option('-i', '--issue', dest='issue',
4355 help='review issue id (defaults to current issue). '
4356 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004357 parser.add_option('-m', '--machine-readable', dest='readable',
4358 action='store_false', default=True,
4359 help='output comments in a format compatible with '
4360 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004361 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004362 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004363 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004364 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004365 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004366 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004367 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004368
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004369 issue = None
4370 if options.issue:
4371 try:
4372 issue = int(options.issue)
4373 except ValueError:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004374 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004375
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00004376 cl = Changelist(issue=issue, codereview='gerrit', auth_config=auth_config)
4377
4378 if not cl.IsGerrit():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004379 parser.error('Rietveld is not supported.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004380
4381 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004382 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004383 return 0
4384
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004385 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4386 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004387 for comment in summary:
4388 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004389 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004390 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004391 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004392 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004393 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00004394 elif comment.autogenerated:
4395 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004396 else:
4397 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004398 print('\n%s%s %s%s\n%s' % (
4399 color,
4400 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4401 comment.sender,
4402 Fore.RESET,
4403 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4404
smut@google.comc85ac942015-09-15 16:34:43 +00004405 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004406 def pre_serialize(c):
4407 dct = c.__dict__.copy()
4408 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4409 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004410 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004411 return 0
4412
4413
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004414@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004415@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004416def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004417 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004418 parser.add_option('-d', '--display', action='store_true',
4419 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004420 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004421 help='New description to set for this issue (- for stdin, '
4422 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004423 parser.add_option('-f', '--force', action='store_true',
4424 help='Delete any unpublished Gerrit edits for this issue '
4425 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004426
4427 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004428 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004429 options, args = parser.parse_args(args)
4430 _process_codereview_select_options(parser, options)
4431
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004432 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004433 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004434 target_issue_arg = ParseIssueNumberArgument(args[0],
4435 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004436 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004437 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004438
martiniss6eda05f2016-06-30 10:18:35 -07004439 kwargs = {
Andrii Shyshkalovdd672fb2018-10-16 06:09:51 +00004440 'auth_config': auth.extract_auth_config_from_options(options),
4441 'codereview': options.forced_codereview,
martiniss6eda05f2016-06-30 10:18:35 -07004442 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004443 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004444 if target_issue_arg:
4445 kwargs['issue'] = target_issue_arg.issue
4446 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004447 if target_issue_arg.codereview and not options.forced_codereview:
4448 detected_codereview_from_url = True
4449 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004450
4451 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004452 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004453 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004454 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004455
4456 if detected_codereview_from_url:
4457 logging.info('canonical issue/change URL: %s (type: %s)\n',
4458 cl.GetIssueURL(), target_issue_arg.codereview)
4459
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004460 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004461
smut@google.com34fb6b12015-07-13 20:03:26 +00004462 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004463 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004464 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004465
4466 if options.new_description:
4467 text = options.new_description
4468 if text == '-':
4469 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004470 elif text == '+':
4471 base_branch = cl.GetCommonAncestorWithUpstream()
4472 change = cl.GetChange(base_branch, None, local_description=True)
4473 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004474
4475 description.set_description(text)
4476 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004477 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004478
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004479 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004480 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004481 return 0
4482
4483
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004484@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004485def CMDlint(parser, args):
4486 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004487 parser.add_option('--filter', action='append', metavar='-x,+y',
4488 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004489 auth.add_auth_options(parser)
4490 options, args = parser.parse_args(args)
4491 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004492
4493 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004494 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004495 try:
4496 import cpplint
4497 import cpplint_chromium
4498 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004499 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004500 return 1
4501
4502 # Change the current working directory before calling lint so that it
4503 # shows the correct base.
4504 previous_cwd = os.getcwd()
4505 os.chdir(settings.GetRoot())
4506 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004507 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004508 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4509 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004510 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004511 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004512 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004513
4514 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004515 command = args + files
4516 if options.filter:
4517 command = ['--filter=' + ','.join(options.filter)] + command
4518 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004519
4520 white_regex = re.compile(settings.GetLintRegex())
4521 black_regex = re.compile(settings.GetLintIgnoreRegex())
4522 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4523 for filename in filenames:
4524 if white_regex.match(filename):
4525 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004526 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004527 else:
4528 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4529 extra_check_functions)
4530 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004531 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004532 finally:
4533 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004534 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004535 if cpplint._cpplint_state.error_count != 0:
4536 return 1
4537 return 0
4538
4539
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004540@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004541def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004542 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004543 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004544 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004545 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004546 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004547 parser.add_option('--all', action='store_true',
4548 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004549 parser.add_option('--parallel', action='store_true',
4550 help='Run all tests specified by input_api.RunTests in all '
4551 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004552 auth.add_auth_options(parser)
4553 options, args = parser.parse_args(args)
4554 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004555
sbc@chromium.org71437c02015-04-09 19:29:40 +00004556 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004557 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004558 return 1
4559
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004560 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004561 if args:
4562 base_branch = args[0]
4563 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004564 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004565 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004566
Aaron Gable8076c282017-11-29 14:39:41 -08004567 if options.all:
4568 base_change = cl.GetChange(base_branch, None)
4569 files = [('M', f) for f in base_change.AllFiles()]
4570 change = presubmit_support.GitChange(
4571 base_change.Name(),
4572 base_change.FullDescriptionText(),
4573 base_change.RepositoryRoot(),
4574 files,
4575 base_change.issue,
4576 base_change.patchset,
4577 base_change.author_email,
4578 base_change._upstream)
4579 else:
4580 change = cl.GetChange(base_branch, None)
4581
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004582 cl.RunHook(
4583 committing=not options.upload,
4584 may_prompt=False,
4585 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004586 change=change,
4587 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004588 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004589
4590
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004591def GenerateGerritChangeId(message):
4592 """Returns Ixxxxxx...xxx change id.
4593
4594 Works the same way as
4595 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4596 but can be called on demand on all platforms.
4597
4598 The basic idea is to generate git hash of a state of the tree, original commit
4599 message, author/committer info and timestamps.
4600 """
4601 lines = []
4602 tree_hash = RunGitSilent(['write-tree'])
4603 lines.append('tree %s' % tree_hash.strip())
4604 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4605 if code == 0:
4606 lines.append('parent %s' % parent.strip())
4607 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4608 lines.append('author %s' % author.strip())
4609 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4610 lines.append('committer %s' % committer.strip())
4611 lines.append('')
4612 # Note: Gerrit's commit-hook actually cleans message of some lines and
4613 # whitespace. This code is not doing this, but it clearly won't decrease
4614 # entropy.
4615 lines.append(message)
4616 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00004617 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004618 return 'I%s' % change_hash.strip()
4619
4620
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004621def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004622 """Computes the remote branch ref to use for the CL.
4623
4624 Args:
4625 remote (str): The git remote for the CL.
4626 remote_branch (str): The git remote branch for the CL.
4627 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004628 """
4629 if not (remote and remote_branch):
4630 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004631
wittman@chromium.org455dc922015-01-26 20:15:50 +00004632 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004633 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004634 # refs, which are then translated into the remote full symbolic refs
4635 # below.
4636 if '/' not in target_branch:
4637 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4638 else:
4639 prefix_replacements = (
4640 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4641 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4642 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4643 )
4644 match = None
4645 for regex, replacement in prefix_replacements:
4646 match = re.search(regex, target_branch)
4647 if match:
4648 remote_branch = target_branch.replace(match.group(0), replacement)
4649 break
4650 if not match:
4651 # This is a branch path but not one we recognize; use as-is.
4652 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004653 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4654 # Handle the refs that need to land in different refs.
4655 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004656
wittman@chromium.org455dc922015-01-26 20:15:50 +00004657 # Create the true path to the remote branch.
4658 # Does the following translation:
4659 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4660 # * refs/remotes/origin/master -> refs/heads/master
4661 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4662 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4663 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4664 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4665 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4666 'refs/heads/')
4667 elif remote_branch.startswith('refs/remotes/branch-heads'):
4668 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004669
wittman@chromium.org455dc922015-01-26 20:15:50 +00004670 return remote_branch
4671
4672
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004673def cleanup_list(l):
4674 """Fixes a list so that comma separated items are put as individual items.
4675
4676 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4677 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4678 """
4679 items = sum((i.split(',') for i in l), [])
4680 stripped_items = (i.strip() for i in items)
4681 return sorted(filter(None, stripped_items))
4682
4683
Aaron Gable4db38df2017-11-03 14:59:07 -07004684@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004685@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004686def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004687 """Uploads the current changelist to codereview.
4688
4689 Can skip dependency patchset uploads for a branch by running:
4690 git config branch.branch_name.skip-deps-uploads True
4691 To unset run:
4692 git config --unset branch.branch_name.skip-deps-uploads
4693 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004694
4695 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4696 a bug number, this bug number is automatically populated in the CL
4697 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004698
4699 If subject contains text in square brackets or has "<text>: " prefix, such
4700 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4701 [git-cl] add support for hashtags
4702 Foo bar: implement foo
4703 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004704 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004705 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4706 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004707 parser.add_option('--bypass-watchlists', action='store_true',
4708 dest='bypass_watchlists',
4709 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004710 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004711 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004712 parser.add_option('--message', '-m', dest='message',
4713 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004714 parser.add_option('-b', '--bug',
4715 help='pre-populate the bug number(s) for this issue. '
4716 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004717 parser.add_option('--message-file', dest='message_file',
4718 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004719 parser.add_option('--title', '-t', dest='title',
4720 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004721 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004722 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004723 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004724 parser.add_option('--tbrs',
4725 action='append', default=[],
4726 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004727 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004728 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004729 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004730 parser.add_option('--hashtag', dest='hashtags',
4731 action='append', default=[],
4732 help=('Gerrit hashtag for new CL; '
4733 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004734 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004735 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004736 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004737 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004738 metavar='TARGET',
4739 help='Apply CL to remote ref TARGET. ' +
4740 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004741 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004742 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004743 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004744 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004745 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004746 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004747 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4748 const='TBR', help='add a set of OWNERS to TBR')
4749 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4750 const='R', help='add a set of OWNERS to R')
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004751 parser.add_option('-c', '--use-commit-queue', action='store_true',
4752 help='tell the CQ to commit this patchset; '
4753 'implies --send-mail')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004754 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4755 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004756 help='Send the patchset to do a CQ dry run right after '
4757 'upload.')
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00004758 parser.add_option('--preserve-tryjobs', action='store_true',
4759 help='instruct the CQ to let tryjobs running even after '
4760 'new patchsets are uploaded instead of canceling '
4761 'prior patchset\' tryjobs')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004762 parser.add_option('--dependencies', action='store_true',
4763 help='Uploads CLs of all the local branches that depend on '
4764 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004765 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4766 help='Sends your change to the CQ after an approval. Only '
4767 'works on repos that have the Auto-Submit label '
4768 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004769 parser.add_option('--parallel', action='store_true',
4770 help='Run all tests specified by input_api.RunTests in all '
4771 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004772
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004773 parser.add_option('--no-autocc', action='store_true',
4774 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004775 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004776 help='Set the review private. This implies --no-autocc.')
4777
rmistry@google.com2dd99862015-06-22 12:22:18 +00004778 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004779 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004780 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004781 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004782 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004783 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004784
sbc@chromium.org71437c02015-04-09 19:29:40 +00004785 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004786 return 1
4787
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004788 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004789 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004790 options.cc = cleanup_list(options.cc)
4791
tandriib80458a2016-06-23 12:20:07 -07004792 if options.message_file:
4793 if options.message:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004794 parser.error('Only one of --message and --message-file allowed.')
tandriib80458a2016-06-23 12:20:07 -07004795 options.message = gclient_utils.FileRead(options.message_file)
4796 options.message_file = None
4797
tandrii4d0545a2016-07-06 03:56:49 -07004798 if options.cq_dry_run and options.use_commit_queue:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004799 parser.error('Only one of --use-commit-queue and --cq-dry-run allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07004800
Aaron Gableedbc4132017-09-11 13:22:28 -07004801 if options.use_commit_queue:
4802 options.send_mail = True
4803
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004804 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4805 settings.GetIsGerrit()
4806
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004807 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004808 if not cl.IsGerrit():
4809 # Error out with instructions for repos not yet configured for Gerrit.
4810 print('=====================================')
4811 print('NOTICE: Rietveld is no longer supported. '
4812 'You can upload changes to Gerrit with')
4813 print(' git cl upload --gerrit')
4814 print('or set Gerrit to be your default code review tool with')
4815 print(' git config gerrit.host true')
4816 print('=====================================')
4817 return 1
4818
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004819 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004820
4821
Francois Dorayd42c6812017-05-30 15:10:20 -04004822@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004823@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004824def CMDsplit(parser, args):
4825 """Splits a branch into smaller branches and uploads CLs.
4826
4827 Creates a branch and uploads a CL for each group of files modified in the
4828 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004829 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004830 the shared OWNERS file.
4831 """
4832 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05004833 help="A text file containing a CL description in which "
4834 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004835 parser.add_option("-c", "--comment", dest="comment_file",
4836 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11004837 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
4838 default=False,
4839 help="List the files and reviewers for each CL that would "
4840 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00004841 parser.add_option("--cq-dry-run", action='store_true',
4842 help="If set, will do a cq dry run for each uploaded CL. "
4843 "Please be careful when doing this; more than ~10 CLs "
4844 "has the potential to overload our build "
4845 "infrastructure. Try to upload these not during high "
4846 "load times (usually 11-3 Mountain View time). Email "
4847 "infra-dev@chromium.org with any questions.")
Takuto Ikuta51eca592019-02-14 19:40:52 +00004848 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4849 default=True,
4850 help='Sends your change to the CQ after an approval. Only '
4851 'works on repos that have the Auto-Submit label '
4852 'enabled')
Francois Dorayd42c6812017-05-30 15:10:20 -04004853 options, _ = parser.parse_args(args)
4854
4855 if not options.description_file:
4856 parser.error('No --description flag specified.')
4857
4858 def WrappedCMDupload(args):
4859 return CMDupload(OptionParser(), args)
4860
4861 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004862 Changelist, WrappedCMDupload, options.dry_run,
Takuto Ikuta51eca592019-02-14 19:40:52 +00004863 options.cq_dry_run, options.enable_auto_submit)
Francois Dorayd42c6812017-05-30 15:10:20 -04004864
4865
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004866@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004867@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004868def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004869 """DEPRECATED: Used to commit the current changelist via git-svn."""
4870 message = ('git-cl no longer supports committing to SVN repositories via '
4871 'git-svn. You probably want to use `git cl land` instead.')
4872 print(message)
4873 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004874
4875
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004876# Two special branches used by git cl land.
4877MERGE_BRANCH = 'git-cl-commit'
4878CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4879
4880
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004881@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004882@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004883def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004884 """Commits the current changelist via git.
4885
4886 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4887 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004888 """
4889 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4890 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004891 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004892 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004893 parser.add_option('--parallel', action='store_true',
4894 help='Run all tests specified by input_api.RunTests in all '
4895 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004896 auth.add_auth_options(parser)
4897 (options, args) = parser.parse_args(args)
4898 auth_config = auth.extract_auth_config_from_options(options)
4899
4900 cl = Changelist(auth_config=auth_config)
4901
Robert Iannucci2e73d432018-03-14 01:10:47 -07004902 if not cl.IsGerrit():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004903 parser.error('Rietveld is not supported.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004904
Robert Iannucci2e73d432018-03-14 01:10:47 -07004905 if not cl.GetIssue():
4906 DieWithError('You must upload the change first to Gerrit.\n'
4907 ' If you would rather have `git cl land` upload '
4908 'automatically for you, see http://crbug.com/642759')
4909 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004910 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004911
4912
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004913@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004914@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004915def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004916 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004917 parser.add_option('-b', dest='newbranch',
4918 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004919 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004920 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004921 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07004922 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004923 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004924 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004925 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004926 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004927 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004928 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004929
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004930
4931 group = optparse.OptionGroup(
4932 parser,
4933 'Options for continuing work on the current issue uploaded from a '
4934 'different clone (e.g. different machine). Must be used independently '
4935 'from the other options. No issue number should be specified, and the '
4936 'branch must have an issue number associated with it')
4937 group.add_option('--reapply', action='store_true', dest='reapply',
4938 help='Reset the branch and reapply the issue.\n'
4939 'CAUTION: This will undo any local changes in this '
4940 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004941
4942 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004943 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004944 parser.add_option_group(group)
4945
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004946 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004947 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004948 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004949 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004950 auth_config = auth.extract_auth_config_from_options(options)
4951
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004952 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004953 if options.newbranch:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004954 parser.error('--reapply works on the current branch only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004955 if len(args) > 0:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004956 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004957
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004958 cl = Changelist(auth_config=auth_config,
4959 codereview=options.forced_codereview)
4960 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004961 parser.error('Current branch must have an associated issue.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004962
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004963 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004964 if upstream is None:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004965 parser.error('No upstream branch specified. Cannot reset branch.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004966
4967 RunGit(['reset', '--hard', upstream])
4968 if options.pull:
4969 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004970
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004971 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4972 options.directory)
4973
4974 if len(args) != 1 or not args[0]:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004975 parser.error('Must specify issue number or URL.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004976
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004977 target_issue_arg = ParseIssueNumberArgument(args[0],
4978 options.forced_codereview)
4979 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004980 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004981
4982 cl_kwargs = {
4983 'auth_config': auth_config,
4984 'codereview_host': target_issue_arg.hostname,
4985 'codereview': options.forced_codereview,
4986 }
4987 detected_codereview_from_url = False
4988 if target_issue_arg.codereview and not options.forced_codereview:
4989 detected_codereview_from_url = True
4990 cl_kwargs['codereview'] = target_issue_arg.codereview
4991 cl_kwargs['issue'] = target_issue_arg.issue
4992
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004993 # We don't want uncommitted changes mixed up with the patch.
4994 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004995 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004996
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004997 if options.newbranch:
4998 if options.force:
4999 RunGit(['branch', '-D', options.newbranch],
5000 stderr=subprocess2.PIPE, error_ok=True)
5001 RunGit(['new-branch', options.newbranch])
5002
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005003 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005004
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005005 if cl.IsGerrit():
5006 if options.reject:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005007 parser.error('--reject is not supported with Gerrit code review.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005008 if options.directory:
5009 parser.error('--directory is not supported with Gerrit codereview.')
5010
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005011 if detected_codereview_from_url:
5012 print('canonical issue/change URL: %s (type: %s)\n' %
5013 (cl.GetIssueURL(), target_issue_arg.codereview))
5014
5015 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005016 options.nocommit, options.directory,
5017 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005018
5019
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005020def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005021 """Fetches the tree status and returns either 'open', 'closed',
5022 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005023 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005024 if url:
5025 status = urllib2.urlopen(url).read().lower()
5026 if status.find('closed') != -1 or status == '0':
5027 return 'closed'
5028 elif status.find('open') != -1 or status == '1':
5029 return 'open'
5030 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005031 return 'unset'
5032
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005033
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005034def GetTreeStatusReason():
5035 """Fetches the tree status from a json url and returns the message
5036 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005037 url = settings.GetTreeStatusUrl()
5038 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005039 connection = urllib2.urlopen(json_url)
5040 status = json.loads(connection.read())
5041 connection.close()
5042 return status['message']
5043
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005044
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005045@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005046def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005047 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005048 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005049 status = GetTreeStatus()
5050 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005051 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005052 return 2
5053
vapiera7fbd5a2016-06-16 09:17:49 -07005054 print('The tree is %s' % status)
5055 print()
5056 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005057 if status != 'open':
5058 return 1
5059 return 0
5060
5061
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005062@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005063def CMDtry(parser, args):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005064 """Triggers tryjobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005065 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005066 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005067 '-b', '--bot', action='append',
5068 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5069 'times to specify multiple builders. ex: '
5070 '"-b win_rel -b win_layout". See '
5071 'the try server waterfall for the builders name and the tests '
5072 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005073 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005074 '-B', '--bucket', default='',
5075 help=('Buildbucket bucket to send the try requests.'))
5076 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005077 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005078 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005079 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005080 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005081 help='Revision to use for the try job; default: the revision will '
5082 'be determined by the try recipe that builder runs, which usually '
5083 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005084 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005085 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005086 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005087 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005088 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005089 '--category', default='git_cl_try', help='Specify custom build category.')
5090 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005091 '--project',
5092 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005093 'in recipe to determine to which repository or directory to '
5094 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005095 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005096 '-p', '--property', dest='properties', action='append', default=[],
5097 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005098 'key2=value2 etc. The value will be treated as '
5099 'json if decodable, or as string otherwise. '
5100 'NOTE: using this may make your try job not usable for CQ, '
5101 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005102 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005103 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5104 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005105 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005106 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005107 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005108 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005109 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005110 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005111
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005112 if options.master and options.master.startswith('luci.'):
5113 parser.error(
5114 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005115 # Make sure that all properties are prop=value pairs.
5116 bad_params = [x for x in options.properties if '=' not in x]
5117 if bad_params:
5118 parser.error('Got properties with missing "=": %s' % bad_params)
5119
maruel@chromium.org15192402012-09-06 12:38:29 +00005120 if args:
5121 parser.error('Unknown arguments: %s' % args)
5122
Koji Ishii31c14782018-01-08 17:17:33 +09005123 cl = Changelist(auth_config=auth_config, issue=options.issue,
5124 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005125 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005126 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005127
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005128 if cl.IsGerrit():
5129 # HACK: warm up Gerrit change detail cache to save on RPCs.
5130 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5131
tandriie113dfd2016-10-11 10:20:12 -07005132 error_message = cl.CannotTriggerTryJobReason()
5133 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005134 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005135
borenet6c0efe62016-10-19 08:13:29 -07005136 if options.bucket and options.master:
5137 parser.error('Only one of --bucket and --master may be used.')
5138
qyearsley1fdfcb62016-10-24 13:22:03 -07005139 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005140
qyearsleydd49f942016-10-28 11:57:22 -07005141 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5142 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005143 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005144 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005145 print('git cl try with no bots now defaults to CQ dry run.')
5146 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5147 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005148
borenet6c0efe62016-10-19 08:13:29 -07005149 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005150 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005151 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005152 'of bot requires an initial job from a parent (usually a builder). '
5153 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005154 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005155 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005156
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005157 patchset = cl.GetMostRecentPatchset()
tandrii568043b2016-10-11 07:49:18 -07005158 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005159 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005160 except BuildbucketResponseException as ex:
5161 print('ERROR: %s' % ex)
5162 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005163 return 0
5164
5165
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005166@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005167def CMDtry_results(parser, args):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005168 """Prints info about results for tryjobs associated with the current CL."""
tandrii1838bad2016-10-06 00:10:52 -07005169 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005170 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005171 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005172 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005173 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005174 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005175 '--color', action='store_true', default=setup_color.IS_TTY,
5176 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005177 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005178 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5179 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005180 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005181 '--json', help=('Path of JSON output file to write try job results to,'
5182 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005183 parser.add_option_group(group)
5184 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005185 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005186 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005187 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005188 if args:
5189 parser.error('Unrecognized args: %s' % ' '.join(args))
5190
5191 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005192 cl = Changelist(
5193 issue=options.issue, codereview=options.forced_codereview,
5194 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005195 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005196 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005197
tandrii221ab252016-10-06 08:12:04 -07005198 patchset = options.patchset
5199 if not patchset:
5200 patchset = cl.GetMostRecentPatchset()
5201 if not patchset:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005202 parser.error('Code review host doesn\'t know about issue %s. '
tandrii221ab252016-10-06 08:12:04 -07005203 'No access to issue or wrong issue number?\n'
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005204 'Either upload first, or pass --patchset explicitly.' %
tandrii221ab252016-10-06 08:12:04 -07005205 cl.GetIssue())
5206
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005207 try:
tandrii221ab252016-10-06 08:12:04 -07005208 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005209 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005210 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005211 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005212 if options.json:
5213 write_try_results_json(options.json, jobs)
5214 else:
5215 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005216 return 0
5217
5218
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005219@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005220@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005221def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005222 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005223 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005224 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005225 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005226
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005227 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005228 if args:
5229 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005230 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005231 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005232 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005233 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005234
5235 # Clear configured merge-base, if there is one.
5236 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005237 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005238 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005239 return 0
5240
5241
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005242@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005243def CMDweb(parser, args):
5244 """Opens the current CL in the web browser."""
5245 _, args = parser.parse_args(args)
5246 if args:
5247 parser.error('Unrecognized args: %s' % ' '.join(args))
5248
5249 issue_url = Changelist().GetIssueURL()
5250 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005251 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005252 return 1
5253
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005254 # Redirect I/O before invoking browser to hide its output. For example, this
5255 # allows to hide "Created new window in existing browser session." message
5256 # from Chrome. Based on https://stackoverflow.com/a/2323563.
5257 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005258 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005259 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005260 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005261 os.open(os.devnull, os.O_RDWR)
5262 try:
5263 webbrowser.open(issue_url)
5264 finally:
5265 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005266 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005267 return 0
5268
5269
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005270@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005271def CMDset_commit(parser, args):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00005272 """Sets the commit bit to trigger the CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005273 parser.add_option('-d', '--dry-run', action='store_true',
5274 help='trigger in dry run mode')
5275 parser.add_option('-c', '--clear', action='store_true',
5276 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005277 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005278 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005279 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005280 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005281 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005282 if args:
5283 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005284 if options.dry_run and options.clear:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005285 parser.error('Only one of --dry-run and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005286
iannuccie53c9352016-08-17 14:40:40 -07005287 cl = Changelist(auth_config=auth_config, issue=options.issue,
5288 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005289 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005290 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005291 elif options.dry_run:
5292 state = _CQState.DRY_RUN
5293 else:
5294 state = _CQState.COMMIT
5295 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005296 parser.error('Must upload the issue first.')
tandrii9de9ec62016-07-13 03:01:59 -07005297 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005298 return 0
5299
5300
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005301@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005302def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005303 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005304 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005305 auth.add_auth_options(parser)
5306 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005307 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005308 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005309 if args:
5310 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005311 cl = Changelist(auth_config=auth_config, issue=options.issue,
5312 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005313 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005314 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005315 DieWithError('ERROR: No issue to close.')
groby@chromium.org411034a2013-02-26 15:12:01 +00005316 cl.CloseIssue()
5317 return 0
5318
5319
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005320@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005321def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005322 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005323 parser.add_option(
5324 '--stat',
5325 action='store_true',
5326 dest='stat',
5327 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005328 auth.add_auth_options(parser)
5329 options, args = parser.parse_args(args)
5330 auth_config = auth.extract_auth_config_from_options(options)
5331 if args:
5332 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005333
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005334 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005335 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005336 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005337 if not issue:
5338 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005339
Aaron Gablea718c3e2017-08-28 17:47:28 -07005340 base = cl._GitGetBranchConfigValue('last-upload-hash')
5341 if not base:
5342 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5343 if not base:
5344 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5345 revision_info = detail['revisions'][detail['current_revision']]
5346 fetch_info = revision_info['fetch']['http']
5347 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5348 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005349
Aaron Gablea718c3e2017-08-28 17:47:28 -07005350 cmd = ['git', 'diff']
5351 if options.stat:
5352 cmd.append('--stat')
5353 cmd.append(base)
5354 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005355
5356 return 0
5357
5358
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005359@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005360def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005361 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005362 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005363 '--ignore-current',
5364 action='store_true',
5365 help='Ignore the CL\'s current reviewers and start from scratch.')
5366 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005367 '--ignore-self',
5368 action='store_true',
5369 help='Do not consider CL\'s author as an owners.')
5370 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005371 '--no-color',
5372 action='store_true',
5373 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005374 parser.add_option(
5375 '--batch',
5376 action='store_true',
5377 help='Do not run interactively, just suggest some')
Yang Guo6e269a02019-06-26 11:17:02 +00005378 # TODO: Consider moving this to another command, since other
5379 # git-cl owners commands deal with owners for a given CL.
5380 parser.add_option(
5381 '--show-all',
5382 action='store_true',
5383 help='Show all owners for a particular file')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005384 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005385 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005386 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005387
5388 author = RunGit(['config', 'user.email']).strip() or None
5389
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005390 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005391
Yang Guo6e269a02019-06-26 11:17:02 +00005392 if options.show_all:
5393 for arg in args:
5394 base_branch = cl.GetCommonAncestorWithUpstream()
5395 change = cl.GetChange(base_branch, None)
5396 database = owners.Database(change.RepositoryRoot(), file, os.path)
5397 database.load_data_needed_for([arg])
5398 print('Owners for %s:' % arg)
5399 for owner in sorted(database.all_possible_owners([arg], None)):
5400 print(' - %s' % owner)
5401 return 0
5402
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005403 if args:
5404 if len(args) > 1:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005405 parser.error('Unknown args.')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005406 base_branch = args[0]
5407 else:
5408 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005409 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005410
5411 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005412 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5413
5414 if options.batch:
5415 db = owners.Database(change.RepositoryRoot(), file, os.path)
5416 print('\n'.join(db.reviewers_for(affected_files, author)))
5417 return 0
5418
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005419 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005420 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005421 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005422 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005423 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005424 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005425 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005426 override_files=change.OriginalOwnersFiles(),
5427 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005428
5429
Aiden Bennerc08566e2018-10-03 17:52:42 +00005430def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005431 """Generates a diff command."""
5432 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005433 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5434
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005435 if allow_prefix:
5436 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5437 # case that diff.noprefix is set in the user's git config.
5438 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5439 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005440 diff_cmd += ['--no-prefix']
5441
5442 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005443
5444 if args:
5445 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005446 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005447 diff_cmd.append(arg)
5448 else:
5449 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005450
5451 return diff_cmd
5452
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005453
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005454def MatchingFileType(file_name, extensions):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005455 """Returns True if the file name ends with one of the given extensions."""
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005456 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005457
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005458
enne@chromium.org555cfe42014-01-29 18:21:39 +00005459@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005460@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005461def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005462 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005463 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005464 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005465 parser.add_option('--full', action='store_true',
5466 help='Reformat the full content of all touched files')
5467 parser.add_option('--dry-run', action='store_true',
5468 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005469 parser.add_option(
5470 '--python',
5471 action='store_true',
5472 default=None,
5473 help='Enables python formatting on all python files.')
5474 parser.add_option(
5475 '--no-python',
5476 action='store_true',
5477 dest='python',
5478 help='Disables python formatting on all python files. '
5479 'Takes precedence over --python. '
5480 'If neither --python or --no-python are set, python '
5481 'files that have a .style.yapf file in an ancestor '
5482 'directory will be formatted.')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005483 parser.add_option('--js', action='store_true',
5484 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005485 parser.add_option('--diff', action='store_true',
5486 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005487 parser.add_option('--presubmit', action='store_true',
5488 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005489 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005490
Daniel Chengc55eecf2016-12-30 03:11:02 -08005491 # Normalize any remaining args against the current path, so paths relative to
5492 # the current directory are still resolved as expected.
5493 args = [os.path.join(os.getcwd(), arg) for arg in args]
5494
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005495 # git diff generates paths against the root of the repository. Change
5496 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005497 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005498 if rel_base_path:
5499 os.chdir(rel_base_path)
5500
digit@chromium.org29e47272013-05-17 17:01:46 +00005501 # Grab the merge-base commit, i.e. the upstream commit of the current
5502 # branch when it was created or the last time it was rebased. This is
5503 # to cover the case where the user may have called "git fetch origin",
5504 # moving the origin branch to a newer commit, but hasn't rebased yet.
5505 upstream_commit = None
5506 cl = Changelist()
5507 upstream_branch = cl.GetUpstreamBranch()
5508 if upstream_branch:
5509 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5510 upstream_commit = upstream_commit.strip()
5511
5512 if not upstream_commit:
5513 DieWithError('Could not find base commit for this branch. '
5514 'Are you in detached state?')
5515
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005516 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5517 diff_output = RunGit(changed_files_cmd)
5518 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005519 # Filter out files deleted by this CL
5520 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005521
Christopher Lamc5ba6922017-01-24 11:19:14 +11005522 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005523 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005524
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005525 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5526 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5527 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005528 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005529
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005530 top_dir = os.path.normpath(
5531 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5532
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005533 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5534 # formatted. This is used to block during the presubmit.
5535 return_value = 0
5536
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005537 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005538 # Locate the clang-format binary in the checkout
5539 try:
5540 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005541 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005542 DieWithError(e)
5543
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005544 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005545 cmd = [clang_format_tool]
5546 if not opts.dry_run and not opts.diff:
5547 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005548 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005549 if opts.diff:
5550 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005551 else:
5552 env = os.environ.copy()
5553 env['PATH'] = str(os.path.dirname(clang_format_tool))
5554 try:
5555 script = clang_format.FindClangFormatScriptInChromiumTree(
5556 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005557 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005558 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005559
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005560 cmd = [sys.executable, script, '-p0']
5561 if not opts.dry_run and not opts.diff:
5562 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005563
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005564 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5565 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005566
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005567 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5568 if opts.diff:
5569 sys.stdout.write(stdout)
5570 if opts.dry_run and len(stdout) > 0:
5571 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005572
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005573 # Similar code to above, but using yapf on .py files rather than clang-format
5574 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005575 py_explicitly_disabled = opts.python is not None and not opts.python
5576 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005577 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5578 yapf_tool = os.path.join(depot_tools_path, 'yapf')
5579 if sys.platform.startswith('win'):
5580 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005581
Aiden Bennerc08566e2018-10-03 17:52:42 +00005582 # If we couldn't find a yapf file we'll default to the chromium style
5583 # specified in depot_tools.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005584 chromium_default_yapf_style = os.path.join(depot_tools_path,
5585 YAPF_CONFIG_FILENAME)
Aiden Bennerc08566e2018-10-03 17:52:42 +00005586 # Used for caching.
5587 yapf_configs = {}
5588 for f in python_diff_files:
5589 # Find the yapf style config for the current file, defaults to depot
5590 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005591 _FindYapfConfigFile(f, yapf_configs, top_dir)
5592
5593 # Turn on python formatting by default if a yapf config is specified.
5594 # This breaks in the case of this repo though since the specified
5595 # style file is also the global default.
5596 if opts.python is None:
5597 filtered_py_files = []
5598 for f in python_diff_files:
5599 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5600 filtered_py_files.append(f)
5601 else:
5602 filtered_py_files = python_diff_files
5603
5604 # Note: yapf still seems to fix indentation of the entire file
5605 # even if line ranges are specified.
5606 # See https://github.com/google/yapf/issues/499
5607 if not opts.full and filtered_py_files:
5608 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5609
5610 for f in filtered_py_files:
5611 yapf_config = _FindYapfConfigFile(f, yapf_configs, top_dir)
5612 if yapf_config is None:
5613 yapf_config = chromium_default_yapf_style
Aiden Bennerc08566e2018-10-03 17:52:42 +00005614
5615 cmd = [yapf_tool, '--style', yapf_config, f]
5616
5617 has_formattable_lines = False
5618 if not opts.full:
5619 # Only run yapf over changed line ranges.
5620 for diff_start, diff_len in py_line_diffs[f]:
5621 diff_end = diff_start + diff_len - 1
5622 # Yapf errors out if diff_end < diff_start but this
5623 # is a valid line range diff for a removal.
5624 if diff_end >= diff_start:
5625 has_formattable_lines = True
5626 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5627 # If all line diffs were removals we have nothing to format.
5628 if not has_formattable_lines:
5629 continue
5630
5631 if opts.diff or opts.dry_run:
5632 cmd += ['--diff']
5633 # Will return non-zero exit code if non-empty diff.
5634 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5635 if opts.diff:
5636 sys.stdout.write(stdout)
5637 elif len(stdout) > 0:
5638 return_value = 2
5639 else:
5640 cmd += ['-i']
5641 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005642
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005643 # Dart's formatter does not have the nice property of only operating on
5644 # modified chunks, so hard code full.
5645 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005646 try:
5647 command = [dart_format.FindDartFmtToolInChromiumTree()]
5648 if not opts.dry_run and not opts.diff:
5649 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005650 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005651
ppi@chromium.org6593d932016-03-03 15:41:15 +00005652 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005653 if opts.dry_run and stdout:
5654 return_value = 2
5655 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005656 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5657 'found in this checkout. Files in other languages are still '
5658 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005659
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005660 # Format GN build files. Always run on full build files for canonical form.
5661 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005662 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005663 if opts.dry_run or opts.diff:
5664 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005665 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005666 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5667 shell=sys.platform == 'win32',
5668 cwd=top_dir)
5669 if opts.dry_run and gn_ret == 2:
5670 return_value = 2 # Not formatted.
5671 elif opts.diff and gn_ret == 2:
5672 # TODO this should compute and print the actual diff.
5673 print("This change has GN build file diff for " + gn_diff_file)
5674 elif gn_ret != 0:
5675 # For non-dry run cases (and non-2 return values for dry-run), a
5676 # nonzero error code indicates a failure, probably because the file
5677 # doesn't parse.
5678 DieWithError("gn format failed on " + gn_diff_file +
5679 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005680
Ilya Shermane081cbe2017-08-15 17:51:04 -07005681 # Skip the metrics formatting from the global presubmit hook. These files have
5682 # a separate presubmit hook that issues an error if the files need formatting,
5683 # whereas the top-level presubmit script merely issues a warning. Formatting
5684 # these files is somewhat slow, so it's important not to duplicate the work.
5685 if not opts.presubmit:
5686 for xml_dir in GetDirtyMetricsDirs(diff_files):
5687 tool_dir = os.path.join(top_dir, xml_dir)
5688 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5689 if opts.dry_run or opts.diff:
5690 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005691 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005692 if opts.diff:
5693 sys.stdout.write(stdout)
5694 if opts.dry_run and stdout:
5695 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005696
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005697 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005698
Steven Holte2e664bf2017-04-21 13:10:47 -07005699def GetDirtyMetricsDirs(diff_files):
5700 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5701 metrics_xml_dirs = [
5702 os.path.join('tools', 'metrics', 'actions'),
5703 os.path.join('tools', 'metrics', 'histograms'),
5704 os.path.join('tools', 'metrics', 'rappor'),
5705 os.path.join('tools', 'metrics', 'ukm')]
5706 for xml_dir in metrics_xml_dirs:
5707 if any(file.startswith(xml_dir) for file in xml_diff_files):
5708 yield xml_dir
5709
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005710
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005711@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005712@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005713def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005714 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005715 _, args = parser.parse_args(args)
5716
5717 if len(args) != 1:
5718 parser.print_help()
5719 return 1
5720
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005721 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005722 if not issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005723 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005724
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005725 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005726
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005727 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005728 output = RunGit(['config', '--local', '--get-regexp',
5729 r'branch\..*\.%s' % issueprefix],
5730 error_ok=True)
5731 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005732 if issue == target_issue:
5733 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005734
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005735 branches = []
5736 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005737 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005738 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005739 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005740 return 1
5741 if len(branches) == 1:
5742 RunGit(['checkout', branches[0]])
5743 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005744 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005745 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005746 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005747 which = raw_input('Choose by index: ')
5748 try:
5749 RunGit(['checkout', branches[int(which)]])
5750 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005751 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005752 return 1
5753
5754 return 0
5755
5756
maruel@chromium.org29404b52014-09-08 22:58:00 +00005757def CMDlol(parser, args):
5758 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005759 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005760 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5761 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5762 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005763 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005764 return 0
5765
5766
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005767class OptionParser(optparse.OptionParser):
5768 """Creates the option parse and add --verbose support."""
5769 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005770 optparse.OptionParser.__init__(
5771 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005772 self.add_option(
5773 '-v', '--verbose', action='count', default=0,
5774 help='Use 2 times for more debugging info')
5775
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005776 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005777 try:
5778 return self._parse_args(args)
5779 finally:
5780 # Regardless of success or failure of args parsing, we want to report
5781 # metrics, but only after logging has been initialized (if parsing
5782 # succeeded).
5783 global settings
5784 settings = Settings()
5785
5786 if not metrics.DISABLE_METRICS_COLLECTION:
5787 # GetViewVCUrl ultimately calls logging method.
5788 project_url = settings.GetViewVCUrl().strip('/+')
5789 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5790 metrics.collector.add('project_urls', [project_url])
5791
5792 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005793 # Create an optparse.Values object that will store only the actual passed
5794 # options, without the defaults.
5795 actual_options = optparse.Values()
5796 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5797 # Create an optparse.Values object with the default options.
5798 options = optparse.Values(self.get_default_values().__dict__)
5799 # Update it with the options passed by the user.
5800 options._update_careful(actual_options.__dict__)
5801 # Store the options passed by the user in an _actual_options attribute.
5802 # We store only the keys, and not the values, since the values can contain
5803 # arbitrary information, which might be PII.
5804 metrics.collector.add('arguments', actual_options.__dict__.keys())
5805
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005806 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005807 logging.basicConfig(
5808 level=levels[min(options.verbose, len(levels) - 1)],
5809 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5810 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005811
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005812 return options, args
5813
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005814
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005815def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005816 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005817 print('\nYour python version %s is unsupported, please upgrade.\n' %
5818 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005819 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005820
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005821 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005822 dispatcher = subcommand.CommandDispatcher(__name__)
5823 try:
5824 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005825 except auth.AuthenticationError as e:
5826 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005827 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005828 if e.code != 500:
5829 raise
5830 DieWithError(
5831 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005832 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005833 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005834
5835
5836if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005837 # These affect sys.stdout so do it outside of main() to simplify mocks in
5838 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005839 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005840 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005841 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005842 sys.exit(main(sys.argv[1:]))