blob: 9b06bc0c8a4ced0e964740af14f80217e65f5a40 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +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
40try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080041 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000042except ImportError:
43 pass
44
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000045from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000046from third_party import httplib2
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000047import auth
skobes6468b902016-10-24 08:45:10 -070048import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000049import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000050import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000051import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000052import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000053import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000054import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000055import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000057import git_footers
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000058import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000059import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000060import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000061import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import presubmit_support
63import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040064import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000065import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000067import watchlists
68
tandrii7400cf02016-06-21 08:48:07 -070069__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000070
Edward Lemur0f58ae42019-04-30 17:24:12 +000071# Traces for git push will be stored in a traces directory inside the
72# depot_tools checkout.
73DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
74TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
75
76# When collecting traces, Git hashes will be reduced to 6 characters to reduce
77# the size after compression.
78GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
79# Used to redact the cookies from the gitcookies file.
80GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
81
Edward Lemur1b52d872019-05-09 21:12:12 +000082# The maximum number of traces we will keep. Multiplied by 3 since we store
83# 3 files per trace.
84MAX_TRACES = 3 * 10
Edward Lemur5737f022019-05-17 01:24:00 +000085# Message to be displayed to the user to inform where to find the traces for a
86# git-cl upload execution.
Edward Lemur0f58ae42019-04-30 17:24:12 +000087TRACES_MESSAGE = (
Edward Lemur1b52d872019-05-09 21:12:12 +000088'\n'
Edward Lemur5737f022019-05-17 01:24:00 +000089'The traces of this git-cl execution have been recorded at:\n'
Edward Lemur1b52d872019-05-09 21:12:12 +000090' %(trace_name)s-traces.zip\n'
Edward Lemur5737f022019-05-17 01:24:00 +000091'Copies of your gitcookies file and git config have been recorded at:\n'
92' %(trace_name)s-git-info.zip\n')
Edward Lemur1b52d872019-05-09 21:12:12 +000093# Format of the message to be stored as part of the traces to give developers a
94# better context when they go through traces.
95TRACES_README_FORMAT = (
96'Date: %(now)s\n'
97'\n'
98'Change: https://%(gerrit_host)s/q/%(change_id)s\n'
99'Title: %(title)s\n'
100'\n'
101'%(description)s\n'
102'\n'
103'Execution time: %(execution_time)s\n'
104'Exit code: %(exit_code)s\n') + TRACES_MESSAGE
Edward Lemur0f58ae42019-04-30 17:24:12 +0000105
tandrii9d2c7a32016-06-22 03:42:45 -0700106COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -0800107POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000108DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +0000109REFS_THAT_ALIAS_TO_OTHER_REFS = {
110 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
111 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
112}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000113
thestig@chromium.org44202a22014-03-11 19:22:18 +0000114# Valid extensions for files we want to lint.
115DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
116DEFAULT_LINT_IGNORE_REGEX = r"$^"
117
Aiden Bennerc08566e2018-10-03 17:52:42 +0000118# File name for yapf style config files.
119YAPF_CONFIG_FILENAME = '.style.yapf'
120
borenet6c0efe62016-10-19 08:13:29 -0700121# Buildbucket master name prefix.
122MASTER_PREFIX = 'master.'
123
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000124# Shortcut since it quickly becomes redundant.
125Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000126
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000127# Initialized in main()
128settings = None
129
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100130# Used by tests/git_cl_test.py to add extra logging.
131# Inside the weirdly failing test, add this:
132# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700133# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100134_IS_BEING_TESTED = False
135
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000136
Christopher Lamf732cd52017-01-24 12:40:11 +1100137def DieWithError(message, change_desc=None):
138 if change_desc:
139 SaveDescriptionBackup(change_desc)
140
vapiera7fbd5a2016-06-16 09:17:49 -0700141 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000142 sys.exit(1)
143
144
Christopher Lamf732cd52017-01-24 12:40:11 +1100145def SaveDescriptionBackup(change_desc):
146 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000147 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100148 backup_file = open(backup_path, 'w')
149 backup_file.write(change_desc.description)
150 backup_file.close()
151
152
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000153def GetNoGitPagerEnv():
154 env = os.environ.copy()
155 # 'cat' is a magical git string that disables pagers on all platforms.
156 env['GIT_PAGER'] = 'cat'
157 return env
158
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000159
bsep@chromium.org627d9002016-04-29 00:00:52 +0000160def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000161 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000162 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000163 except subprocess2.CalledProcessError as e:
164 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000165 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000166 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000167 'Command "%s" failed.\n%s' % (
168 ' '.join(args), error_message or e.stdout or ''))
169 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000170
171
172def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000173 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000174 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000175
176
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000177def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000178 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700179 if suppress_stderr:
180 stderr = subprocess2.VOID
181 else:
182 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000183 try:
tandrii5d48c322016-08-18 16:19:37 -0700184 (out, _), code = subprocess2.communicate(['git'] + args,
185 env=GetNoGitPagerEnv(),
186 stdout=subprocess2.PIPE,
187 stderr=stderr)
188 return code, out
189 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900190 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700191 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000192
193
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000194def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000195 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000196 return RunGitWithCode(args, suppress_stderr=True)[1]
197
198
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000199def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000200 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000201 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000202 return (version.startswith(prefix) and
203 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000204
205
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000206def BranchExists(branch):
207 """Return True if specified branch exists."""
208 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
209 suppress_stderr=True)
210 return not code
211
212
tandrii2a16b952016-10-19 07:09:44 -0700213def time_sleep(seconds):
214 # Use this so that it can be mocked in tests without interfering with python
215 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700216 return time.sleep(seconds)
217
218
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000219def time_time():
220 # Use this so that it can be mocked in tests without interfering with python
221 # system machinery.
222 return time.time()
223
224
Edward Lemur1b52d872019-05-09 21:12:12 +0000225def datetime_now():
226 # Use this so that it can be mocked in tests without interfering with python
227 # system machinery.
228 return datetime.datetime.now()
229
230
maruel@chromium.org90541732011-04-01 17:54:18 +0000231def ask_for_data(prompt):
232 try:
233 return raw_input(prompt)
234 except KeyboardInterrupt:
235 # Hide the exception.
236 sys.exit(1)
237
238
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100239def confirm_or_exit(prefix='', action='confirm'):
240 """Asks user to press enter to continue or press Ctrl+C to abort."""
241 if not prefix or prefix.endswith('\n'):
242 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100243 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100244 mid = ' Press'
245 elif prefix.endswith(' '):
246 mid = 'press'
247 else:
248 mid = ' press'
249 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
250
251
252def ask_for_explicit_yes(prompt):
253 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
254 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
255 while True:
256 if 'yes'.startswith(result):
257 return True
258 if 'no'.startswith(result):
259 return False
260 result = ask_for_data('Please, type yes or no: ').lower()
261
262
tandrii5d48c322016-08-18 16:19:37 -0700263def _git_branch_config_key(branch, key):
264 """Helper method to return Git config key for a branch."""
265 assert branch, 'branch name is required to set git config for it'
266 return 'branch.%s.%s' % (branch, key)
267
268
269def _git_get_branch_config_value(key, default=None, value_type=str,
270 branch=False):
271 """Returns git config value of given or current branch if any.
272
273 Returns default in all other cases.
274 """
275 assert value_type in (int, str, bool)
276 if branch is False: # Distinguishing default arg value from None.
277 branch = GetCurrentBranch()
278
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000279 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700280 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000281
tandrii5d48c322016-08-18 16:19:37 -0700282 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700283 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700284 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700285 # git config also has --int, but apparently git config suffers from integer
286 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700287 args.append(_git_branch_config_key(branch, key))
288 code, out = RunGitWithCode(args)
289 if code == 0:
290 value = out.strip()
291 if value_type == int:
292 return int(value)
293 if value_type == bool:
294 return bool(value.lower() == 'true')
295 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000296 return default
297
298
tandrii5d48c322016-08-18 16:19:37 -0700299def _git_set_branch_config_value(key, value, branch=None, **kwargs):
300 """Sets the value or unsets if it's None of a git branch config.
301
302 Valid, though not necessarily existing, branch must be provided,
303 otherwise currently checked out branch is used.
304 """
305 if not branch:
306 branch = GetCurrentBranch()
307 assert branch, 'a branch name OR currently checked out branch is required'
308 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700309 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700310 if value is None:
311 args.append('--unset')
312 elif isinstance(value, bool):
313 args.append('--bool')
314 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700315 else:
tandrii33a46ff2016-08-23 05:53:40 -0700316 # git config also has --int, but apparently git config suffers from integer
317 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700318 value = str(value)
319 args.append(_git_branch_config_key(branch, key))
320 if value is not None:
321 args.append(value)
322 RunGit(args, **kwargs)
323
324
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100325def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700326 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100327
328 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
329 """
330 # Git also stores timezone offset, but it only affects visual display,
331 # actual point in time is defined by this timestamp only.
332 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
333
334
335def _git_amend_head(message, committer_timestamp):
336 """Amends commit with new message and desired committer_timestamp.
337
338 Sets committer timezone to UTC.
339 """
340 env = os.environ.copy()
341 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
342 return RunGit(['commit', '--amend', '-m', message], env=env)
343
344
machenbach@chromium.org45453142015-09-15 08:45:22 +0000345def _get_properties_from_options(options):
346 properties = dict(x.split('=', 1) for x in options.properties)
347 for key, val in properties.iteritems():
348 try:
349 properties[key] = json.loads(val)
350 except ValueError:
351 pass # If a value couldn't be evaluated, treat it as a string.
352 return properties
353
354
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000355def _prefix_master(master):
356 """Convert user-specified master name to full master name.
357
358 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
359 name, while the developers always use shortened master name
360 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
361 function does the conversion for buildbucket migration.
362 """
borenet6c0efe62016-10-19 08:13:29 -0700363 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000364 return master
borenet6c0efe62016-10-19 08:13:29 -0700365 return '%s%s' % (MASTER_PREFIX, master)
366
367
368def _unprefix_master(bucket):
369 """Convert bucket name to shortened master name.
370
371 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
372 name, while the developers always use shortened master name
373 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
374 function does the conversion for buildbucket migration.
375 """
376 if bucket.startswith(MASTER_PREFIX):
377 return bucket[len(MASTER_PREFIX):]
378 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000379
380
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000381def _buildbucket_retry(operation_name, http, *args, **kwargs):
382 """Retries requests to buildbucket service and returns parsed json content."""
383 try_count = 0
384 while True:
385 response, content = http.request(*args, **kwargs)
386 try:
387 content_json = json.loads(content)
388 except ValueError:
389 content_json = None
390
391 # Buildbucket could return an error even if status==200.
392 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000393 error = content_json.get('error')
394 if error.get('code') == 403:
395 raise BuildbucketResponseException(
396 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000397 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000398 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000399 raise BuildbucketResponseException(msg)
400
401 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700402 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000403 raise BuildbucketResponseException(
404 'Buildbucket returns invalid json content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700405 'Please file bugs at http://crbug.com, '
406 'component "Infra>Platform>BuildBucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000407 content)
408 return content_json
409 if response.status < 500 or try_count >= 2:
410 raise httplib2.HttpLib2Error(content)
411
412 # status >= 500 means transient failures.
413 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700414 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000415 try_count += 1
416 assert False, 'unreachable'
417
418
qyearsley1fdfcb62016-10-24 13:22:03 -0700419def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700420 """Returns a dict mapping bucket names to builders and tests,
421 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700422 """
qyearsleydd49f942016-10-28 11:57:22 -0700423 # If no bots are listed, we try to get a set of builders and tests based
424 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700425 if not options.bot:
426 change = changelist.GetChange(
427 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700428 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700429 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700430 change=change,
431 changed_files=change.LocalPaths(),
432 repository_root=settings.GetRoot(),
433 default_presubmit=None,
434 project=None,
435 verbose=options.verbose,
436 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700437 if masters is None:
438 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100439 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700440
qyearsley1fdfcb62016-10-24 13:22:03 -0700441 if options.bucket:
442 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700443 if options.master:
444 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700445
qyearsleydd49f942016-10-28 11:57:22 -0700446 # If bots are listed but no master or bucket, then we need to find out
447 # the corresponding master for each bot.
448 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
449 if error_message:
450 option_parser.error(
451 'Tryserver master cannot be found because: %s\n'
452 'Please manually specify the tryserver master, e.g. '
453 '"-m tryserver.chromium.linux".' % error_message)
454 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700455
456
qyearsley123a4682016-10-26 09:12:17 -0700457def _get_bucket_map_for_builders(builders):
458 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700459 map_url = 'https://builders-map.appspot.com/'
460 try:
qyearsley123a4682016-10-26 09:12:17 -0700461 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700462 except urllib2.URLError as e:
463 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
464 (map_url, e))
465 except ValueError as e:
466 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700467 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700468 return None, 'Failed to build master map.'
469
qyearsley123a4682016-10-26 09:12:17 -0700470 bucket_map = {}
471 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800472 bucket = builders_map.get(builder, {}).get('bucket')
473 if bucket:
474 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700475 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700476
477
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800478def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700479 """Sends a request to Buildbucket to trigger try jobs for a changelist.
480
481 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700482 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700483 changelist: Changelist that the try jobs are associated with.
484 buckets: A nested dict mapping bucket names to builders to tests.
485 options: Command-line options.
486 """
tandriide281ae2016-10-12 06:02:30 -0700487 assert changelist.GetIssue(), 'CL must be uploaded first'
488 codereview_url = changelist.GetCodereviewServer()
489 assert codereview_url, 'CL must be uploaded first'
490 patchset = patchset or changelist.GetMostRecentPatchset()
491 assert patchset, 'CL must be uploaded first'
492
493 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700494 # Cache the buildbucket credentials under the codereview host key, so that
495 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700496 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000497 http = authenticator.authorize(httplib2.Http())
498 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700499
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000500 buildbucket_put_url = (
501 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000502 hostname=options.buildbucket_host))
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000503 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandriide281ae2016-10-12 06:02:30 -0700504 hostname=codereview_host,
505 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000506 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700507
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700508 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800509 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700510 if options.clobber:
511 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700512 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700513 if extra_properties:
514 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000515
516 batch_req_body = {'builds': []}
517 print_text = []
518 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700519 for bucket, builders_and_tests in sorted(buckets.iteritems()):
520 print_text.append('Bucket: %s' % bucket)
521 master = None
522 if bucket.startswith(MASTER_PREFIX):
523 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000524 for builder, tests in sorted(builders_and_tests.iteritems()):
525 print_text.append(' %s: %s' % (builder, tests))
526 parameters = {
527 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000528 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100529 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000530 'revision': options.revision,
531 }],
tandrii8c5a3532016-11-04 07:52:02 -0700532 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000533 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000534 if 'presubmit' in builder.lower():
535 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000536 if tests:
537 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700538
539 tags = [
540 'builder:%s' % builder,
541 'buildset:%s' % buildset,
542 'user_agent:git_cl_try',
543 ]
544 if master:
545 parameters['properties']['master'] = master
546 tags.append('master:%s' % master)
547
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000548 batch_req_body['builds'].append(
549 {
550 'bucket': bucket,
551 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000552 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700553 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000554 }
555 )
556
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000557 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700558 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000559 http,
560 buildbucket_put_url,
561 'PUT',
562 body=json.dumps(batch_req_body),
563 headers={'Content-Type': 'application/json'}
564 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000565 print_text.append('To see results here, run: git cl try-results')
566 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700567 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000568
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000569
tandrii221ab252016-10-06 08:12:04 -0700570def fetch_try_jobs(auth_config, changelist, buildbucket_host,
571 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700572 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000573
qyearsley53f48a12016-09-01 10:45:13 -0700574 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000575 """
tandrii221ab252016-10-06 08:12:04 -0700576 assert buildbucket_host
577 assert changelist.GetIssue(), 'CL must be uploaded first'
578 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
579 patchset = patchset or changelist.GetMostRecentPatchset()
580 assert patchset, 'CL must be uploaded first'
581
582 codereview_url = changelist.GetCodereviewServer()
583 codereview_host = urlparse.urlparse(codereview_url).hostname
584 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000585 if authenticator.has_cached_credentials():
586 http = authenticator.authorize(httplib2.Http())
587 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700588 print('Warning: Some results might be missing because %s' %
589 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700590 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000591 http = httplib2.Http()
592
593 http.force_exception_to_status_code = True
594
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000595 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandrii221ab252016-10-06 08:12:04 -0700596 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000597 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700598 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000599 params = {'tag': 'buildset:%s' % buildset}
600
601 builds = {}
602 while True:
603 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700604 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000605 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700606 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000607 for build in content.get('builds', []):
608 builds[build['id']] = build
609 if 'next_cursor' in content:
610 params['start_cursor'] = content['next_cursor']
611 else:
612 break
613 return builds
614
615
qyearsleyeab3c042016-08-24 09:18:28 -0700616def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000617 """Prints nicely result of fetch_try_jobs."""
618 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700619 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000620 return
621
622 # Make a copy, because we'll be modifying builds dictionary.
623 builds = builds.copy()
624 builder_names_cache = {}
625
626 def get_builder(b):
627 try:
628 return builder_names_cache[b['id']]
629 except KeyError:
630 try:
631 parameters = json.loads(b['parameters_json'])
632 name = parameters['builder_name']
633 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700634 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700635 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000636 name = None
637 builder_names_cache[b['id']] = name
638 return name
639
640 def get_bucket(b):
641 bucket = b['bucket']
642 if bucket.startswith('master.'):
643 return bucket[len('master.'):]
644 return bucket
645
646 if options.print_master:
647 name_fmt = '%%-%ds %%-%ds' % (
648 max(len(str(get_bucket(b))) for b in builds.itervalues()),
649 max(len(str(get_builder(b))) for b in builds.itervalues()))
650 def get_name(b):
651 return name_fmt % (get_bucket(b), get_builder(b))
652 else:
653 name_fmt = '%%-%ds' % (
654 max(len(str(get_builder(b))) for b in builds.itervalues()))
655 def get_name(b):
656 return name_fmt % get_builder(b)
657
658 def sort_key(b):
659 return b['status'], b.get('result'), get_name(b), b.get('url')
660
661 def pop(title, f, color=None, **kwargs):
662 """Pop matching builds from `builds` dict and print them."""
663
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000664 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000665 colorize = str
666 else:
667 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
668
669 result = []
670 for b in builds.values():
671 if all(b.get(k) == v for k, v in kwargs.iteritems()):
672 builds.pop(b['id'])
673 result.append(b)
674 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700675 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000676 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700677 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000678
679 total = len(builds)
680 pop(status='COMPLETED', result='SUCCESS',
681 title='Successes:', color=Fore.GREEN,
682 f=lambda b: (get_name(b), b.get('url')))
683 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
684 title='Infra Failures:', color=Fore.MAGENTA,
685 f=lambda b: (get_name(b), b.get('url')))
686 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
687 title='Failures:', color=Fore.RED,
688 f=lambda b: (get_name(b), b.get('url')))
689 pop(status='COMPLETED', result='CANCELED',
690 title='Canceled:', color=Fore.MAGENTA,
691 f=lambda b: (get_name(b),))
692 pop(status='COMPLETED', result='FAILURE',
693 failure_reason='INVALID_BUILD_DEFINITION',
694 title='Wrong master/builder name:', color=Fore.MAGENTA,
695 f=lambda b: (get_name(b),))
696 pop(status='COMPLETED', result='FAILURE',
697 title='Other failures:',
698 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
699 pop(status='COMPLETED',
700 title='Other finished:',
701 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
702 pop(status='STARTED',
703 title='Started:', color=Fore.YELLOW,
704 f=lambda b: (get_name(b), b.get('url')))
705 pop(status='SCHEDULED',
706 title='Scheduled:',
707 f=lambda b: (get_name(b), 'id=%s' % b['id']))
708 # The last section is just in case buildbucket API changes OR there is a bug.
709 pop(title='Other:',
710 f=lambda b: (get_name(b), 'id=%s' % b['id']))
711 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700712 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000713
714
Aiden Bennerc08566e2018-10-03 17:52:42 +0000715def _ComputeDiffLineRanges(files, upstream_commit):
716 """Gets the changed line ranges for each file since upstream_commit.
717
718 Parses a git diff on provided files and returns a dict that maps a file name
719 to an ordered list of range tuples in the form (start_line, count).
720 Ranges are in the same format as a git diff.
721 """
722 # If files is empty then diff_output will be a full diff.
723 if len(files) == 0:
724 return {}
725
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000726 # Take the git diff and find the line ranges where there are changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000727 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
728 diff_output = RunGit(diff_cmd)
729
730 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
731 # 2 capture groups
732 # 0 == fname of diff file
733 # 1 == 'diff_start,diff_count' or 'diff_start'
734 # will match each of
735 # diff --git a/foo.foo b/foo.py
736 # @@ -12,2 +14,3 @@
737 # @@ -12,2 +17 @@
738 # running re.findall on the above string with pattern will give
739 # [('foo.py', ''), ('', '14,3'), ('', '17')]
740
741 curr_file = None
742 line_diffs = {}
743 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
744 if match[0] != '':
745 # Will match the second filename in diff --git a/a.py b/b.py.
746 curr_file = match[0]
747 line_diffs[curr_file] = []
748 else:
749 # Matches +14,3
750 if ',' in match[1]:
751 diff_start, diff_count = match[1].split(',')
752 else:
753 # Single line changes are of the form +12 instead of +12,1.
754 diff_start = match[1]
755 diff_count = 1
756
757 diff_start = int(diff_start)
758 diff_count = int(diff_count)
759
760 # If diff_count == 0 this is a removal we can ignore.
761 line_diffs[curr_file].append((diff_start, diff_count))
762
763 return line_diffs
764
765
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000766def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000767 """Checks if a yapf file is in any parent directory of fpath until top_dir.
768
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000769 Recursively checks parent directories to find yapf file and if no yapf file
770 is found returns None. Uses yapf_config_cache as a cache for
771 previously found configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000772 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000773 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000774 # Return result if we've already computed it.
775 if fpath in yapf_config_cache:
776 return yapf_config_cache[fpath]
777
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000778 parent_dir = os.path.dirname(fpath)
779 if os.path.isfile(fpath):
780 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000781 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000782 # Otherwise fpath is a directory
783 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
784 if os.path.isfile(yapf_file):
785 ret = yapf_file
786 elif fpath == top_dir or parent_dir == fpath:
787 # If we're at the top level directory, or if we're at root
788 # there is no provided style.
789 ret = None
790 else:
791 # Otherwise recurse on the current directory.
792 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000793 yapf_config_cache[fpath] = ret
794 return ret
795
796
qyearsley53f48a12016-09-01 10:45:13 -0700797def write_try_results_json(output_file, builds):
798 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
799
800 The input |builds| dict is assumed to be generated by Buildbucket.
801 Buildbucket documentation: http://goo.gl/G0s101
802 """
803
804 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800805 """Extracts some of the information from one build dict."""
806 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700807 return {
808 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700809 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800810 'builder_name': parameters.get('builder_name'),
811 'created_ts': build.get('created_ts'),
812 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700813 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800814 'result': build.get('result'),
815 'status': build.get('status'),
816 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700817 'url': build.get('url'),
818 }
819
820 converted = []
821 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000822 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 10:45:13 -0700823 write_json(output_file, converted)
824
825
Aaron Gable13101a62018-02-09 13:20:41 -0800826def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000827 """Prints statistics about the change to the user."""
828 # --no-ext-diff is broken in some versions of Git, so try to work around
829 # this by overriding the environment (but there is still a problem if the
830 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000831 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000832 if 'GIT_EXTERNAL_DIFF' in env:
833 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000834
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000835 try:
836 stdout = sys.stdout.fileno()
837 except AttributeError:
838 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000839 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800840 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000841 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000842
843
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000844class BuildbucketResponseException(Exception):
845 pass
846
847
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848class Settings(object):
849 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000850 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000851 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000852 self.tree_status_url = None
853 self.viewvc_url = None
854 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000855 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000856 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000857 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000858 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000859
860 def LazyUpdateIfNeeded(self):
861 """Updates the settings from a codereview.settings file, if available."""
862 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000863 # The only value that actually changes the behavior is
864 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000865 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000866 error_ok=True
867 ).strip().lower()
868
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000869 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000870 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000871 LoadCodereviewSettingsFromFile(cr_settings_file)
872 self.updated = True
873
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000874 @staticmethod
875 def GetRelativeRoot():
876 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000877
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000878 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000879 if self.root is None:
880 self.root = os.path.abspath(self.GetRelativeRoot())
881 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000882
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000883 def GetTreeStatusUrl(self, error_ok=False):
884 if not self.tree_status_url:
885 error_message = ('You must configure your tree status URL by running '
886 '"git cl config".')
Edward Lemur61ea3072018-12-01 00:34:36 +0000887 self.tree_status_url = self._GetConfig(
888 'rietveld.tree-status-url', error_ok=error_ok,
889 error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000890 return self.tree_status_url
891
892 def GetViewVCUrl(self):
893 if not self.viewvc_url:
Edward Lemur61ea3072018-12-01 00:34:36 +0000894 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000895 return self.viewvc_url
896
rmistry@google.com90752582014-01-14 21:04:50 +0000897 def GetBugPrefix(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000898 return self._GetConfig('rietveld.bug-prefix', error_ok=True)
rmistry@google.com78948ed2015-07-08 23:09:57 +0000899
rmistry@google.com5626a922015-02-26 14:03:30 +0000900 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000901 run_post_upload_hook = self._GetConfig(
902 'rietveld.run-post-upload-hook', error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +0000903 return run_post_upload_hook == "True"
904
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000905 def GetDefaultCCList(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000906 return self._GetConfig('rietveld.cc', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000907
ukai@chromium.orge8077812012-02-03 03:41:46 +0000908 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700909 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000910 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700911 self.is_gerrit = (
912 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000913 return self.is_gerrit
914
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000915 def GetSquashGerritUploads(self):
916 """Return true if uploads to Gerrit should be squashed by default."""
917 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700918 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
919 if self.squash_gerrit_uploads is None:
920 # Default is squash now (http://crbug.com/611892#c23).
921 self.squash_gerrit_uploads = not (
922 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
923 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000924 return self.squash_gerrit_uploads
925
tandriia60502f2016-06-20 02:01:53 -0700926 def GetSquashGerritUploadsOverride(self):
927 """Return True or False if codereview.settings should be overridden.
928
929 Returns None if no override has been defined.
930 """
931 # See also http://crbug.com/611892#c23
932 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
933 error_ok=True).strip()
934 if result == 'true':
935 return True
936 if result == 'false':
937 return False
938 return None
939
tandrii@chromium.org28253532016-04-14 13:46:56 +0000940 def GetGerritSkipEnsureAuthenticated(self):
941 """Return True if EnsureAuthenticated should not be done for Gerrit
942 uploads."""
943 if self.gerrit_skip_ensure_authenticated is None:
944 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000945 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000946 error_ok=True).strip() == 'true')
947 return self.gerrit_skip_ensure_authenticated
948
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000949 def GetGitEditor(self):
950 """Return the editor specified in the git config, or None if none is."""
951 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000952 # Git requires single quotes for paths with spaces. We need to replace
953 # them with double quotes for Windows to treat such paths as a single
954 # path.
955 self.git_editor = self._GetConfig(
956 'core.editor', error_ok=True).replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000957 return self.git_editor or None
958
thestig@chromium.org44202a22014-03-11 19:22:18 +0000959 def GetLintRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000960 return (self._GetConfig('rietveld.cpplint-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000961 DEFAULT_LINT_REGEX)
962
963 def GetLintIgnoreRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000964 return (self._GetConfig('rietveld.cpplint-ignore-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000965 DEFAULT_LINT_IGNORE_REGEX)
966
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000967 def _GetConfig(self, param, **kwargs):
968 self.LazyUpdateIfNeeded()
969 return RunGit(['config', param], **kwargs).strip()
970
971
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100972@contextlib.contextmanager
973def _get_gerrit_project_config_file(remote_url):
974 """Context manager to fetch and store Gerrit's project.config from
975 refs/meta/config branch and store it in temp file.
976
977 Provides a temporary filename or None if there was error.
978 """
979 error, _ = RunGitWithCode([
980 'fetch', remote_url,
981 '+refs/meta/config:refs/git_cl/meta/config'])
982 if error:
983 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700984 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100985 (remote_url, error))
986 yield None
987 return
988
989 error, project_config_data = RunGitWithCode(
990 ['show', 'refs/git_cl/meta/config:project.config'])
991 if error:
992 print('WARNING: project.config file not found')
993 yield None
994 return
995
996 with gclient_utils.temporary_directory() as tempdir:
997 project_config_file = os.path.join(tempdir, 'project.config')
998 gclient_utils.FileWrite(project_config_file, project_config_data)
999 yield project_config_file
1000
1001
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002def ShortBranchName(branch):
1003 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001004 return branch.replace('refs/heads/', '', 1)
1005
1006
1007def GetCurrentBranchRef():
1008 """Returns branch ref (e.g., refs/heads/master) or None."""
1009 return RunGit(['symbolic-ref', 'HEAD'],
1010 stderr=subprocess2.VOID, error_ok=True).strip() or None
1011
1012
1013def GetCurrentBranch():
1014 """Returns current branch or None.
1015
1016 For refs/heads/* branches, returns just last part. For others, full ref.
1017 """
1018 branchref = GetCurrentBranchRef()
1019 if branchref:
1020 return ShortBranchName(branchref)
1021 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001022
1023
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001024class _CQState(object):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00001025 """Enum for states of CL with respect to CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001026 NONE = 'none'
1027 DRY_RUN = 'dry_run'
1028 COMMIT = 'commit'
1029
1030 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1031
1032
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001033class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001034 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001035 self.issue = issue
1036 self.patchset = patchset
1037 self.hostname = hostname
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +00001038 assert codereview in (None, 'gerrit', 'rietveld')
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001039 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001040
1041 @property
1042 def valid(self):
1043 return self.issue is not None
1044
1045
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001046def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001047 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1048 fail_result = _ParsedIssueNumberArgument()
1049
1050 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001051 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001052 if not arg.startswith('http'):
1053 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001054
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001055 url = gclient_utils.UpgradeToHttps(arg)
1056 try:
1057 parsed_url = urlparse.urlparse(url)
1058 except ValueError:
1059 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001060
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001061 if codereview is not None:
1062 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1063 return parsed or fail_result
1064
Andrii Shyshkalov0a264d82018-11-21 00:36:16 +00001065 return _GerritChangelistImpl.ParseIssueURL(parsed_url) or fail_result
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001066
1067
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001068def _create_description_from_log(args):
1069 """Pulls out the commit log to use as a base for the CL description."""
1070 log_args = []
1071 if len(args) == 1 and not args[0].endswith('.'):
1072 log_args = [args[0] + '..']
1073 elif len(args) == 1 and args[0].endswith('...'):
1074 log_args = [args[0][:-1]]
1075 elif len(args) == 2:
1076 log_args = [args[0] + '..' + args[1]]
1077 else:
1078 log_args = args[:] # Hope for the best!
1079 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
1080
1081
Aaron Gablea45ee112016-11-22 15:14:38 -08001082class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001083 def __init__(self, issue, url):
1084 self.issue = issue
1085 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001086 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001087
1088 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001089 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001090 self.issue, self.url)
1091
1092
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001093_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001094 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001095 # TODO(tandrii): these two aren't known in Gerrit.
1096 'approval', 'disapproval'])
1097
1098
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001100 """Changelist works with one changelist in local branch.
1101
1102 Supports two codereview backends: Rietveld or Gerrit, selected at object
1103 creation.
1104
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001105 Notes:
1106 * Not safe for concurrent multi-{thread,process} use.
1107 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001108 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001109 """
1110
1111 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1112 """Create a new ChangeList instance.
1113
1114 If issue is given, the codereview must be given too.
1115
1116 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1117 Otherwise, it's decided based on current configuration of the local branch,
1118 with default being 'rietveld' for backwards compatibility.
1119 See _load_codereview_impl for more details.
1120
1121 **kwargs will be passed directly to codereview implementation.
1122 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001123 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001124 global settings
1125 if not settings:
1126 # Happens when git_cl.py is used as a utility library.
1127 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001128
1129 if issue:
1130 assert codereview, 'codereview must be known, if issue is known'
1131
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001132 self.branchref = branchref
1133 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001134 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001135 self.branch = ShortBranchName(self.branchref)
1136 else:
1137 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001139 self.lookedup_issue = False
1140 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141 self.has_description = False
1142 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001143 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001144 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001145 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001146 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001147 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001148 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001149
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001150 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001151 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001152 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001153 assert self._codereview_impl
1154 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001155
1156 def _load_codereview_impl(self, codereview=None, **kwargs):
1157 if codereview:
Joe Masond87b0962018-12-03 21:04:46 +00001158 assert codereview in _CODEREVIEW_IMPLEMENTATIONS, (
1159 'codereview {} not in {}'.format(codereview,
1160 _CODEREVIEW_IMPLEMENTATIONS))
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001161 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1162 self._codereview = codereview
1163 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001164 return
1165
1166 # Automatic selection based on issue number set for a current branch.
1167 # Rietveld takes precedence over Gerrit.
1168 assert not self.issue
1169 # Whether we find issue or not, we are doing the lookup.
1170 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001171 if self.GetBranch():
1172 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1173 issue = _git_get_branch_config_value(
1174 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1175 if issue:
1176 self._codereview = codereview
1177 self._codereview_impl = cls(self, **kwargs)
1178 self.issue = int(issue)
1179 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001180
Bryce Thomascfc97122018-12-13 20:21:47 +00001181 # No issue is set for this branch, so default to gerrit.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001182 return self._load_codereview_impl(
Bryce Thomascfc97122018-12-13 20:21:47 +00001183 codereview='gerrit',
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001184 **kwargs)
1185
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001186 def IsGerrit(self):
1187 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001188
1189 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001190 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001191
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001192 The return value is a string suitable for passing to git cl with the --cc
1193 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001194 """
1195 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001196 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001197 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001198 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1199 return self.cc
1200
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001201 def GetCCListWithoutDefault(self):
1202 """Return the users cc'd on this CL excluding default ones."""
1203 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001204 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001205 return self.cc
1206
Daniel Cheng7227d212017-11-17 08:12:37 -08001207 def ExtendCC(self, more_cc):
1208 """Extends the list of users to cc on this CL based on the changed files."""
1209 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210
1211 def GetBranch(self):
1212 """Returns the short branch name, e.g. 'master'."""
1213 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001214 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001215 if not branchref:
1216 return None
1217 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218 self.branch = ShortBranchName(self.branchref)
1219 return self.branch
1220
1221 def GetBranchRef(self):
1222 """Returns the full branch name, e.g. 'refs/heads/master'."""
1223 self.GetBranch() # Poke the lazy loader.
1224 return self.branchref
1225
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001226 def ClearBranch(self):
1227 """Clears cached branch data of this object."""
1228 self.branch = self.branchref = None
1229
tandrii5d48c322016-08-18 16:19:37 -07001230 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1231 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1232 kwargs['branch'] = self.GetBranch()
1233 return _git_get_branch_config_value(key, default, **kwargs)
1234
1235 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1236 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1237 assert self.GetBranch(), (
1238 'this CL must have an associated branch to %sset %s%s' %
1239 ('un' if value is None else '',
1240 key,
1241 '' if value is None else ' to %r' % value))
1242 kwargs['branch'] = self.GetBranch()
1243 return _git_set_branch_config_value(key, value, **kwargs)
1244
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001245 @staticmethod
1246 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001247 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001248 e.g. 'origin', 'refs/heads/master'
1249 """
1250 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001251 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1252
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001253 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001254 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001256 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1257 error_ok=True).strip()
1258 if upstream_branch:
1259 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001261 # Else, try to guess the origin remote.
1262 remote_branches = RunGit(['branch', '-r']).split()
1263 if 'origin/master' in remote_branches:
1264 # Fall back on origin/master if it exits.
1265 remote = 'origin'
1266 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001268 DieWithError(
1269 'Unable to determine default branch to diff against.\n'
1270 'Either pass complete "git diff"-style arguments, like\n'
1271 ' git cl upload origin/master\n'
1272 'or verify this branch is set up to track another \n'
1273 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274
1275 return remote, upstream_branch
1276
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001277 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001278 upstream_branch = self.GetUpstreamBranch()
1279 if not BranchExists(upstream_branch):
1280 DieWithError('The upstream for the current branch (%s) does not exist '
1281 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001282 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001283 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001284
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001285 def GetUpstreamBranch(self):
1286 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001287 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Raul Tambrefe1dbe12019-05-02 04:43:57 +00001288 if remote != '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001289 upstream_branch = upstream_branch.replace('refs/heads/',
1290 'refs/remotes/%s/' % remote)
1291 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1292 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293 self.upstream_branch = upstream_branch
1294 return self.upstream_branch
1295
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001296 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001297 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001298 remote, branch = None, self.GetBranch()
1299 seen_branches = set()
1300 while branch not in seen_branches:
1301 seen_branches.add(branch)
1302 remote, branch = self.FetchUpstreamTuple(branch)
1303 branch = ShortBranchName(branch)
1304 if remote != '.' or branch.startswith('refs/remotes'):
1305 break
1306 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001307 remotes = RunGit(['remote'], error_ok=True).split()
1308 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001309 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001310 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001311 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001312 logging.warn('Could not determine which remote this change is '
1313 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001314 else:
1315 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001316 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001317 branch = 'HEAD'
1318 if branch.startswith('refs/remotes'):
1319 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001320 elif branch.startswith('refs/branch-heads/'):
1321 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001322 else:
1323 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001324 return self._remote
1325
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001326 def GitSanityChecks(self, upstream_git_obj):
1327 """Checks git repo status and ensures diff is from local commits."""
1328
sbc@chromium.org79706062015-01-14 21:18:12 +00001329 if upstream_git_obj is None:
1330 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001331 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001332 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001333 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001334 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001335 return False
1336
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001337 # Verify the commit we're diffing against is in our current branch.
1338 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1339 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1340 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001341 print('ERROR: %s is not in the current branch. You may need to rebase '
1342 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001343 return False
1344
1345 # List the commits inside the diff, and verify they are all local.
1346 commits_in_diff = RunGit(
1347 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1348 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1349 remote_branch = remote_branch.strip()
1350 if code != 0:
1351 _, remote_branch = self.GetRemoteBranch()
1352
1353 commits_in_remote = RunGit(
1354 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1355
1356 common_commits = set(commits_in_diff) & set(commits_in_remote)
1357 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001358 print('ERROR: Your diff contains %d commits already in %s.\n'
1359 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1360 'the diff. If you are using a custom git flow, you can override'
1361 ' the reference used for this check with "git config '
1362 'gitcl.remotebranch <git-ref>".' % (
1363 len(common_commits), remote_branch, upstream_git_obj),
1364 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001365 return False
1366 return True
1367
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001368 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001369 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001370
1371 Returns None if it is not set.
1372 """
tandrii5d48c322016-08-18 16:19:37 -07001373 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001374
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001375 def GetRemoteUrl(self):
1376 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1377
1378 Returns None if there is no remote.
1379 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001380 is_cached, value = self._cached_remote_url
1381 if is_cached:
1382 return value
1383
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001384 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001385 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1386
Edward Lemur298f2cf2019-02-22 21:40:39 +00001387 # Check if the remote url can be parsed as an URL.
1388 host = urlparse.urlparse(url).netloc
1389 if host:
1390 self._cached_remote_url = (True, url)
1391 return url
1392
1393 # If it cannot be parsed as an url, assume it is a local directory, probably
1394 # a git cache.
1395 logging.warning('"%s" doesn\'t appear to point to a git host. '
1396 'Interpreting it as a local directory.', url)
1397 if not os.path.isdir(url):
1398 logging.error(
1399 'Remote "%s" for branch "%s" points to "%s", but it doesn\'t exist.',
1400 remote, url, self.GetBranch())
1401 return None
1402
1403 cache_path = url
1404 url = RunGit(['config', 'remote.%s.url' % remote],
1405 error_ok=True,
1406 cwd=url).strip()
1407
1408 host = urlparse.urlparse(url).netloc
1409 if not host:
1410 logging.error(
1411 'Remote "%(remote)s" for branch "%(branch)s" points to '
1412 '"%(cache_path)s", but it is misconfigured.\n'
1413 '"%(cache_path)s" must be a git repo and must have a remote named '
1414 '"%(remote)s" pointing to the git host.', {
1415 'remote': remote,
1416 'cache_path': cache_path,
1417 'branch': self.GetBranch()})
1418 return None
1419
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001420 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001421 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001423 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001424 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001425 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001426 self.issue = self._GitGetBranchConfigValue(
1427 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001428 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001429 return self.issue
1430
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431 def GetIssueURL(self):
1432 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001433 issue = self.GetIssue()
1434 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001435 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001436 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001437
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001438 def GetDescription(self, pretty=False, force=False):
1439 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001441 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001442 self.has_description = True
1443 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001444 # Set width to 72 columns + 2 space indent.
1445 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001446 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001447 lines = self.description.splitlines()
1448 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001449 return self.description
1450
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001451 def GetDescriptionFooters(self):
1452 """Returns (non_footer_lines, footers) for the commit message.
1453
1454 Returns:
1455 non_footer_lines (list(str)) - Simple list of description lines without
1456 any footer. The lines do not contain newlines, nor does the list contain
1457 the empty line between the message and the footers.
1458 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1459 [("Change-Id", "Ideadbeef...."), ...]
1460 """
1461 raw_description = self.GetDescription()
1462 msg_lines, _, footers = git_footers.split_footers(raw_description)
1463 if footers:
1464 msg_lines = msg_lines[:len(msg_lines)-1]
1465 return msg_lines, footers
1466
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001467 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001468 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001469 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001470 self.patchset = self._GitGetBranchConfigValue(
1471 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001472 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001473 return self.patchset
1474
1475 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001476 """Set this branch's patchset. If patchset=0, clears the patchset."""
1477 assert self.GetBranch()
1478 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001479 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001480 else:
1481 self.patchset = int(patchset)
1482 self._GitSetBranchConfigValue(
1483 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001484
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001485 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001486 """Set this branch's issue. If issue isn't given, clears the issue."""
1487 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001488 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001489 issue = int(issue)
1490 self._GitSetBranchConfigValue(
1491 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001492 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001493 codereview_server = self._codereview_impl.GetCodereviewServer()
1494 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001495 self._GitSetBranchConfigValue(
1496 self._codereview_impl.CodereviewServerConfigKey(),
1497 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001498 else:
tandrii5d48c322016-08-18 16:19:37 -07001499 # Reset all of these just to be clean.
1500 reset_suffixes = [
1501 'last-upload-hash',
1502 self._codereview_impl.IssueConfigKey(),
1503 self._codereview_impl.PatchsetConfigKey(),
1504 self._codereview_impl.CodereviewServerConfigKey(),
1505 ] + self._PostUnsetIssueProperties()
1506 for prop in reset_suffixes:
1507 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001508 msg = RunGit(['log', '-1', '--format=%B']).strip()
1509 if msg and git_footers.get_footer_change_id(msg):
1510 print('WARNING: The change patched into this branch has a Change-Id. '
1511 'Removing it.')
1512 RunGit(['commit', '--amend', '-m',
1513 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001514 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001515 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001516
dnjba1b0f32016-09-02 12:37:42 -07001517 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001518 if not self.GitSanityChecks(upstream_branch):
1519 DieWithError('\nGit sanity check failure')
1520
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001521 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001522 if not root:
1523 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001524 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001525
1526 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001527 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001528 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001529 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001530 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001531 except subprocess2.CalledProcessError:
1532 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001533 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001534 'This branch probably doesn\'t exist anymore. To reset the\n'
1535 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001536 ' git branch --set-upstream-to origin/master %s\n'
1537 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001538 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001539
maruel@chromium.org52424302012-08-29 15:14:30 +00001540 issue = self.GetIssue()
1541 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001542 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001543 description = self.GetDescription()
1544 else:
1545 # If the change was never uploaded, use the log messages of all commits
1546 # up to the branch point, as git cl upload will prefill the description
1547 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001548 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1549 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001550
1551 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001552 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001553 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001554 name,
1555 description,
1556 absroot,
1557 files,
1558 issue,
1559 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001560 author,
1561 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001562
dsansomee2d6fd92016-09-08 00:10:47 -07001563 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001564 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001565 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001566 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001567
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001568 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1569 """Sets the description for this CL remotely.
1570
1571 You can get description_lines and footers with GetDescriptionFooters.
1572
1573 Args:
1574 description_lines (list(str)) - List of CL description lines without
1575 newline characters.
1576 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1577 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1578 `List-Of-Tokens`). It will be case-normalized so that each token is
1579 title-cased.
1580 """
1581 new_description = '\n'.join(description_lines)
1582 if footers:
1583 new_description += '\n'
1584 for k, v in footers:
1585 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1586 if not git_footers.FOOTER_PATTERN.match(foot):
1587 raise ValueError('Invalid footer %r' % foot)
1588 new_description += foot + '\n'
1589 self.UpdateDescription(new_description, force)
1590
Edward Lesmes8e282792018-04-03 18:50:29 -04001591 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001592 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1593 try:
Edward Lemur2c48f242019-06-04 16:14:09 +00001594 start = time_time()
1595 result = presubmit_support.DoPresubmitChecks(change, committing,
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001596 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1597 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001598 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1599 parallel=parallel)
Edward Lemur2c48f242019-06-04 16:14:09 +00001600 metrics.collector.add_repeated('sub_commands', {
1601 'command': 'presubmit',
1602 'execution_time': time_time() - start,
1603 'exit_code': 0 if result.should_continue() else 1,
1604 })
1605 return result
vapierfd77ac72016-06-16 08:33:57 -07001606 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001607 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001608
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001609 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1610 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001611 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1612 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001613 else:
1614 # Assume url.
1615 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1616 urlparse.urlparse(issue_arg))
1617 if not parsed_issue_arg or not parsed_issue_arg.valid:
1618 DieWithError('Failed to parse issue argument "%s". '
1619 'Must be an issue number or a valid URL.' % issue_arg)
1620 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001621 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001622
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001623 def CMDUpload(self, options, git_diff_args, orig_args):
1624 """Uploads a change to codereview."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00001625 assert self.IsGerrit()
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001626 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001627 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001628 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001629 else:
1630 if self.GetBranch() is None:
1631 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1632
1633 # Default to diffing against common ancestor of upstream branch
1634 base_branch = self.GetCommonAncestorWithUpstream()
1635 git_diff_args = [base_branch, 'HEAD']
1636
Aaron Gablec4c40d12017-05-22 11:49:53 -07001637
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001638 # Fast best-effort checks to abort before running potentially
1639 # expensive hooks if uploading is likely to fail anyway. Passing these
1640 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001641 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001642 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001643
1644 # Apply watchlists on upload.
1645 change = self.GetChange(base_branch, None)
1646 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1647 files = [f.LocalPath() for f in change.AffectedFiles()]
1648 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001649 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001650
1651 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001652 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001653 # Set the reviewer list now so that presubmit checks can access it.
1654 change_description = ChangeDescription(change.FullDescriptionText())
1655 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001656 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001657 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001658 change)
1659 change.SetDescriptionText(change_description.description)
1660 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001661 may_prompt=not options.force,
1662 verbose=options.verbose,
1663 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001664 if not hook_results.should_continue():
1665 return 1
1666 if not options.reviewers and hook_results.reviewers:
1667 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001668 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001669
Aaron Gable13101a62018-02-09 13:20:41 -08001670 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001671 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001672 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001673 _git_set_branch_config_value('last-upload-hash',
1674 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001675 # Run post upload hooks, if specified.
1676 if settings.GetRunPostUploadHook():
1677 presubmit_support.DoPostUploadExecuter(
1678 change,
1679 self,
1680 settings.GetRoot(),
1681 options.verbose,
1682 sys.stdout)
1683
1684 # Upload all dependencies if specified.
1685 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001686 print()
1687 print('--dependencies has been specified.')
1688 print('All dependent local branches will be re-uploaded.')
1689 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001690 # Remove the dependencies flag from args so that we do not end up in a
1691 # loop.
1692 orig_args.remove('--dependencies')
1693 ret = upload_branch_deps(self, orig_args)
1694 return ret
1695
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001696 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001697 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001698
1699 Issue must have been already uploaded and known.
1700 """
1701 assert new_state in _CQState.ALL_STATES
1702 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001703 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001704 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001705 return 0
1706 except KeyboardInterrupt:
1707 raise
1708 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001709 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001710 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001711 ' * Your project has no CQ,\n'
1712 ' * You don\'t have permission to change the CQ state,\n'
1713 ' * There\'s a bug in this code (see stack trace below).\n'
1714 'Consider specifying which bots to trigger manually or asking your '
1715 'project owners for permissions or contacting Chrome Infra at:\n'
1716 'https://www.chromium.org/infra\n\n' %
1717 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001718 # Still raise exception so that stack trace is printed.
1719 raise
1720
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001721 # Forward methods to codereview specific implementation.
1722
Aaron Gable636b13f2017-07-14 10:42:48 -07001723 def AddComment(self, message, publish=None):
1724 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001725
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001726 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001727 """Returns list of _CommentSummary for each comment.
1728
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001729 args:
1730 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001731 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001732 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001733
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001734 def CloseIssue(self):
1735 return self._codereview_impl.CloseIssue()
1736
1737 def GetStatus(self):
1738 return self._codereview_impl.GetStatus()
1739
1740 def GetCodereviewServer(self):
1741 return self._codereview_impl.GetCodereviewServer()
1742
tandriide281ae2016-10-12 06:02:30 -07001743 def GetIssueOwner(self):
1744 """Get owner from codereview, which may differ from this checkout."""
1745 return self._codereview_impl.GetIssueOwner()
1746
Edward Lemur707d70b2018-02-07 00:50:14 +01001747 def GetReviewers(self):
1748 return self._codereview_impl.GetReviewers()
1749
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001750 def GetMostRecentPatchset(self):
1751 return self._codereview_impl.GetMostRecentPatchset()
1752
tandriide281ae2016-10-12 06:02:30 -07001753 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001754 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001755 return self._codereview_impl.CannotTriggerTryJobReason()
1756
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001757 def GetTryJobProperties(self, patchset=None):
1758 """Returns dictionary of properties to launch try job."""
1759 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001760
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001761 def __getattr__(self, attr):
1762 # This is because lots of untested code accesses Rietveld-specific stuff
1763 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001764 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001765 # Note that child method defines __getattr__ as well, and forwards it here,
1766 # because _RietveldChangelistImpl is not cleaned up yet, and given
1767 # deprecation of Rietveld, it should probably be just removed.
1768 # Until that time, avoid infinite recursion by bypassing __getattr__
1769 # of implementation class.
1770 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001771
1772
1773class _ChangelistCodereviewBase(object):
1774 """Abstract base class encapsulating codereview specifics of a changelist."""
1775 def __init__(self, changelist):
1776 self._changelist = changelist # instance of Changelist
1777
1778 def __getattr__(self, attr):
1779 # Forward methods to changelist.
1780 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1781 # _RietveldChangelistImpl to avoid this hack?
1782 return getattr(self._changelist, attr)
1783
1784 def GetStatus(self):
1785 """Apply a rough heuristic to give a simple summary of an issue's review
1786 or CQ status, assuming adherence to a common workflow.
1787
1788 Returns None if no issue for this branch, or specific string keywords.
1789 """
1790 raise NotImplementedError()
1791
1792 def GetCodereviewServer(self):
1793 """Returns server URL without end slash, like "https://codereview.com"."""
1794 raise NotImplementedError()
1795
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001796 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001797 """Fetches and returns description from the codereview server."""
1798 raise NotImplementedError()
1799
tandrii5d48c322016-08-18 16:19:37 -07001800 @classmethod
1801 def IssueConfigKey(cls):
1802 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001803 raise NotImplementedError()
1804
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001805 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001806 def PatchsetConfigKey(cls):
1807 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001808 raise NotImplementedError()
1809
tandrii5d48c322016-08-18 16:19:37 -07001810 @classmethod
1811 def CodereviewServerConfigKey(cls):
1812 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001813 raise NotImplementedError()
1814
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001815 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001816 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001817 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001818
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001819 def GetGerritObjForPresubmit(self):
1820 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1821 return None
1822
dsansomee2d6fd92016-09-08 00:10:47 -07001823 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001824 """Update the description on codereview site."""
1825 raise NotImplementedError()
1826
Aaron Gable636b13f2017-07-14 10:42:48 -07001827 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001828 """Posts a comment to the codereview site."""
1829 raise NotImplementedError()
1830
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001831 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001832 raise NotImplementedError()
1833
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001834 def CloseIssue(self):
1835 """Closes the issue."""
1836 raise NotImplementedError()
1837
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001838 def GetMostRecentPatchset(self):
1839 """Returns the most recent patchset number from the codereview site."""
1840 raise NotImplementedError()
1841
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001842 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001843 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001844 """Fetches and applies the issue.
1845
1846 Arguments:
1847 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1848 reject: if True, reject the failed patch instead of switching to 3-way
1849 merge. Rietveld only.
1850 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1851 only.
1852 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001853 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001854 """
1855 raise NotImplementedError()
1856
1857 @staticmethod
1858 def ParseIssueURL(parsed_url):
1859 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1860 failed."""
1861 raise NotImplementedError()
1862
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001863 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001864 """Best effort check that user is authenticated with codereview server.
1865
1866 Arguments:
1867 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001868 refresh: whether to attempt to refresh credentials. Ignored if not
1869 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001870 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001871 raise NotImplementedError()
1872
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001873 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001874 """Best effort check that uploading isn't supposed to fail for predictable
1875 reasons.
1876
1877 This method should raise informative exception if uploading shouldn't
1878 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001879
1880 Arguments:
1881 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001882 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001883 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001884
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001885 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001886 """Uploads a change to codereview."""
1887 raise NotImplementedError()
1888
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001889 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001890 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001891
1892 Issue must have been already uploaded and known.
1893 """
1894 raise NotImplementedError()
1895
tandriie113dfd2016-10-11 10:20:12 -07001896 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001897 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001898 raise NotImplementedError()
1899
tandriide281ae2016-10-12 06:02:30 -07001900 def GetIssueOwner(self):
1901 raise NotImplementedError()
1902
Edward Lemur707d70b2018-02-07 00:50:14 +01001903 def GetReviewers(self):
1904 raise NotImplementedError()
1905
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001906 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001907 raise NotImplementedError()
1908
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001909
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001910class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001911 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001912 # auth_config is Rietveld thing, kept here to preserve interface only.
1913 super(_GerritChangelistImpl, self).__init__(changelist)
1914 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001915 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001916 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001917 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001918 # Map from change number (issue) to its detail cache.
1919 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001920
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001921 if codereview_host is not None:
1922 assert not codereview_host.startswith('https://'), codereview_host
1923 self._gerrit_host = codereview_host
1924 self._gerrit_server = 'https://%s' % codereview_host
1925
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001926 def _GetGerritHost(self):
1927 # Lazy load of configs.
1928 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001929 if self._gerrit_host and '.' not in self._gerrit_host:
1930 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1931 # This happens for internal stuff http://crbug.com/614312.
1932 parsed = urlparse.urlparse(self.GetRemoteUrl())
1933 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001934 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07001935 ' Your current remote is: %s' % self.GetRemoteUrl())
1936 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1937 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001938 return self._gerrit_host
1939
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001940 def _GetGitHost(self):
1941 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001942 remote_url = self.GetRemoteUrl()
1943 if not remote_url:
1944 return None
1945 return urlparse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001946
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001947 def GetCodereviewServer(self):
1948 if not self._gerrit_server:
1949 # If we're on a branch then get the server potentially associated
1950 # with that branch.
1951 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001952 self._gerrit_server = self._GitGetBranchConfigValue(
1953 self.CodereviewServerConfigKey())
1954 if self._gerrit_server:
1955 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001956 if not self._gerrit_server:
1957 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1958 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001959 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001960 parts[0] = parts[0] + '-review'
1961 self._gerrit_host = '.'.join(parts)
1962 self._gerrit_server = 'https://%s' % self._gerrit_host
1963 return self._gerrit_server
1964
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001965 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001966 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001967 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001968 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001969 logging.warn('can\'t detect Gerrit project.')
1970 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001971 project = urlparse.urlparse(remote_url).path.strip('/')
1972 if project.endswith('.git'):
1973 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001974 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1975 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1976 # gitiles/git-over-https protocol. E.g.,
1977 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1978 # as
1979 # https://chromium.googlesource.com/v8/v8
1980 if project.startswith('a/'):
1981 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001982 return project
1983
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001984 def _GerritChangeIdentifier(self):
1985 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1986
1987 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001988 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001989 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001990 project = self._GetGerritProject()
1991 if project:
1992 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1993 # Fall back on still unique, but less efficient change number.
1994 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001995
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001996 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001997 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001998 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001999
tandrii5d48c322016-08-18 16:19:37 -07002000 @classmethod
2001 def PatchsetConfigKey(cls):
2002 return 'gerritpatchset'
2003
2004 @classmethod
2005 def CodereviewServerConfigKey(cls):
2006 return 'gerritserver'
2007
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002008 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002009 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002010 if settings.GetGerritSkipEnsureAuthenticated():
2011 # For projects with unusual authentication schemes.
2012 # See http://crbug.com/603378.
2013 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002014
2015 # Check presence of cookies only if using cookies-based auth method.
2016 cookie_auth = gerrit_util.Authenticator.get()
2017 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002018 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002019
Daniel Chengcf6269b2019-05-18 01:02:12 +00002020 if urlparse.urlparse(self.GetRemoteUrl()).scheme != 'https':
2021 print('WARNING: Ignoring branch %s with non-https remote %s' %
2022 (self._changelist.branch, self.GetRemoteUrl()))
2023 return
2024
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002025 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002026 self.GetCodereviewServer()
2027 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00002028 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002029
2030 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2031 git_auth = cookie_auth.get_auth_header(git_host)
2032 if gerrit_auth and git_auth:
2033 if gerrit_auth == git_auth:
2034 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002035 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
Raul Tambre80ee78e2019-05-06 22:41:05 +00002036 print(
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002037 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002038 ' %s\n'
2039 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002040 ' Consider running the following command:\n'
2041 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002042 ' %s\n'
Raul Tambre80ee78e2019-05-06 22:41:05 +00002043 ' %s' %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002044 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002045 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002046 cookie_auth.get_new_password_message(git_host)))
2047 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002048 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002049 return
2050 else:
2051 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002052 ([] if gerrit_auth else [self._gerrit_host]) +
2053 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002054 DieWithError('Credentials for the following hosts are required:\n'
2055 ' %s\n'
2056 'These are read from %s (or legacy %s)\n'
2057 '%s' % (
2058 '\n '.join(missing),
2059 cookie_auth.get_gitcookies_path(),
2060 cookie_auth.get_netrc_path(),
2061 cookie_auth.get_new_password_message(git_host)))
2062
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002063 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002064 if not self.GetIssue():
2065 return
2066
2067 # Warm change details cache now to avoid RPCs later, reducing latency for
2068 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002069 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002070 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002071
2072 status = self._GetChangeDetail()['status']
2073 if status in ('MERGED', 'ABANDONED'):
2074 DieWithError('Change %s has been %s, new uploads are not allowed' %
2075 (self.GetIssueURL(),
2076 'submitted' if status == 'MERGED' else 'abandoned'))
2077
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002078 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2079 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2080 # Apparently this check is not very important? Otherwise get_auth_email
2081 # could have been added to other implementations of Authenticator.
2082 cookies_auth = gerrit_util.Authenticator.get()
2083 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002084 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002085
2086 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002087 if self.GetIssueOwner() == cookies_user:
2088 return
2089 logging.debug('change %s owner is %s, cookies user is %s',
2090 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002091 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002092 # so ask what Gerrit thinks of this user.
2093 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2094 if details['email'] == self.GetIssueOwner():
2095 return
2096 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002097 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002098 'as %s.\n'
2099 'Uploading may fail due to lack of permissions.' %
2100 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2101 confirm_or_exit(action='upload')
2102
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002103 def _PostUnsetIssueProperties(self):
2104 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002105 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002106
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002107 def GetGerritObjForPresubmit(self):
2108 return presubmit_support.GerritAccessor(self._GetGerritHost())
2109
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002110 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002111 """Apply a rough heuristic to give a simple summary of an issue's review
2112 or CQ status, assuming adherence to a common workflow.
2113
2114 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002115 * 'error' - error from review tool (including deleted issues)
2116 * 'unsent' - no reviewers added
2117 * 'waiting' - waiting for review
2118 * 'reply' - waiting for uploader to reply to review
2119 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002120 * 'dry-run' - dry-running in the CQ
2121 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07002122 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002123 """
2124 if not self.GetIssue():
2125 return None
2126
2127 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002128 data = self._GetChangeDetail([
2129 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002130 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002131 return 'error'
2132
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002133 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002134 return 'closed'
2135
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002136 cq_label = data['labels'].get('Commit-Queue', {})
2137 max_cq_vote = 0
2138 for vote in cq_label.get('all', []):
2139 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
2140 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002141 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002142 if max_cq_vote == 1:
2143 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002144
Aaron Gable9ab38c62017-04-06 14:36:33 -07002145 if data['labels'].get('Code-Review', {}).get('approved'):
2146 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002147
2148 if not data.get('reviewers', {}).get('REVIEWER', []):
2149 return 'unsent'
2150
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002151 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002152 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2153 last_message_author = messages.pop().get('author', {})
2154 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002155 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2156 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002157 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002158 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002159 if last_message_author.get('_account_id') == owner:
2160 # Most recent message was by owner.
2161 return 'waiting'
2162 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002163 # Some reply from non-owner.
2164 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002165
2166 # Somehow there are no messages even though there are reviewers.
2167 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002168
2169 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002170 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002171 patchset = data['revisions'][data['current_revision']]['_number']
2172 self.SetPatchset(patchset)
2173 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002174
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002175 def FetchDescription(self, force=False):
2176 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2177 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002178 current_rev = data['current_revision']
Dan Beamcf6df902018-11-08 01:48:37 +00002179 return data['revisions'][current_rev]['commit']['message'].encode(
2180 'utf-8', 'ignore')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002181
dsansomee2d6fd92016-09-08 00:10:47 -07002182 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002183 if gerrit_util.HasPendingChangeEdit(
2184 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002185 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002186 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002187 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002188 'unpublished edit. Either publish the edit in the Gerrit web UI '
2189 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002190
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002191 gerrit_util.DeletePendingChangeEdit(
2192 self._GetGerritHost(), self._GerritChangeIdentifier())
2193 gerrit_util.SetCommitMessage(
2194 self._GetGerritHost(), self._GerritChangeIdentifier(),
2195 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002196
Aaron Gable636b13f2017-07-14 10:42:48 -07002197 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002198 gerrit_util.SetReview(
2199 self._GetGerritHost(), self._GerritChangeIdentifier(),
2200 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002201
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002202 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002203 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002204 # CURRENT_REVISION is included to get the latest patchset so that
2205 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002206 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002207 options=['MESSAGES', 'DETAILED_ACCOUNTS',
2208 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002209 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002210 self._GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002211 robot_file_comments = gerrit_util.GetChangeRobotComments(
2212 self._GetGerritHost(), self._GerritChangeIdentifier())
2213
2214 # Add the robot comments onto the list of comments, but only
2215 # keep those that are from the latest pachset.
2216 latest_patch_set = self.GetMostRecentPatchset()
2217 for path, robot_comments in robot_file_comments.iteritems():
2218 line_comments = file_comments.setdefault(path, [])
2219 line_comments.extend(
2220 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002221
2222 # Build dictionary of file comments for easy access and sorting later.
2223 # {author+date: {path: {patchset: {line: url+message}}}}
2224 comments = collections.defaultdict(
2225 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2226 for path, line_comments in file_comments.iteritems():
2227 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002228 tag = comment.get('tag', '')
2229 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002230 continue
2231 key = (comment['author']['email'], comment['updated'])
2232 if comment.get('side', 'REVISION') == 'PARENT':
2233 patchset = 'Base'
2234 else:
2235 patchset = 'PS%d' % comment['patch_set']
2236 line = comment.get('line', 0)
2237 url = ('https://%s/c/%s/%s/%s#%s%s' %
2238 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2239 'b' if comment.get('side') == 'PARENT' else '',
2240 str(line) if line else ''))
2241 comments[key][path][patchset][line] = (url, comment['message'])
2242
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002243 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002244 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002245 summary = self._BuildCommentSummary(msg, comments, readable)
2246 if summary:
2247 summaries.append(summary)
2248 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002249
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002250 @staticmethod
2251 def _BuildCommentSummary(msg, comments, readable):
2252 key = (msg['author']['email'], msg['date'])
2253 # Don't bother showing autogenerated messages that don't have associated
2254 # file or line comments. this will filter out most autogenerated
2255 # messages, but will keep robot comments like those from Tricium.
2256 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
2257 if is_autogenerated and not comments.get(key):
2258 return None
2259 message = msg['message']
2260 # Gerrit spits out nanoseconds.
2261 assert len(msg['date'].split('.')[-1]) == 9
2262 date = datetime.datetime.strptime(msg['date'][:-3],
2263 '%Y-%m-%d %H:%M:%S.%f')
2264 if key in comments:
2265 message += '\n'
2266 for path, patchsets in sorted(comments.get(key, {}).items()):
2267 if readable:
2268 message += '\n%s' % path
2269 for patchset, lines in sorted(patchsets.items()):
2270 for line, (url, content) in sorted(lines.items()):
2271 if line:
2272 line_str = 'Line %d' % line
2273 path_str = '%s:%d:' % (path, line)
2274 else:
2275 line_str = 'File comment'
2276 path_str = '%s:0:' % path
2277 if readable:
2278 message += '\n %s, %s: %s' % (patchset, line_str, url)
2279 message += '\n %s\n' % content
2280 else:
2281 message += '\n%s ' % path_str
2282 message += '\n%s\n' % content
2283
2284 return _CommentSummary(
2285 date=date,
2286 message=message,
2287 sender=msg['author']['email'],
2288 autogenerated=is_autogenerated,
2289 # These could be inferred from the text messages and correlated with
2290 # Code-Review label maximum, however this is not reliable.
2291 # Leaving as is until the need arises.
2292 approval=False,
2293 disapproval=False,
2294 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002295
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002296 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002297 gerrit_util.AbandonChange(
2298 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002299
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002300 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002301 gerrit_util.SubmitChange(
2302 self._GetGerritHost(), self._GerritChangeIdentifier(),
2303 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002304
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002305 def _GetChangeDetail(self, options=None, no_cache=False):
2306 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002307
2308 If fresh data is needed, set no_cache=True which will clear cache and
2309 thus new data will be fetched from Gerrit.
2310 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002311 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002312 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002313
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002314 # Optimization to avoid multiple RPCs:
2315 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2316 'CURRENT_COMMIT' not in options):
2317 options.append('CURRENT_COMMIT')
2318
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002319 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002320 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002321 options = [o.upper() for o in options]
2322
2323 # Check in cache first unless no_cache is True.
2324 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002325 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002326 else:
2327 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002328 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002329 # Assumption: data fetched before with extra options is suitable
2330 # for return for a smaller set of options.
2331 # For example, if we cached data for
2332 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2333 # and request is for options=[CURRENT_REVISION],
2334 # THEN we can return prior cached data.
2335 if options_set.issubset(cached_options_set):
2336 return data
2337
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002338 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002339 data = gerrit_util.GetChangeDetail(
2340 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002341 except gerrit_util.GerritError as e:
2342 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002343 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002344 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002345
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002346 self._detail_cache.setdefault(cache_key, []).append(
2347 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002348 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002349
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002350 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002351 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002352 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002353 data = gerrit_util.GetChangeCommit(
2354 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002355 except gerrit_util.GerritError as e:
2356 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002357 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002358 raise
agable32978d92016-11-01 12:55:02 -07002359 return data
2360
Karen Qian40c19422019-03-13 21:28:29 +00002361 def _IsCqConfigured(self):
2362 detail = self._GetChangeDetail(['LABELS'])
2363 if not u'Commit-Queue' in detail.get('labels', {}):
2364 return False
2365 # TODO(crbug/753213): Remove temporary hack
2366 if ('https://chromium.googlesource.com/chromium/src' ==
2367 self._changelist.GetRemoteUrl() and
2368 detail['branch'].startswith('refs/branch-heads/')):
2369 return False
2370 return True
2371
Olivier Robin75ee7252018-04-13 10:02:56 +02002372 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002373 if git_common.is_dirty_git_tree('land'):
2374 return 1
Karen Qian40c19422019-03-13 21:28:29 +00002375
tandriid60367b2016-06-22 05:25:12 -07002376 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00002377 if not force and self._IsCqConfigured():
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002378 confirm_or_exit('\nIt seems this repository has a CQ, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002379 'which can test and land changes for you. '
2380 'Are you sure you wish to bypass it?\n',
2381 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002382 differs = True
tandriic4344b52016-08-29 06:04:54 -07002383 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002384 # Note: git diff outputs nothing if there is no diff.
2385 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002386 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002387 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002388 if detail['current_revision'] == last_upload:
2389 differs = False
2390 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002391 print('WARNING: Local branch contents differ from latest uploaded '
2392 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002393 if differs:
2394 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002395 confirm_or_exit(
2396 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2397 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002398 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002399 elif not bypass_hooks:
2400 hook_results = self.RunHook(
2401 committing=True,
2402 may_prompt=not force,
2403 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002404 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2405 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002406 if not hook_results.should_continue():
2407 return 1
2408
2409 self.SubmitIssue(wait_for_merge=True)
2410 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002411 links = self._GetChangeCommit().get('web_links', [])
2412 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002413 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002414 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002415 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002416 return 0
2417
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002418 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002419 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002420 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002421 assert not directory
2422 assert parsed_issue_arg.valid
2423
2424 self._changelist.issue = parsed_issue_arg.issue
2425
2426 if parsed_issue_arg.hostname:
2427 self._gerrit_host = parsed_issue_arg.hostname
2428 self._gerrit_server = 'https://%s' % self._gerrit_host
2429
tandriic2405f52016-10-10 08:13:15 -07002430 try:
2431 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002432 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002433 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002434
2435 if not parsed_issue_arg.patchset:
2436 # Use current revision by default.
2437 revision_info = detail['revisions'][detail['current_revision']]
2438 patchset = int(revision_info['_number'])
2439 else:
2440 patchset = parsed_issue_arg.patchset
2441 for revision_info in detail['revisions'].itervalues():
2442 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2443 break
2444 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002445 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002446 (parsed_issue_arg.patchset, self.GetIssue()))
2447
Aaron Gable697a91b2018-01-19 15:20:15 -08002448 remote_url = self._changelist.GetRemoteUrl()
2449 if remote_url.endswith('.git'):
2450 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002451 remote_url = remote_url.rstrip('/')
2452
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002453 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002454 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002455
2456 if remote_url != fetch_info['url']:
2457 DieWithError('Trying to patch a change from %s but this repo appears '
2458 'to be %s.' % (fetch_info['url'], remote_url))
2459
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002460 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002461
Aaron Gable62619a32017-06-16 08:22:09 -07002462 if force:
2463 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2464 print('Checked out commit for change %i patchset %i locally' %
2465 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002466 elif nocommit:
2467 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2468 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002469 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002470 RunGit(['cherry-pick', 'FETCH_HEAD'])
2471 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002472 (parsed_issue_arg.issue, patchset))
2473 print('Note: this created a local commit which does not have '
2474 'the same hash as the one uploaded for review. This will make '
2475 'uploading changes based on top of this branch difficult.\n'
2476 'If you want to do that, use "git cl patch --force" instead.')
2477
Stefan Zagerd08043c2017-10-12 12:07:02 -07002478 if self.GetBranch():
2479 self.SetIssue(parsed_issue_arg.issue)
2480 self.SetPatchset(patchset)
2481 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2482 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2483 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2484 else:
2485 print('WARNING: You are in detached HEAD state.\n'
2486 'The patch has been applied to your checkout, but you will not be '
2487 'able to upload a new patch set to the gerrit issue.\n'
2488 'Try using the \'-b\' option if you would like to work on a '
2489 'branch and/or upload a new patch set.')
2490
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002491 return 0
2492
2493 @staticmethod
2494 def ParseIssueURL(parsed_url):
2495 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2496 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002497 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2498 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002499 # Short urls like https://domain/<issue_number> can be used, but don't allow
2500 # specifying the patchset (you'd 404), but we allow that here.
2501 if parsed_url.path == '/':
2502 part = parsed_url.fragment
2503 else:
2504 part = parsed_url.path
Bruce Dawson9c062012019-05-02 19:20:28 +00002505 match = re.match(r'(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002506 if match:
2507 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002508 issue=int(match.group(3)),
2509 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002510 hostname=parsed_url.netloc,
2511 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002512 return None
2513
tandrii16e0b4e2016-06-07 10:34:28 -07002514 def _GerritCommitMsgHookCheck(self, offer_removal):
2515 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2516 if not os.path.exists(hook):
2517 return
2518 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2519 # custom developer made one.
2520 data = gclient_utils.FileRead(hook)
2521 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2522 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002523 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002524 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002525 'and may interfere with it in subtle ways.\n'
2526 'We recommend you remove the commit-msg hook.')
2527 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002528 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002529 gclient_utils.rm_file_or_tree(hook)
2530 print('Gerrit commit-msg hook removed.')
2531 else:
2532 print('OK, will keep Gerrit commit-msg hook in place.')
2533
Edward Lemur1b52d872019-05-09 21:12:12 +00002534 def _CleanUpOldTraces(self):
2535 """Keep only the last |MAX_TRACES| traces."""
2536 try:
2537 traces = sorted([
2538 os.path.join(TRACES_DIR, f)
2539 for f in os.listdir(TRACES_DIR)
2540 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2541 and not f.startswith('tmp'))
2542 ])
2543 traces_to_delete = traces[:-MAX_TRACES]
2544 for trace in traces_to_delete:
Daniel Chengcf6269b2019-05-18 01:02:12 +00002545 os.remove(trace)
Edward Lemur1b52d872019-05-09 21:12:12 +00002546 except OSError:
2547 print('WARNING: Failed to remove old git traces from\n'
2548 ' %s'
2549 'Consider removing them manually.' % TRACES_DIR)
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002550
Edward Lemur5737f022019-05-17 01:24:00 +00002551 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
Edward Lemur1b52d872019-05-09 21:12:12 +00002552 """Zip and write the git push traces stored in traces_dir."""
2553 gclient_utils.safe_makedirs(TRACES_DIR)
Edward Lemur1b52d872019-05-09 21:12:12 +00002554 traces_zip = trace_name + '-traces'
2555 traces_readme = trace_name + '-README'
Michael Mosse7f0b4c2019-05-08 04:36:24 +00002556 # Create a temporary dir to store git config and gitcookies in. It will be
2557 # compressed and stored next to the traces.
2558 git_info_dir = tempfile.mkdtemp()
Edward Lemur1b52d872019-05-09 21:12:12 +00002559 git_info_zip = trace_name + '-git-info'
2560
Edward Lemur5737f022019-05-17 01:24:00 +00002561 git_push_metadata['now'] = datetime_now().strftime('%c')
Eric Boren67c48202019-05-30 16:52:51 +00002562 if sys.stdin.encoding and sys.stdin.encoding != 'utf-8':
sangwoo.ko7a614332019-05-22 02:46:19 +00002563 git_push_metadata['now'] = git_push_metadata['now'].decode(
2564 sys.stdin.encoding)
2565
Edward Lemur1b52d872019-05-09 21:12:12 +00002566 git_push_metadata['trace_name'] = trace_name
2567 gclient_utils.FileWrite(
2568 traces_readme, TRACES_README_FORMAT % git_push_metadata)
2569
2570 # Keep only the first 6 characters of the git hashes on the packet
2571 # trace. This greatly decreases size after compression.
2572 packet_traces = os.path.join(traces_dir, 'trace-packet')
2573 if os.path.isfile(packet_traces):
2574 contents = gclient_utils.FileRead(packet_traces)
2575 gclient_utils.FileWrite(
2576 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2577 shutil.make_archive(traces_zip, 'zip', traces_dir)
2578
2579 # Collect and compress the git config and gitcookies.
2580 git_config = RunGit(['config', '-l'])
2581 gclient_utils.FileWrite(
2582 os.path.join(git_info_dir, 'git-config'),
2583 git_config)
2584
2585 cookie_auth = gerrit_util.Authenticator.get()
2586 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2587 gitcookies_path = cookie_auth.get_gitcookies_path()
2588 if os.path.isfile(gitcookies_path):
2589 gitcookies = gclient_utils.FileRead(gitcookies_path)
2590 gclient_utils.FileWrite(
2591 os.path.join(git_info_dir, 'gitcookies'),
2592 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2593 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2594
Edward Lemur1b52d872019-05-09 21:12:12 +00002595 gclient_utils.rmtree(git_info_dir)
2596
2597 def _RunGitPushWithTraces(
2598 self, change_desc, refspec, refspec_opts, git_push_metadata):
2599 """Run git push and collect the traces resulting from the execution."""
2600 # Create a temporary directory to store traces in. Traces will be compressed
2601 # and stored in a 'traces' dir inside depot_tools.
2602 traces_dir = tempfile.mkdtemp()
Edward Lemur5737f022019-05-17 01:24:00 +00002603 trace_name = os.path.join(
2604 TRACES_DIR, datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
Edward Lemur0f58ae42019-04-30 17:24:12 +00002605
2606 env = os.environ.copy()
2607 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2608 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Jonathan Nieder9779b142019-05-29 23:19:29 +00002609 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002610 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2611 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2612 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2613
2614 try:
2615 push_returncode = 0
Edward Lemur1b52d872019-05-09 21:12:12 +00002616 remote_url = self.GetRemoteUrl()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002617 before_push = time_time()
2618 push_stdout = gclient_utils.CheckCallAndFilter(
Edward Lemur1b52d872019-05-09 21:12:12 +00002619 ['git', 'push', remote_url, refspec],
Edward Lemur0f58ae42019-04-30 17:24:12 +00002620 env=env,
2621 print_stdout=True,
2622 # Flush after every line: useful for seeing progress when running as
2623 # recipe.
2624 filter_fn=lambda _: sys.stdout.flush())
2625 except subprocess2.CalledProcessError as e:
2626 push_returncode = e.returncode
2627 DieWithError('Failed to create a change. Please examine output above '
2628 'for the reason of the failure.\n'
2629 'Hint: run command below to diagnose common Git/Gerrit '
2630 'credential problems:\n'
Edward Lemur5737f022019-05-17 01:24:00 +00002631 ' git cl creds-check\n'
2632 '\n'
2633 'If git-cl is not working correctly, file a bug under the '
2634 'Infra>SDK component including the files below.\n'
2635 'Review the files before upload, since they might contain '
2636 'sensitive information.\n'
2637 'Set the Restrict-View-Google label so that they are not '
2638 'publicly accessible.\n'
2639 + TRACES_MESSAGE % {'trace_name': trace_name},
Edward Lemur0f58ae42019-04-30 17:24:12 +00002640 change_desc)
2641 finally:
2642 execution_time = time_time() - before_push
2643 metrics.collector.add_repeated('sub_commands', {
2644 'command': 'git push',
2645 'execution_time': execution_time,
2646 'exit_code': push_returncode,
2647 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2648 })
2649
Edward Lemur1b52d872019-05-09 21:12:12 +00002650 git_push_metadata['execution_time'] = execution_time
2651 git_push_metadata['exit_code'] = push_returncode
Edward Lemur5737f022019-05-17 01:24:00 +00002652 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002653
Edward Lemur1b52d872019-05-09 21:12:12 +00002654 self._CleanUpOldTraces()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002655 gclient_utils.rmtree(traces_dir)
2656
2657 return push_stdout
2658
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002659 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002660 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002661 if options.squash and options.no_squash:
2662 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002663
2664 if not options.squash and not options.no_squash:
2665 # Load default for user, repo, squash=true, in this order.
2666 options.squash = settings.GetSquashGerritUploads()
2667 elif options.no_squash:
2668 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002669
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002670 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002671 branch = GetTargetRef(remote, remote_branch, options.target_branch)
Aaron Gableb56ad332017-01-06 15:24:31 -08002672 # This may be None; default fallback value is determined in logic below.
2673 title = options.title
2674
Dominic Battre7d1c4842017-10-27 09:17:28 +02002675 # Extract bug number from branch name.
2676 bug = options.bug
2677 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2678 if not bug and match:
2679 bug = match.group(1)
2680
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002681 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002682 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002683 if self.GetIssue():
2684 # Try to get the message from a previous upload.
2685 message = self.GetDescription()
2686 if not message:
2687 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002688 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002689 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002690 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002691 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002692 # When uploading a subsequent patchset, -m|--message is taken
2693 # as the patchset title if --title was not provided.
2694 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002695 else:
2696 default_title = RunGit(
2697 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002698 if options.force:
2699 title = default_title
2700 else:
2701 title = ask_for_data(
2702 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002703 change_id = self._GetChangeDetail()['change_id']
2704 while True:
2705 footer_change_ids = git_footers.get_footer_change_id(message)
2706 if footer_change_ids == [change_id]:
2707 break
2708 if not footer_change_ids:
2709 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002710 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002711 continue
2712 # There is already a valid footer but with different or several ids.
2713 # Doing this automatically is non-trivial as we don't want to lose
2714 # existing other footers, yet we want to append just 1 desired
2715 # Change-Id. Thus, just create a new footer, but let user verify the
2716 # new description.
2717 message = '%s\n\nChange-Id: %s' % (message, change_id)
2718 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002719 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002720 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002721 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002722 'Please, check the proposed correction to the description, '
2723 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2724 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2725 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002726 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002727 if not options.force:
2728 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002729 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002730 message = change_desc.description
2731 if not message:
2732 DieWithError("Description is empty. Aborting...")
2733 # Continue the while loop.
2734 # Sanity check of this code - we should end up with proper message
2735 # footer.
2736 assert [change_id] == git_footers.get_footer_change_id(message)
2737 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002738 else: # if not self.GetIssue()
2739 if options.message:
2740 message = options.message
2741 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002742 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002743 if options.title:
2744 message = options.title + '\n\n' + message
2745 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002746
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002747 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002748 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002749 # On first upload, patchset title is always this string, while
2750 # --title flag gets converted to first line of message.
2751 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002752 if not change_desc.description:
2753 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002754 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002755 if len(change_ids) > 1:
2756 DieWithError('too many Change-Id footers, at most 1 allowed.')
2757 if not change_ids:
2758 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002759 change_desc.set_description(git_footers.add_footer_change_id(
2760 change_desc.description,
2761 GenerateGerritChangeId(change_desc.description)))
2762 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002763 assert len(change_ids) == 1
2764 change_id = change_ids[0]
2765
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002766 if options.reviewers or options.tbrs or options.add_owners_to:
2767 change_desc.update_reviewers(options.reviewers, options.tbrs,
2768 options.add_owners_to, change)
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002769 if options.preserve_tryjobs:
2770 change_desc.set_preserve_tryjobs()
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002771
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002772 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002773 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2774 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002775 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002776 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2777 desc_tempfile.write(change_desc.description)
2778 desc_tempfile.close()
2779 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2780 '-F', desc_tempfile.name]).strip()
2781 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002782 else:
2783 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002784 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002785 if not change_desc.description:
2786 DieWithError("Description is empty. Aborting...")
2787
2788 if not git_footers.get_footer_change_id(change_desc.description):
2789 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002790 change_desc.set_description(
2791 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002792 if options.reviewers or options.tbrs or options.add_owners_to:
2793 change_desc.update_reviewers(options.reviewers, options.tbrs,
2794 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002795 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002796 # For no-squash mode, we assume the remote called "origin" is the one we
2797 # want. It is not worthwhile to support different workflows for
2798 # no-squash mode.
2799 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002800 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2801
2802 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002803 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002804 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2805 ref_to_push)]).splitlines()
2806 if len(commits) > 1:
2807 print('WARNING: This will upload %d commits. Run the following command '
2808 'to see which commits will be uploaded: ' % len(commits))
2809 print('git log %s..%s' % (parent, ref_to_push))
2810 print('You can also use `git squash-branch` to squash these into a '
2811 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002812 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002813
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002814 if options.reviewers or options.tbrs or options.add_owners_to:
2815 change_desc.update_reviewers(options.reviewers, options.tbrs,
2816 options.add_owners_to, change)
2817
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002818 reviewers = sorted(change_desc.get_reviewers())
2819 # Add cc's from the CC_LIST and --cc flag (if any).
2820 if not options.private and not options.no_autocc:
2821 cc = self.GetCCList().split(',')
2822 else:
2823 cc = []
2824 if options.cc:
2825 cc.extend(options.cc)
2826 cc = filter(None, [email.strip() for email in cc])
2827 if change_desc.get_cced():
2828 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002829 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2830 valid_accounts = set(reviewers + cc)
2831 # TODO(crbug/877717): relax this for all hosts.
2832 else:
2833 valid_accounts = gerrit_util.ValidAccounts(
2834 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002835 logging.info('accounts %s are recognized, %s invalid',
2836 sorted(valid_accounts),
2837 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002838
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002839 # Extra options that can be specified at push time. Doc:
2840 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002841 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002842
Aaron Gable844cf292017-06-28 11:32:59 -07002843 # By default, new changes are started in WIP mode, and subsequent patchsets
2844 # don't send email. At any time, passing --send-mail will mark the change
2845 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002846 if options.send_mail:
2847 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002848 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002849 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002850 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002851 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002852 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002853
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002854 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002855 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002856
Aaron Gable9b713dd2016-12-14 16:04:21 -08002857 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002858 # Punctuation and whitespace in |title| must be percent-encoded.
2859 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002860
agablec6787972016-09-09 16:13:34 -07002861 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002862 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002863
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002864 for r in sorted(reviewers):
2865 if r in valid_accounts:
2866 refspec_opts.append('r=%s' % r)
2867 reviewers.remove(r)
2868 else:
2869 # TODO(tandrii): this should probably be a hard failure.
2870 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2871 % r)
2872 for c in sorted(cc):
2873 # refspec option will be rejected if cc doesn't correspond to an
2874 # account, even though REST call to add such arbitrary cc may succeed.
2875 if c in valid_accounts:
2876 refspec_opts.append('cc=%s' % c)
2877 cc.remove(c)
2878
rmistry9eadede2016-09-19 11:22:43 -07002879 if options.topic:
2880 # Documentation on Gerrit topics is here:
2881 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002882 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002883
Edward Lemur687ca902018-12-05 02:30:30 +00002884 if options.enable_auto_submit:
2885 refspec_opts.append('l=Auto-Submit+1')
2886 if options.use_commit_queue:
2887 refspec_opts.append('l=Commit-Queue+2')
2888 elif options.cq_dry_run:
2889 refspec_opts.append('l=Commit-Queue+1')
2890
2891 if change_desc.get_reviewers(tbr_only=True):
2892 score = gerrit_util.GetCodeReviewTbrScore(
2893 self._GetGerritHost(),
2894 self._GetGerritProject())
2895 refspec_opts.append('l=Code-Review+%s' % score)
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002896
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002897 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002898 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002899 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002900 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002901 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2902
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002903 refspec_suffix = ''
2904 if refspec_opts:
2905 refspec_suffix = '%' + ','.join(refspec_opts)
2906 assert ' ' not in refspec_suffix, (
2907 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2908 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2909
Edward Lemur1b52d872019-05-09 21:12:12 +00002910 git_push_metadata = {
2911 'gerrit_host': self._GetGerritHost(),
2912 'title': title or '<untitled>',
2913 'change_id': change_id,
2914 'description': change_desc.description,
2915 }
2916 push_stdout = self._RunGitPushWithTraces(
2917 change_desc, refspec, refspec_opts, git_push_metadata)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002918
2919 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002920 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002921 change_numbers = [m.group(1)
2922 for m in map(regex.match, push_stdout.splitlines())
2923 if m]
2924 if len(change_numbers) != 1:
2925 DieWithError(
2926 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002927 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002928 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002929 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002930
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002931 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002932 # GetIssue() is not set in case of non-squash uploads according to tests.
2933 # TODO(agable): non-squash uploads in git cl should be removed.
2934 gerrit_util.AddReviewers(
2935 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002936 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002937 reviewers, cc,
2938 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002939
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002940 return 0
2941
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002942 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2943 change_desc):
2944 """Computes parent of the generated commit to be uploaded to Gerrit.
2945
2946 Returns revision or a ref name.
2947 """
2948 if custom_cl_base:
2949 # Try to avoid creating additional unintended CLs when uploading, unless
2950 # user wants to take this risk.
2951 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2952 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2953 local_ref_of_target_remote])
2954 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002955 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002956 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2957 'If you proceed with upload, more than 1 CL may be created by '
2958 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2959 'If you are certain that specified base `%s` has already been '
2960 'uploaded to Gerrit as another CL, you may proceed.\n' %
2961 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2962 if not force:
2963 confirm_or_exit(
2964 'Do you take responsibility for cleaning up potential mess '
2965 'resulting from proceeding with upload?',
2966 action='upload')
2967 return custom_cl_base
2968
Aaron Gablef97e33d2017-03-30 15:44:27 -07002969 if remote != '.':
2970 return self.GetCommonAncestorWithUpstream()
2971
2972 # If our upstream branch is local, we base our squashed commit on its
2973 # squashed version.
2974 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2975
Aaron Gablef97e33d2017-03-30 15:44:27 -07002976 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002977 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002978
2979 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002980 # TODO(tandrii): consider checking parent change in Gerrit and using its
2981 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2982 # the tree hash of the parent branch. The upside is less likely bogus
2983 # requests to reupload parent change just because it's uploadhash is
2984 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07002985 parent = RunGit(['config',
2986 'branch.%s.gerritsquashhash' % upstream_branch_name],
2987 error_ok=True).strip()
2988 # Verify that the upstream branch has been uploaded too, otherwise
2989 # Gerrit will create additional CLs when uploading.
2990 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2991 RunGitSilent(['rev-parse', parent + ':'])):
2992 DieWithError(
2993 '\nUpload upstream branch %s first.\n'
2994 'It is likely that this branch has been rebased since its last '
2995 'upload, so you just need to upload it again.\n'
2996 '(If you uploaded it with --no-squash, then branch dependencies '
2997 'are not supported, and you should reupload with --squash.)'
2998 % upstream_branch_name,
2999 change_desc)
3000 return parent
3001
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003002 def _AddChangeIdToCommitMessage(self, options, args):
3003 """Re-commits using the current message, assumes the commit hook is in
3004 place.
3005 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00003006 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003007 git_command = ['commit', '--amend', '-m', log_desc]
3008 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00003009 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003010 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003011 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003012 return new_log_desc
3013 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003014 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003015
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003016 def SetCQState(self, new_state):
3017 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003018 vote_map = {
3019 _CQState.NONE: 0,
3020 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003021 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003022 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003023 labels = {'Commit-Queue': vote_map[new_state]}
3024 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00003025 gerrit_util.SetReview(
3026 self._GetGerritHost(), self._GerritChangeIdentifier(),
3027 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003028
tandriie113dfd2016-10-11 10:20:12 -07003029 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003030 try:
3031 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003032 except GerritChangeNotExists:
3033 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003034
3035 if data['status'] in ('ABANDONED', 'MERGED'):
3036 return 'CL %s is closed' % self.GetIssue()
3037
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003038 def GetTryJobProperties(self, patchset=None):
3039 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003040 data = self._GetChangeDetail(['ALL_REVISIONS'])
3041 patchset = int(patchset or self.GetPatchset())
3042 assert patchset
3043 revision_data = None # Pylint wants it to be defined.
3044 for revision_data in data['revisions'].itervalues():
3045 if int(revision_data['_number']) == patchset:
3046 break
3047 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003048 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003049 (patchset, self.GetIssue()))
3050 return {
3051 'patch_issue': self.GetIssue(),
3052 'patch_set': patchset or self.GetPatchset(),
3053 'patch_project': data['project'],
3054 'patch_storage': 'gerrit',
3055 'patch_ref': revision_data['fetch']['http']['ref'],
3056 'patch_repository_url': revision_data['fetch']['http']['url'],
3057 'patch_gerrit_url': self.GetCodereviewServer(),
3058 }
tandriie113dfd2016-10-11 10:20:12 -07003059
tandriide281ae2016-10-12 06:02:30 -07003060 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003061 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003062
Edward Lemur707d70b2018-02-07 00:50:14 +01003063 def GetReviewers(self):
3064 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00003065 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01003066
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003067
3068_CODEREVIEW_IMPLEMENTATIONS = {
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003069 'gerrit': _GerritChangelistImpl,
3070}
3071
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003072
iannuccie53c9352016-08-17 14:40:40 -07003073def _add_codereview_issue_select_options(parser, extra=""):
3074 _add_codereview_select_options(parser)
3075
3076 text = ('Operate on this issue number instead of the current branch\'s '
3077 'implicit issue.')
3078 if extra:
3079 text += ' '+extra
3080 parser.add_option('-i', '--issue', type=int, help=text)
3081
3082
3083def _process_codereview_issue_select_options(parser, options):
3084 _process_codereview_select_options(parser, options)
3085 if options.issue is not None and not options.forced_codereview:
3086 parser.error('--issue must be specified with either --rietveld or --gerrit')
3087
3088
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003089def _add_codereview_select_options(parser):
3090 """Appends --gerrit and --rietveld options to force specific codereview."""
3091 parser.codereview_group = optparse.OptionGroup(
3092 parser, 'EXPERIMENTAL! Codereview override options')
3093 parser.add_option_group(parser.codereview_group)
3094 parser.codereview_group.add_option(
3095 '--gerrit', action='store_true',
3096 help='Force the use of Gerrit for codereview')
3097 parser.codereview_group.add_option(
3098 '--rietveld', action='store_true',
3099 help='Force the use of Rietveld for codereview')
3100
3101
3102def _process_codereview_select_options(parser, options):
Andrii Shyshkalovfeec80e2018-10-16 01:00:47 +00003103 if options.rietveld:
3104 parser.error('--rietveld is no longer supported')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003105 options.forced_codereview = None
3106 if options.gerrit:
3107 options.forced_codereview = 'gerrit'
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003108
3109
tandriif9aefb72016-07-01 09:06:51 -07003110def _get_bug_line_values(default_project, bugs):
3111 """Given default_project and comma separated list of bugs, yields bug line
3112 values.
3113
3114 Each bug can be either:
3115 * a number, which is combined with default_project
3116 * string, which is left as is.
3117
3118 This function may produce more than one line, because bugdroid expects one
3119 project per line.
3120
3121 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3122 ['v8:123', 'chromium:789']
3123 """
3124 default_bugs = []
3125 others = []
3126 for bug in bugs.split(','):
3127 bug = bug.strip()
3128 if bug:
3129 try:
3130 default_bugs.append(int(bug))
3131 except ValueError:
3132 others.append(bug)
3133
3134 if default_bugs:
3135 default_bugs = ','.join(map(str, default_bugs))
3136 if default_project:
3137 yield '%s:%s' % (default_project, default_bugs)
3138 else:
3139 yield default_bugs
3140 for other in sorted(others):
3141 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3142 yield other
3143
3144
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003145class ChangeDescription(object):
3146 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003147 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003148 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003149 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003150 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003151 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3152 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3153 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3154 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003155
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003156 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003157 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003158
agable@chromium.org42c20792013-09-12 17:34:49 +00003159 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003160 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003161 return '\n'.join(self._description_lines)
3162
3163 def set_description(self, desc):
3164 if isinstance(desc, basestring):
3165 lines = desc.splitlines()
3166 else:
3167 lines = [line.rstrip() for line in desc]
3168 while lines and not lines[0]:
3169 lines.pop(0)
3170 while lines and not lines[-1]:
3171 lines.pop(-1)
3172 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003173
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003174 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3175 """Rewrites the R=/TBR= line(s) as a single line each.
3176
3177 Args:
3178 reviewers (list(str)) - list of additional emails to use for reviewers.
3179 tbrs (list(str)) - list of additional emails to use for TBRs.
3180 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3181 the change that are missing OWNER coverage. If this is not None, you
3182 must also pass a value for `change`.
3183 change (Change) - The Change that should be used for OWNERS lookups.
3184 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003185 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003186 assert isinstance(tbrs, list), tbrs
3187
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003188 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003189 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003190
3191 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003192 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003193
3194 reviewers = set(reviewers)
3195 tbrs = set(tbrs)
3196 LOOKUP = {
3197 'TBR': tbrs,
3198 'R': reviewers,
3199 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003200
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003201 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003202 regexp = re.compile(self.R_LINE)
3203 matches = [regexp.match(line) for line in self._description_lines]
3204 new_desc = [l for i, l in enumerate(self._description_lines)
3205 if not matches[i]]
3206 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003207
agable@chromium.org42c20792013-09-12 17:34:49 +00003208 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003209
3210 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003211 for match in matches:
3212 if not match:
3213 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003214 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3215
3216 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003217 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003218 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003219 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003220 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003221 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003222 LOOKUP[add_owners_to].update(
3223 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003224
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003225 # If any folks ended up in both groups, remove them from tbrs.
3226 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003227
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003228 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3229 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003230
3231 # Put the new lines in the description where the old first R= line was.
3232 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3233 if 0 <= line_loc < len(self._description_lines):
3234 if new_tbr_line:
3235 self._description_lines.insert(line_loc, new_tbr_line)
3236 if new_r_line:
3237 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003238 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003239 if new_r_line:
3240 self.append_footer(new_r_line)
3241 if new_tbr_line:
3242 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003243
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00003244 def set_preserve_tryjobs(self):
3245 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
3246 footers = git_footers.parse_footers(self.description)
3247 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
3248 if v.lower() == 'true':
3249 return
3250 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
3251
Aaron Gable3a16ed12017-03-23 10:51:55 -07003252 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003253 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003254 self.set_description([
3255 '# Enter a description of the change.',
3256 '# This will be displayed on the codereview site.',
3257 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003258 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003259 '--------------------',
3260 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003261
agable@chromium.org42c20792013-09-12 17:34:49 +00003262 regexp = re.compile(self.BUG_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00003263 prefix = settings.GetBugPrefix()
agable@chromium.org42c20792013-09-12 17:34:49 +00003264 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003265 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003266 if git_footer:
3267 self.append_footer('Bug: %s' % ', '.join(values))
3268 else:
3269 for value in values:
3270 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003271
agable@chromium.org42c20792013-09-12 17:34:49 +00003272 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003273 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003274 if not content:
3275 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003276 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003277
Bruce Dawson2377b012018-01-11 16:46:49 -08003278 # Strip off comments and default inserted "Bug:" line.
3279 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00003280 (line.startswith('#') or
3281 line.rstrip() == "Bug:" or
3282 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00003283 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003284 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003285 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003286
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003287 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003288 """Adds a footer line to the description.
3289
3290 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3291 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3292 that Gerrit footers are always at the end.
3293 """
3294 parsed_footer_line = git_footers.parse_footer(line)
3295 if parsed_footer_line:
3296 # Line is a gerrit footer in the form: Footer-Key: any value.
3297 # Thus, must be appended observing Gerrit footer rules.
3298 self.set_description(
3299 git_footers.add_footer(self.description,
3300 key=parsed_footer_line[0],
3301 value=parsed_footer_line[1]))
3302 return
3303
3304 if not self._description_lines:
3305 self._description_lines.append(line)
3306 return
3307
3308 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3309 if gerrit_footers:
3310 # git_footers.split_footers ensures that there is an empty line before
3311 # actual (gerrit) footers, if any. We have to keep it that way.
3312 assert top_lines and top_lines[-1] == ''
3313 top_lines, separator = top_lines[:-1], top_lines[-1:]
3314 else:
3315 separator = [] # No need for separator if there are no gerrit_footers.
3316
3317 prev_line = top_lines[-1] if top_lines else ''
3318 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3319 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3320 top_lines.append('')
3321 top_lines.append(line)
3322 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003323
tandrii99a72f22016-08-17 14:33:24 -07003324 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003325 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003326 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003327 reviewers = [match.group(2).strip()
3328 for match in matches
3329 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003330 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003331
bradnelsond975b302016-10-23 12:20:23 -07003332 def get_cced(self):
3333 """Retrieves the list of reviewers."""
3334 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3335 cced = [match.group(2).strip() for match in matches if match]
3336 return cleanup_list(cced)
3337
Nodir Turakulov23b82142017-11-16 11:04:25 -08003338 def get_hash_tags(self):
3339 """Extracts and sanitizes a list of Gerrit hashtags."""
3340 subject = (self._description_lines or ('',))[0]
3341 subject = re.sub(
3342 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3343
3344 tags = []
3345 start = 0
3346 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3347 while True:
3348 m = bracket_exp.match(subject, start)
3349 if not m:
3350 break
3351 tags.append(self.sanitize_hash_tag(m.group(1)))
3352 start = m.end()
3353
3354 if not tags:
3355 # Try "Tag: " prefix.
3356 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3357 if m:
3358 tags.append(self.sanitize_hash_tag(m.group(1)))
3359 return tags
3360
3361 @classmethod
3362 def sanitize_hash_tag(cls, tag):
3363 """Returns a sanitized Gerrit hash tag.
3364
3365 A sanitized hashtag can be used as a git push refspec parameter value.
3366 """
3367 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3368
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003369 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3370 """Updates this commit description given the parent.
3371
3372 This is essentially what Gnumbd used to do.
3373 Consult https://goo.gl/WMmpDe for more details.
3374 """
3375 assert parent_msg # No, orphan branch creation isn't supported.
3376 assert parent_hash
3377 assert dest_ref
3378 parent_footer_map = git_footers.parse_footers(parent_msg)
3379 # This will also happily parse svn-position, which GnumbD is no longer
3380 # supporting. While we'd generate correct footers, the verifier plugin
3381 # installed in Gerrit will block such commit (ie git push below will fail).
3382 parent_position = git_footers.get_position(parent_footer_map)
3383
3384 # Cherry-picks may have last line obscuring their prior footers,
3385 # from git_footers perspective. This is also what Gnumbd did.
3386 cp_line = None
3387 if (self._description_lines and
3388 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3389 cp_line = self._description_lines.pop()
3390
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003391 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003392
3393 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3394 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003395 for i, line in enumerate(footer_lines):
3396 k, v = git_footers.parse_footer(line) or (None, None)
3397 if k and k.startswith('Cr-'):
3398 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003399
3400 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003401 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003402 if parent_position[0] == dest_ref:
3403 # Same branch as parent.
3404 number = int(parent_position[1]) + 1
3405 else:
3406 number = 1 # New branch, and extra lineage.
3407 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3408 int(parent_position[1])))
3409
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003410 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3411 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003412
3413 self._description_lines = top_lines
3414 if cp_line:
3415 self._description_lines.append(cp_line)
3416 if self._description_lines[-1] != '':
3417 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003418 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003419
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003420
Aaron Gablea1bab272017-04-11 16:38:18 -07003421def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003422 """Retrieves the reviewers that approved a CL from the issue properties with
3423 messages.
3424
3425 Note that the list may contain reviewers that are not committer, thus are not
3426 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003427
3428 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003429 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003430 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003431 return sorted(
3432 set(
3433 message['sender']
3434 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003435 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003436 )
3437 )
3438
3439
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003440def FindCodereviewSettingsFile(filename='codereview.settings'):
3441 """Finds the given file starting in the cwd and going up.
3442
3443 Only looks up to the top of the repository unless an
3444 'inherit-review-settings-ok' file exists in the root of the repository.
3445 """
3446 inherit_ok_file = 'inherit-review-settings-ok'
3447 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003448 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003449 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3450 root = '/'
3451 while True:
3452 if filename in os.listdir(cwd):
3453 if os.path.isfile(os.path.join(cwd, filename)):
3454 return open(os.path.join(cwd, filename))
3455 if cwd == root:
3456 break
3457 cwd = os.path.dirname(cwd)
3458
3459
3460def LoadCodereviewSettingsFromFile(fileobj):
3461 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003462 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003463
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003464 def SetProperty(name, setting, unset_error_ok=False):
3465 fullname = 'rietveld.' + name
3466 if setting in keyvals:
3467 RunGit(['config', fullname, keyvals[setting]])
3468 else:
3469 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3470
tandrii48df5812016-10-17 03:55:37 -07003471 if not keyvals.get('GERRIT_HOST', False):
3472 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003473 # Only server setting is required. Other settings can be absent.
3474 # In that case, we ignore errors raised during option deletion attempt.
3475 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3476 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3477 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003478 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003479 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3480 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003481 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3482 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003483
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003484 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003485 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003486
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003487 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003488 RunGit(['config', 'gerrit.squash-uploads',
3489 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003490
tandrii@chromium.org28253532016-04-14 13:46:56 +00003491 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003492 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003493 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3494
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003495 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003496 # should be of the form
3497 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3498 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003499 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3500 keyvals['ORIGIN_URL_CONFIG']])
3501
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003502
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003503def urlretrieve(source, destination):
3504 """urllib is broken for SSL connections via a proxy therefore we
3505 can't use urllib.urlretrieve()."""
3506 with open(destination, 'w') as f:
3507 f.write(urllib2.urlopen(source).read())
3508
3509
ukai@chromium.org712d6102013-11-27 00:52:58 +00003510def hasSheBang(fname):
3511 """Checks fname is a #! script."""
3512 with open(fname) as f:
3513 return f.read(2).startswith('#!')
3514
3515
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003516# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3517def DownloadHooks(*args, **kwargs):
3518 pass
3519
3520
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003521def DownloadGerritHook(force):
3522 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003523
3524 Args:
3525 force: True to update hooks. False to install hooks if not present.
3526 """
3527 if not settings.GetIsGerrit():
3528 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003529 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003530 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3531 if not os.access(dst, os.X_OK):
3532 if os.path.exists(dst):
3533 if not force:
3534 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003535 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003536 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003537 if not hasSheBang(dst):
3538 DieWithError('Not a script: %s\n'
3539 'You need to download from\n%s\n'
3540 'into .git/hooks/commit-msg and '
3541 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003542 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3543 except Exception:
3544 if os.path.exists(dst):
3545 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003546 DieWithError('\nFailed to download hooks.\n'
3547 'You need to download from\n%s\n'
3548 'into .git/hooks/commit-msg and '
3549 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003550
3551
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003552class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003553 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003554
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003555 _GOOGLESOURCE = 'googlesource.com'
3556
3557 def __init__(self):
3558 # Cached list of [host, identity, source], where source is either
3559 # .gitcookies or .netrc.
3560 self._all_hosts = None
3561
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003562 def ensure_configured_gitcookies(self):
3563 """Runs checks and suggests fixes to make git use .gitcookies from default
3564 path."""
3565 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3566 configured_path = RunGitSilent(
3567 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003568 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003569 if configured_path:
3570 self._ensure_default_gitcookies_path(configured_path, default)
3571 else:
3572 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003573
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003574 @staticmethod
3575 def _ensure_default_gitcookies_path(configured_path, default_path):
3576 assert configured_path
3577 if configured_path == default_path:
3578 print('git is already configured to use your .gitcookies from %s' %
3579 configured_path)
3580 return
3581
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003582 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003583 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3584 (configured_path, default_path))
3585
3586 if not os.path.exists(configured_path):
3587 print('However, your configured .gitcookies file is missing.')
3588 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3589 action='reconfigure')
3590 RunGit(['config', '--global', 'http.cookiefile', default_path])
3591 return
3592
3593 if os.path.exists(default_path):
3594 print('WARNING: default .gitcookies file already exists %s' %
3595 default_path)
3596 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3597 default_path)
3598
3599 confirm_or_exit('Move existing .gitcookies to default location?',
3600 action='move')
3601 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003602 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003603 print('Moved and reconfigured git to use .gitcookies from %s' %
3604 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003605
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003606 @staticmethod
3607 def _configure_gitcookies_path(default_path):
3608 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3609 if os.path.exists(netrc_path):
3610 print('You seem to be using outdated .netrc for git credentials: %s' %
3611 netrc_path)
3612 print('This tool will guide you through setting up recommended '
3613 '.gitcookies store for git credentials.\n'
3614 '\n'
3615 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3616 ' git config --global --unset http.cookiefile\n'
3617 ' mv %s %s.backup\n\n' % (default_path, default_path))
3618 confirm_or_exit(action='setup .gitcookies')
3619 RunGit(['config', '--global', 'http.cookiefile', default_path])
3620 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003621
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003622 def get_hosts_with_creds(self, include_netrc=False):
3623 if self._all_hosts is None:
3624 a = gerrit_util.CookiesAuthenticator()
3625 self._all_hosts = [
3626 (h, u, s)
3627 for h, u, s in itertools.chain(
3628 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3629 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3630 )
3631 if h.endswith(self._GOOGLESOURCE)
3632 ]
3633
3634 if include_netrc:
3635 return self._all_hosts
3636 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3637
3638 def print_current_creds(self, include_netrc=False):
3639 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3640 if not hosts:
3641 print('No Git/Gerrit credentials found')
3642 return
3643 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3644 header = [('Host', 'User', 'Which file'),
3645 ['=' * l for l in lengths]]
3646 for row in (header + hosts):
3647 print('\t'.join((('%%+%ds' % l) % s)
3648 for l, s in zip(lengths, row)))
3649
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003650 @staticmethod
3651 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003652 """Parses identity "git-<username>.domain" into <username> and domain."""
3653 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003654 # distinguishable from sub-domains. But we do know typical domains:
3655 if identity.endswith('.chromium.org'):
3656 domain = 'chromium.org'
3657 username = identity[:-len('.chromium.org')]
3658 else:
3659 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003660 if username.startswith('git-'):
3661 username = username[len('git-'):]
3662 return username, domain
3663
3664 def _get_usernames_of_domain(self, domain):
3665 """Returns list of usernames referenced by .gitcookies in a given domain."""
3666 identities_by_domain = {}
3667 for _, identity, _ in self.get_hosts_with_creds():
3668 username, domain = self._parse_identity(identity)
3669 identities_by_domain.setdefault(domain, []).append(username)
3670 return identities_by_domain.get(domain)
3671
3672 def _canonical_git_googlesource_host(self, host):
3673 """Normalizes Gerrit hosts (with '-review') to Git host."""
3674 assert host.endswith(self._GOOGLESOURCE)
3675 # Prefix doesn't include '.' at the end.
3676 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3677 if prefix.endswith('-review'):
3678 prefix = prefix[:-len('-review')]
3679 return prefix + '.' + self._GOOGLESOURCE
3680
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003681 def _canonical_gerrit_googlesource_host(self, host):
3682 git_host = self._canonical_git_googlesource_host(host)
3683 prefix = git_host.split('.', 1)[0]
3684 return prefix + '-review.' + self._GOOGLESOURCE
3685
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003686 def _get_counterpart_host(self, host):
3687 assert host.endswith(self._GOOGLESOURCE)
3688 git = self._canonical_git_googlesource_host(host)
3689 gerrit = self._canonical_gerrit_googlesource_host(git)
3690 return git if gerrit == host else gerrit
3691
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003692 def has_generic_host(self):
3693 """Returns whether generic .googlesource.com has been configured.
3694
3695 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3696 """
3697 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3698 if host == '.' + self._GOOGLESOURCE:
3699 return True
3700 return False
3701
3702 def _get_git_gerrit_identity_pairs(self):
3703 """Returns map from canonic host to pair of identities (Git, Gerrit).
3704
3705 One of identities might be None, meaning not configured.
3706 """
3707 host_to_identity_pairs = {}
3708 for host, identity, _ in self.get_hosts_with_creds():
3709 canonical = self._canonical_git_googlesource_host(host)
3710 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3711 idx = 0 if canonical == host else 1
3712 pair[idx] = identity
3713 return host_to_identity_pairs
3714
3715 def get_partially_configured_hosts(self):
3716 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003717 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3718 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3719 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003720
3721 def get_conflicting_hosts(self):
3722 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003723 host
3724 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003725 if None not in (i1, i2) and i1 != i2)
3726
3727 def get_duplicated_hosts(self):
3728 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3729 return set(host for host, count in counters.iteritems() if count > 1)
3730
3731 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3732 'chromium.googlesource.com': 'chromium.org',
3733 'chrome-internal.googlesource.com': 'google.com',
3734 }
3735
3736 def get_hosts_with_wrong_identities(self):
3737 """Finds hosts which **likely** reference wrong identities.
3738
3739 Note: skips hosts which have conflicting identities for Git and Gerrit.
3740 """
3741 hosts = set()
3742 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3743 pair = self._get_git_gerrit_identity_pairs().get(host)
3744 if pair and pair[0] == pair[1]:
3745 _, domain = self._parse_identity(pair[0])
3746 if domain != expected:
3747 hosts.add(host)
3748 return hosts
3749
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003750 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003751 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003752 hosts = sorted(hosts)
3753 assert hosts
3754 if extra_column_func is None:
3755 extras = [''] * len(hosts)
3756 else:
3757 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003758 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3759 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003760 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003761 lines.append(tmpl % he)
3762 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003763
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003764 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003765 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003766 yield ('.googlesource.com wildcard record detected',
3767 ['Chrome Infrastructure team recommends to list full host names '
3768 'explicitly.'],
3769 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003770
3771 dups = self.get_duplicated_hosts()
3772 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003773 yield ('The following hosts were defined twice',
3774 self._format_hosts(dups),
3775 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003776
3777 partial = self.get_partially_configured_hosts()
3778 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003779 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3780 'These hosts are missing',
3781 self._format_hosts(partial, lambda host: 'but %s defined' %
3782 self._get_counterpart_host(host)),
3783 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003784
3785 conflicting = self.get_conflicting_hosts()
3786 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003787 yield ('The following Git hosts have differing credentials from their '
3788 'Gerrit counterparts',
3789 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3790 tuple(self._get_git_gerrit_identity_pairs()[host])),
3791 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003792
3793 wrong = self.get_hosts_with_wrong_identities()
3794 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003795 yield ('These hosts likely use wrong identity',
3796 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3797 (self._get_git_gerrit_identity_pairs()[host][0],
3798 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3799 wrong)
3800
3801 def find_and_report_problems(self):
3802 """Returns True if there was at least one problem, else False."""
3803 found = False
3804 bad_hosts = set()
3805 for title, sublines, hosts in self._find_problems():
3806 if not found:
3807 found = True
3808 print('\n\n.gitcookies problem report:\n')
3809 bad_hosts.update(hosts or [])
3810 print(' %s%s' % (title , (':' if sublines else '')))
3811 if sublines:
3812 print()
3813 print(' %s' % '\n '.join(sublines))
3814 print()
3815
3816 if bad_hosts:
3817 assert found
3818 print(' You can manually remove corresponding lines in your %s file and '
3819 'visit the following URLs with correct account to generate '
3820 'correct credential lines:\n' %
3821 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3822 print(' %s' % '\n '.join(sorted(set(
3823 gerrit_util.CookiesAuthenticator().get_new_password_url(
3824 self._canonical_git_googlesource_host(host))
3825 for host in bad_hosts
3826 ))))
3827 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003828
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003829
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003830@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003831def CMDcreds_check(parser, args):
3832 """Checks credentials and suggests changes."""
3833 _, _ = parser.parse_args(args)
3834
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003835 # Code below checks .gitcookies. Abort if using something else.
3836 authn = gerrit_util.Authenticator.get()
3837 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3838 if isinstance(authn, gerrit_util.GceAuthenticator):
3839 DieWithError(
3840 'This command is not designed for GCE, are you on a bot?\n'
3841 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3842 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003843 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003844 'This command is not designed for bot environment. It checks '
3845 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003846
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003847 checker = _GitCookiesChecker()
3848 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003849
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003850 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003851 checker.print_current_creds(include_netrc=True)
3852
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003853 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003854 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003855 return 0
3856 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003857
3858
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003859@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003860def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003861 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003862 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3863 branch = ShortBranchName(branchref)
3864 _, args = parser.parse_args(args)
3865 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003866 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003867 return RunGit(['config', 'branch.%s.base-url' % branch],
3868 error_ok=False).strip()
3869 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003870 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003871 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3872 error_ok=False).strip()
3873
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003874def color_for_status(status):
3875 """Maps a Changelist status to color, for CMDstatus and other tools."""
3876 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003877 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003878 'waiting': Fore.BLUE,
3879 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003880 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003881 'lgtm': Fore.GREEN,
3882 'commit': Fore.MAGENTA,
3883 'closed': Fore.CYAN,
3884 'error': Fore.WHITE,
3885 }.get(status, Fore.WHITE)
3886
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003887
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003888def get_cl_statuses(changes, fine_grained, max_processes=None):
3889 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003890
3891 If fine_grained is true, this will fetch CL statuses from the server.
3892 Otherwise, simply indicate if there's a matching url for the given branches.
3893
3894 If max_processes is specified, it is used as the maximum number of processes
3895 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3896 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003897
3898 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003899 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003900 if not changes:
3901 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003902
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003903 if not fine_grained:
3904 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003905 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003906 for cl in changes:
3907 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003908 return
3909
3910 # First, sort out authentication issues.
3911 logging.debug('ensuring credentials exist')
3912 for cl in changes:
3913 cl.EnsureAuthenticated(force=False, refresh=True)
3914
3915 def fetch(cl):
3916 try:
3917 return (cl, cl.GetStatus())
3918 except:
3919 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003920 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003921 raise
3922
3923 threads_count = len(changes)
3924 if max_processes:
3925 threads_count = max(1, min(threads_count, max_processes))
3926 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3927
3928 pool = ThreadPool(threads_count)
3929 fetched_cls = set()
3930 try:
3931 it = pool.imap_unordered(fetch, changes).__iter__()
3932 while True:
3933 try:
3934 cl, status = it.next(timeout=5)
3935 except multiprocessing.TimeoutError:
3936 break
3937 fetched_cls.add(cl)
3938 yield cl, status
3939 finally:
3940 pool.close()
3941
3942 # Add any branches that failed to fetch.
3943 for cl in set(changes) - fetched_cls:
3944 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003945
rmistry@google.com2dd99862015-06-22 12:22:18 +00003946
3947def upload_branch_deps(cl, args):
3948 """Uploads CLs of local branches that are dependents of the current branch.
3949
3950 If the local branch dependency tree looks like:
3951 test1 -> test2.1 -> test3.1
3952 -> test3.2
3953 -> test2.2 -> test3.3
3954
3955 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3956 run on the dependent branches in this order:
3957 test2.1, test3.1, test3.2, test2.2, test3.3
3958
3959 Note: This function does not rebase your local dependent branches. Use it when
3960 you make a change to the parent branch that will not conflict with its
3961 dependent branches, and you would like their dependencies updated in
3962 Rietveld.
3963 """
3964 if git_common.is_dirty_git_tree('upload-branch-deps'):
3965 return 1
3966
3967 root_branch = cl.GetBranch()
3968 if root_branch is None:
3969 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3970 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003971 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003972 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3973 'patchset dependencies without an uploaded CL.')
3974
3975 branches = RunGit(['for-each-ref',
3976 '--format=%(refname:short) %(upstream:short)',
3977 'refs/heads'])
3978 if not branches:
3979 print('No local branches found.')
3980 return 0
3981
3982 # Create a dictionary of all local branches to the branches that are dependent
3983 # on it.
3984 tracked_to_dependents = collections.defaultdict(list)
3985 for b in branches.splitlines():
3986 tokens = b.split()
3987 if len(tokens) == 2:
3988 branch_name, tracked = tokens
3989 tracked_to_dependents[tracked].append(branch_name)
3990
vapiera7fbd5a2016-06-16 09:17:49 -07003991 print()
3992 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003993 dependents = []
3994 def traverse_dependents_preorder(branch, padding=''):
3995 dependents_to_process = tracked_to_dependents.get(branch, [])
3996 padding += ' '
3997 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003998 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003999 dependents.append(dependent)
4000 traverse_dependents_preorder(dependent, padding)
4001 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004002 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004003
4004 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004005 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004006 return 0
4007
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004008 confirm_or_exit('This command will checkout all dependent branches and run '
4009 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004010
rmistry@google.com2dd99862015-06-22 12:22:18 +00004011 # Record all dependents that failed to upload.
4012 failures = {}
4013 # Go through all dependents, checkout the branch and upload.
4014 try:
4015 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004016 print()
4017 print('--------------------------------------')
4018 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004019 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004020 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004021 try:
4022 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004023 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004024 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004025 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004026 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004027 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004028 finally:
4029 # Swap back to the original root branch.
4030 RunGit(['checkout', '-q', root_branch])
4031
vapiera7fbd5a2016-06-16 09:17:49 -07004032 print()
4033 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004034 for dependent_branch in dependents:
4035 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004036 print(' %s : %s' % (dependent_branch, upload_status))
4037 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004038
4039 return 0
4040
4041
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004042@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004043def CMDarchive(parser, args):
4044 """Archives and deletes branches associated with closed changelists."""
4045 parser.add_option(
4046 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004047 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004048 parser.add_option(
4049 '-f', '--force', action='store_true',
4050 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004051 parser.add_option(
4052 '-d', '--dry-run', action='store_true',
4053 help='Skip the branch tagging and removal steps.')
4054 parser.add_option(
4055 '-t', '--notags', action='store_true',
4056 help='Do not tag archived branches. '
4057 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004058
4059 auth.add_auth_options(parser)
4060 options, args = parser.parse_args(args)
4061 if args:
4062 parser.error('Unsupported args: %s' % ' '.join(args))
4063 auth_config = auth.extract_auth_config_from_options(options)
4064
4065 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4066 if not branches:
4067 return 0
4068
vapiera7fbd5a2016-06-16 09:17:49 -07004069 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004070 changes = [Changelist(branchref=b, auth_config=auth_config)
4071 for b in branches.splitlines()]
4072 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4073 statuses = get_cl_statuses(changes,
4074 fine_grained=True,
4075 max_processes=options.maxjobs)
4076 proposal = [(cl.GetBranch(),
4077 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4078 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00004079 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07004080 proposal.sort()
4081
4082 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004083 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004084 return 0
4085
4086 current_branch = GetCurrentBranch()
4087
vapiera7fbd5a2016-06-16 09:17:49 -07004088 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004089 if options.notags:
4090 for next_item in proposal:
4091 print(' ' + next_item[0])
4092 else:
4093 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4094 for next_item in proposal:
4095 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004096
kmarshall9249e012016-08-23 12:02:16 -07004097 # Quit now on precondition failure or if instructed by the user, either
4098 # via an interactive prompt or by command line flags.
4099 if options.dry_run:
4100 print('\nNo changes were made (dry run).\n')
4101 return 0
4102 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004103 print('You are currently on a branch \'%s\' which is associated with a '
4104 'closed codereview issue, so archive cannot proceed. Please '
4105 'checkout another branch and run this command again.' %
4106 current_branch)
4107 return 1
kmarshall9249e012016-08-23 12:02:16 -07004108 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004109 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4110 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004111 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004112 return 1
4113
4114 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004115 if not options.notags:
4116 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004117 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004118
vapiera7fbd5a2016-06-16 09:17:49 -07004119 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004120
4121 return 0
4122
4123
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004124@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004125def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004126 """Show status of changelists.
4127
4128 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004129 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004130 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004131 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004132 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004133 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004134 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004135 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004136
4137 Also see 'git cl comments'.
4138 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00004139 parser.add_option(
4140 '--no-branch-color',
4141 action='store_true',
4142 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004143 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004144 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004145 parser.add_option('-f', '--fast', action='store_true',
4146 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004147 parser.add_option(
4148 '-j', '--maxjobs', action='store', type=int,
4149 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004150
4151 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004152 _add_codereview_issue_select_options(
4153 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004154 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004155 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004156 if args:
4157 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004158 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004159
iannuccie53c9352016-08-17 14:40:40 -07004160 if options.issue is not None and not options.field:
4161 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004162
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004163 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004164 cl = Changelist(auth_config=auth_config, issue=options.issue,
4165 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004166 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004167 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004168 elif options.field == 'id':
4169 issueid = cl.GetIssue()
4170 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004171 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004172 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004173 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004174 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004175 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004176 elif options.field == 'status':
4177 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004178 elif options.field == 'url':
4179 url = cl.GetIssueURL()
4180 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004181 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004182 return 0
4183
4184 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4185 if not branches:
4186 print('No local branch found.')
4187 return 0
4188
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004189 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004190 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004191 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004192 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004193 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004194 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004195 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004196
Daniel McArdlea23bf592019-02-12 00:25:12 +00004197 current_branch = GetCurrentBranch()
4198
4199 def FormatBranchName(branch, colorize=False):
4200 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
4201 an asterisk when it is the current branch."""
4202
4203 asterisk = ""
4204 color = Fore.RESET
4205 if branch == current_branch:
4206 asterisk = "* "
4207 color = Fore.GREEN
4208 branch_name = ShortBranchName(branch)
4209
4210 if colorize:
4211 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00004212 return asterisk + branch_name
4213
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004214 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00004215
4216 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004217 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4218 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004219 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004220 c, status = output.next()
4221 branch_statuses[c.GetBranch()] = status
4222 status = branch_statuses.pop(branch)
4223 url = cl.GetIssueURL()
4224 if url and (not status or status == 'error'):
4225 # The issue probably doesn't exist anymore.
4226 url += ' (broken)'
4227
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004228 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004229 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004230 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004231 color = ''
4232 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004233 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00004234
Alan Cuttera3be9a52019-03-04 18:50:33 +00004235 branch_display = FormatBranchName(branch)
4236 padding = ' ' * (alignment - len(branch_display))
4237 if not options.no_branch_color:
4238 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00004239
Alan Cuttera3be9a52019-03-04 18:50:33 +00004240 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
4241 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004242
vapiera7fbd5a2016-06-16 09:17:49 -07004243 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00004244 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004245 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00004246 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004247 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004248 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004249 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004250 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004251 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004252 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004253 print('Issue description:')
4254 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004255 return 0
4256
4257
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004258def colorize_CMDstatus_doc():
4259 """To be called once in main() to add colors to git cl status help."""
4260 colors = [i for i in dir(Fore) if i[0].isupper()]
4261
4262 def colorize_line(line):
4263 for color in colors:
4264 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004265 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004266 indent = len(line) - len(line.lstrip(' ')) + 1
4267 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4268 return line
4269
4270 lines = CMDstatus.__doc__.splitlines()
4271 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4272
4273
phajdan.jre328cf92016-08-22 04:12:17 -07004274def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004275 if path == '-':
4276 json.dump(contents, sys.stdout)
4277 else:
4278 with open(path, 'w') as f:
4279 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004280
4281
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004282@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004283@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004284def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004285 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004286
4287 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004288 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004289 parser.add_option('-r', '--reverse', action='store_true',
4290 help='Lookup the branch(es) for the specified issues. If '
4291 'no issues are specified, all branches with mapped '
4292 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004293 parser.add_option('--json',
4294 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004295 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004296 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004297 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004298
dnj@chromium.org406c4402015-03-03 17:22:28 +00004299 if options.reverse:
4300 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004301 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004302 # Reverse issue lookup.
4303 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004304
4305 git_config = {}
4306 for config in RunGit(['config', '--get-regexp',
4307 r'branch\..*issue']).splitlines():
4308 name, _space, val = config.partition(' ')
4309 git_config[name] = val
4310
dnj@chromium.org406c4402015-03-03 17:22:28 +00004311 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004312 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4313 config_key = _git_branch_config_key(ShortBranchName(branch),
4314 cls.IssueConfigKey())
4315 issue = git_config.get(config_key)
4316 if issue:
4317 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004318 if not args:
4319 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004320 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004321 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00004322 try:
4323 issue_num = int(issue)
4324 except ValueError:
4325 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004326 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00004327 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07004328 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00004329 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004330 if options.json:
4331 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004332 return 0
4333
4334 if len(args) > 0:
4335 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4336 if not issue.valid:
4337 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4338 'or no argument to list it.\n'
4339 'Maybe you want to run git cl status?')
4340 cl = Changelist(codereview=issue.codereview)
4341 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004342 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004343 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004344 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4345 if options.json:
4346 write_json(options.json, {
4347 'issue': cl.GetIssue(),
4348 'issue_url': cl.GetIssueURL(),
4349 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004350 return 0
4351
4352
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004353@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004354def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004355 """Shows or posts review comments for any changelist."""
4356 parser.add_option('-a', '--add-comment', dest='comment',
4357 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004358 parser.add_option('-p', '--publish', action='store_true',
4359 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004360 parser.add_option('-i', '--issue', dest='issue',
4361 help='review issue id (defaults to current issue). '
4362 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004363 parser.add_option('-m', '--machine-readable', dest='readable',
4364 action='store_false', default=True,
4365 help='output comments in a format compatible with '
4366 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004367 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004368 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004369 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004370 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004371 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004372 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004373 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004374
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004375 issue = None
4376 if options.issue:
4377 try:
4378 issue = int(options.issue)
4379 except ValueError:
4380 DieWithError('A review issue id is expected to be a number')
4381
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00004382 cl = Changelist(issue=issue, codereview='gerrit', auth_config=auth_config)
4383
4384 if not cl.IsGerrit():
4385 parser.error('rietveld is not supported')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004386
4387 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004388 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004389 return 0
4390
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004391 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4392 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004393 for comment in summary:
4394 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004395 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004396 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004397 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004398 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004399 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00004400 elif comment.autogenerated:
4401 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004402 else:
4403 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004404 print('\n%s%s %s%s\n%s' % (
4405 color,
4406 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4407 comment.sender,
4408 Fore.RESET,
4409 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4410
smut@google.comc85ac942015-09-15 16:34:43 +00004411 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004412 def pre_serialize(c):
4413 dct = c.__dict__.copy()
4414 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4415 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004416 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004417 return 0
4418
4419
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004420@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004421@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004422def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004423 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004424 parser.add_option('-d', '--display', action='store_true',
4425 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004426 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004427 help='New description to set for this issue (- for stdin, '
4428 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004429 parser.add_option('-f', '--force', action='store_true',
4430 help='Delete any unpublished Gerrit edits for this issue '
4431 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004432
4433 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004434 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004435 options, args = parser.parse_args(args)
4436 _process_codereview_select_options(parser, options)
4437
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004438 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004439 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004440 target_issue_arg = ParseIssueNumberArgument(args[0],
4441 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004442 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004443 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004444
martiniss6eda05f2016-06-30 10:18:35 -07004445 kwargs = {
Andrii Shyshkalovdd672fb2018-10-16 06:09:51 +00004446 'auth_config': auth.extract_auth_config_from_options(options),
4447 'codereview': options.forced_codereview,
martiniss6eda05f2016-06-30 10:18:35 -07004448 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004449 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004450 if target_issue_arg:
4451 kwargs['issue'] = target_issue_arg.issue
4452 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004453 if target_issue_arg.codereview and not options.forced_codereview:
4454 detected_codereview_from_url = True
4455 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004456
4457 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004458 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004459 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004460 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004461
4462 if detected_codereview_from_url:
4463 logging.info('canonical issue/change URL: %s (type: %s)\n',
4464 cl.GetIssueURL(), target_issue_arg.codereview)
4465
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004466 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004467
smut@google.com34fb6b12015-07-13 20:03:26 +00004468 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004469 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004470 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004471
4472 if options.new_description:
4473 text = options.new_description
4474 if text == '-':
4475 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004476 elif text == '+':
4477 base_branch = cl.GetCommonAncestorWithUpstream()
4478 change = cl.GetChange(base_branch, None, local_description=True)
4479 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004480
4481 description.set_description(text)
4482 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004483 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004484
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004485 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004486 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004487 return 0
4488
4489
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004490@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004491def CMDlint(parser, args):
4492 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004493 parser.add_option('--filter', action='append', metavar='-x,+y',
4494 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004495 auth.add_auth_options(parser)
4496 options, args = parser.parse_args(args)
4497 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004498
4499 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004500 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004501 try:
4502 import cpplint
4503 import cpplint_chromium
4504 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004505 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004506 return 1
4507
4508 # Change the current working directory before calling lint so that it
4509 # shows the correct base.
4510 previous_cwd = os.getcwd()
4511 os.chdir(settings.GetRoot())
4512 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004513 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004514 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4515 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004516 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004517 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004518 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004519
4520 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004521 command = args + files
4522 if options.filter:
4523 command = ['--filter=' + ','.join(options.filter)] + command
4524 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004525
4526 white_regex = re.compile(settings.GetLintRegex())
4527 black_regex = re.compile(settings.GetLintIgnoreRegex())
4528 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4529 for filename in filenames:
4530 if white_regex.match(filename):
4531 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004532 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004533 else:
4534 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4535 extra_check_functions)
4536 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004537 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004538 finally:
4539 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004540 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004541 if cpplint._cpplint_state.error_count != 0:
4542 return 1
4543 return 0
4544
4545
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004546@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004547def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004548 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004549 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004550 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004551 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004552 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004553 parser.add_option('--all', action='store_true',
4554 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004555 parser.add_option('--parallel', action='store_true',
4556 help='Run all tests specified by input_api.RunTests in all '
4557 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004558 auth.add_auth_options(parser)
4559 options, args = parser.parse_args(args)
4560 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004561
sbc@chromium.org71437c02015-04-09 19:29:40 +00004562 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004563 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004564 return 1
4565
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004566 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004567 if args:
4568 base_branch = args[0]
4569 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004570 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004571 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004572
Aaron Gable8076c282017-11-29 14:39:41 -08004573 if options.all:
4574 base_change = cl.GetChange(base_branch, None)
4575 files = [('M', f) for f in base_change.AllFiles()]
4576 change = presubmit_support.GitChange(
4577 base_change.Name(),
4578 base_change.FullDescriptionText(),
4579 base_change.RepositoryRoot(),
4580 files,
4581 base_change.issue,
4582 base_change.patchset,
4583 base_change.author_email,
4584 base_change._upstream)
4585 else:
4586 change = cl.GetChange(base_branch, None)
4587
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004588 cl.RunHook(
4589 committing=not options.upload,
4590 may_prompt=False,
4591 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004592 change=change,
4593 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004594 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004595
4596
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004597def GenerateGerritChangeId(message):
4598 """Returns Ixxxxxx...xxx change id.
4599
4600 Works the same way as
4601 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4602 but can be called on demand on all platforms.
4603
4604 The basic idea is to generate git hash of a state of the tree, original commit
4605 message, author/committer info and timestamps.
4606 """
4607 lines = []
4608 tree_hash = RunGitSilent(['write-tree'])
4609 lines.append('tree %s' % tree_hash.strip())
4610 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4611 if code == 0:
4612 lines.append('parent %s' % parent.strip())
4613 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4614 lines.append('author %s' % author.strip())
4615 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4616 lines.append('committer %s' % committer.strip())
4617 lines.append('')
4618 # Note: Gerrit's commit-hook actually cleans message of some lines and
4619 # whitespace. This code is not doing this, but it clearly won't decrease
4620 # entropy.
4621 lines.append(message)
4622 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00004623 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004624 return 'I%s' % change_hash.strip()
4625
4626
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004627def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004628 """Computes the remote branch ref to use for the CL.
4629
4630 Args:
4631 remote (str): The git remote for the CL.
4632 remote_branch (str): The git remote branch for the CL.
4633 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004634 """
4635 if not (remote and remote_branch):
4636 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004637
wittman@chromium.org455dc922015-01-26 20:15:50 +00004638 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004639 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004640 # refs, which are then translated into the remote full symbolic refs
4641 # below.
4642 if '/' not in target_branch:
4643 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4644 else:
4645 prefix_replacements = (
4646 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4647 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4648 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4649 )
4650 match = None
4651 for regex, replacement in prefix_replacements:
4652 match = re.search(regex, target_branch)
4653 if match:
4654 remote_branch = target_branch.replace(match.group(0), replacement)
4655 break
4656 if not match:
4657 # This is a branch path but not one we recognize; use as-is.
4658 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004659 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4660 # Handle the refs that need to land in different refs.
4661 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004662
wittman@chromium.org455dc922015-01-26 20:15:50 +00004663 # Create the true path to the remote branch.
4664 # Does the following translation:
4665 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4666 # * refs/remotes/origin/master -> refs/heads/master
4667 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4668 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4669 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4670 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4671 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4672 'refs/heads/')
4673 elif remote_branch.startswith('refs/remotes/branch-heads'):
4674 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004675
wittman@chromium.org455dc922015-01-26 20:15:50 +00004676 return remote_branch
4677
4678
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004679def cleanup_list(l):
4680 """Fixes a list so that comma separated items are put as individual items.
4681
4682 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4683 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4684 """
4685 items = sum((i.split(',') for i in l), [])
4686 stripped_items = (i.strip() for i in items)
4687 return sorted(filter(None, stripped_items))
4688
4689
Aaron Gable4db38df2017-11-03 14:59:07 -07004690@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004691@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004692def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004693 """Uploads the current changelist to codereview.
4694
4695 Can skip dependency patchset uploads for a branch by running:
4696 git config branch.branch_name.skip-deps-uploads True
4697 To unset run:
4698 git config --unset branch.branch_name.skip-deps-uploads
4699 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004700
4701 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4702 a bug number, this bug number is automatically populated in the CL
4703 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004704
4705 If subject contains text in square brackets or has "<text>: " prefix, such
4706 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4707 [git-cl] add support for hashtags
4708 Foo bar: implement foo
4709 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004710 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004711 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4712 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004713 parser.add_option('--bypass-watchlists', action='store_true',
4714 dest='bypass_watchlists',
4715 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004716 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004717 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004718 parser.add_option('--message', '-m', dest='message',
4719 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004720 parser.add_option('-b', '--bug',
4721 help='pre-populate the bug number(s) for this issue. '
4722 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004723 parser.add_option('--message-file', dest='message_file',
4724 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004725 parser.add_option('--title', '-t', dest='title',
4726 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004727 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004728 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004729 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004730 parser.add_option('--tbrs',
4731 action='append', default=[],
4732 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004733 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004734 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004735 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004736 parser.add_option('--hashtag', dest='hashtags',
4737 action='append', default=[],
4738 help=('Gerrit hashtag for new CL; '
4739 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004740 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004741 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004742 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004743 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004744 metavar='TARGET',
4745 help='Apply CL to remote ref TARGET. ' +
4746 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004747 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004748 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004749 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004750 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004751 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004752 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004753 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4754 const='TBR', help='add a set of OWNERS to TBR')
4755 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4756 const='R', help='add a set of OWNERS to R')
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004757 parser.add_option('-c', '--use-commit-queue', action='store_true',
4758 help='tell the CQ to commit this patchset; '
4759 'implies --send-mail')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004760 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4761 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004762 help='Send the patchset to do a CQ dry run right after '
4763 'upload.')
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00004764 parser.add_option('--preserve-tryjobs', action='store_true',
4765 help='instruct the CQ to let tryjobs running even after '
4766 'new patchsets are uploaded instead of canceling '
4767 'prior patchset\' tryjobs')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004768 parser.add_option('--dependencies', action='store_true',
4769 help='Uploads CLs of all the local branches that depend on '
4770 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004771 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4772 help='Sends your change to the CQ after an approval. Only '
4773 'works on repos that have the Auto-Submit label '
4774 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004775 parser.add_option('--parallel', action='store_true',
4776 help='Run all tests specified by input_api.RunTests in all '
4777 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004778
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004779 parser.add_option('--no-autocc', action='store_true',
4780 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004781 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004782 help='Set the review private. This implies --no-autocc.')
4783
rmistry@google.com2dd99862015-06-22 12:22:18 +00004784 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004785 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004786 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004787 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004788 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004789 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004790
sbc@chromium.org71437c02015-04-09 19:29:40 +00004791 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004792 return 1
4793
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004794 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004795 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004796 options.cc = cleanup_list(options.cc)
4797
tandriib80458a2016-06-23 12:20:07 -07004798 if options.message_file:
4799 if options.message:
4800 parser.error('only one of --message and --message-file allowed.')
4801 options.message = gclient_utils.FileRead(options.message_file)
4802 options.message_file = None
4803
tandrii4d0545a2016-07-06 03:56:49 -07004804 if options.cq_dry_run and options.use_commit_queue:
4805 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4806
Aaron Gableedbc4132017-09-11 13:22:28 -07004807 if options.use_commit_queue:
4808 options.send_mail = True
4809
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004810 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4811 settings.GetIsGerrit()
4812
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004813 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004814 if not cl.IsGerrit():
4815 # Error out with instructions for repos not yet configured for Gerrit.
4816 print('=====================================')
4817 print('NOTICE: Rietveld is no longer supported. '
4818 'You can upload changes to Gerrit with')
4819 print(' git cl upload --gerrit')
4820 print('or set Gerrit to be your default code review tool with')
4821 print(' git config gerrit.host true')
4822 print('=====================================')
4823 return 1
4824
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004825 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004826
4827
Francois Dorayd42c6812017-05-30 15:10:20 -04004828@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004829@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004830def CMDsplit(parser, args):
4831 """Splits a branch into smaller branches and uploads CLs.
4832
4833 Creates a branch and uploads a CL for each group of files modified in the
4834 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004835 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004836 the shared OWNERS file.
4837 """
4838 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05004839 help="A text file containing a CL description in which "
4840 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004841 parser.add_option("-c", "--comment", dest="comment_file",
4842 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11004843 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
4844 default=False,
4845 help="List the files and reviewers for each CL that would "
4846 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00004847 parser.add_option("--cq-dry-run", action='store_true',
4848 help="If set, will do a cq dry run for each uploaded CL. "
4849 "Please be careful when doing this; more than ~10 CLs "
4850 "has the potential to overload our build "
4851 "infrastructure. Try to upload these not during high "
4852 "load times (usually 11-3 Mountain View time). Email "
4853 "infra-dev@chromium.org with any questions.")
Takuto Ikuta51eca592019-02-14 19:40:52 +00004854 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4855 default=True,
4856 help='Sends your change to the CQ after an approval. Only '
4857 'works on repos that have the Auto-Submit label '
4858 'enabled')
Francois Dorayd42c6812017-05-30 15:10:20 -04004859 options, _ = parser.parse_args(args)
4860
4861 if not options.description_file:
4862 parser.error('No --description flag specified.')
4863
4864 def WrappedCMDupload(args):
4865 return CMDupload(OptionParser(), args)
4866
4867 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004868 Changelist, WrappedCMDupload, options.dry_run,
Takuto Ikuta51eca592019-02-14 19:40:52 +00004869 options.cq_dry_run, options.enable_auto_submit)
Francois Dorayd42c6812017-05-30 15:10:20 -04004870
4871
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004872@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004873@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004874def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004875 """DEPRECATED: Used to commit the current changelist via git-svn."""
4876 message = ('git-cl no longer supports committing to SVN repositories via '
4877 'git-svn. You probably want to use `git cl land` instead.')
4878 print(message)
4879 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004880
4881
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004882# Two special branches used by git cl land.
4883MERGE_BRANCH = 'git-cl-commit'
4884CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4885
4886
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004887@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004888@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004889def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004890 """Commits the current changelist via git.
4891
4892 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4893 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004894 """
4895 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4896 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004897 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004898 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004899 parser.add_option('--parallel', action='store_true',
4900 help='Run all tests specified by input_api.RunTests in all '
4901 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004902 auth.add_auth_options(parser)
4903 (options, args) = parser.parse_args(args)
4904 auth_config = auth.extract_auth_config_from_options(options)
4905
4906 cl = Changelist(auth_config=auth_config)
4907
Robert Iannucci2e73d432018-03-14 01:10:47 -07004908 if not cl.IsGerrit():
4909 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004910
Robert Iannucci2e73d432018-03-14 01:10:47 -07004911 if not cl.GetIssue():
4912 DieWithError('You must upload the change first to Gerrit.\n'
4913 ' If you would rather have `git cl land` upload '
4914 'automatically for you, see http://crbug.com/642759')
4915 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004916 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004917
4918
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004919@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004920@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004921def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004922 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004923 parser.add_option('-b', dest='newbranch',
4924 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004925 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004926 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004927 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07004928 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004929 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004930 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004931 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004932 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004933 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004934 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004935
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004936
4937 group = optparse.OptionGroup(
4938 parser,
4939 'Options for continuing work on the current issue uploaded from a '
4940 'different clone (e.g. different machine). Must be used independently '
4941 'from the other options. No issue number should be specified, and the '
4942 'branch must have an issue number associated with it')
4943 group.add_option('--reapply', action='store_true', dest='reapply',
4944 help='Reset the branch and reapply the issue.\n'
4945 'CAUTION: This will undo any local changes in this '
4946 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004947
4948 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004949 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004950 parser.add_option_group(group)
4951
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004952 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004953 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004954 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004955 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004956 auth_config = auth.extract_auth_config_from_options(options)
4957
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004958 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004959 if options.newbranch:
4960 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004961 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004962 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004963
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004964 cl = Changelist(auth_config=auth_config,
4965 codereview=options.forced_codereview)
4966 if not cl.GetIssue():
4967 parser.error('current branch must have an associated issue')
4968
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004969 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004970 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004971 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004972
4973 RunGit(['reset', '--hard', upstream])
4974 if options.pull:
4975 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004976
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004977 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4978 options.directory)
4979
4980 if len(args) != 1 or not args[0]:
4981 parser.error('Must specify issue number or url')
4982
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004983 target_issue_arg = ParseIssueNumberArgument(args[0],
4984 options.forced_codereview)
4985 if not target_issue_arg.valid:
4986 parser.error('invalid codereview url or CL id')
4987
4988 cl_kwargs = {
4989 'auth_config': auth_config,
4990 'codereview_host': target_issue_arg.hostname,
4991 'codereview': options.forced_codereview,
4992 }
4993 detected_codereview_from_url = False
4994 if target_issue_arg.codereview and not options.forced_codereview:
4995 detected_codereview_from_url = True
4996 cl_kwargs['codereview'] = target_issue_arg.codereview
4997 cl_kwargs['issue'] = target_issue_arg.issue
4998
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004999 # We don't want uncommitted changes mixed up with the patch.
5000 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005001 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005002
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005003 if options.newbranch:
5004 if options.force:
5005 RunGit(['branch', '-D', options.newbranch],
5006 stderr=subprocess2.PIPE, error_ok=True)
5007 RunGit(['new-branch', options.newbranch])
5008
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005009 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005010
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005011 if cl.IsGerrit():
5012 if options.reject:
5013 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005014 if options.directory:
5015 parser.error('--directory is not supported with Gerrit codereview.')
5016
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005017 if detected_codereview_from_url:
5018 print('canonical issue/change URL: %s (type: %s)\n' %
5019 (cl.GetIssueURL(), target_issue_arg.codereview))
5020
5021 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005022 options.nocommit, options.directory,
5023 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005024
5025
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005026def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005027 """Fetches the tree status and returns either 'open', 'closed',
5028 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005029 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005030 if url:
5031 status = urllib2.urlopen(url).read().lower()
5032 if status.find('closed') != -1 or status == '0':
5033 return 'closed'
5034 elif status.find('open') != -1 or status == '1':
5035 return 'open'
5036 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005037 return 'unset'
5038
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005039
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005040def GetTreeStatusReason():
5041 """Fetches the tree status from a json url and returns the message
5042 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005043 url = settings.GetTreeStatusUrl()
5044 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005045 connection = urllib2.urlopen(json_url)
5046 status = json.loads(connection.read())
5047 connection.close()
5048 return status['message']
5049
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005050
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005051@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005052def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005053 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005054 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005055 status = GetTreeStatus()
5056 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005057 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005058 return 2
5059
vapiera7fbd5a2016-06-16 09:17:49 -07005060 print('The tree is %s' % status)
5061 print()
5062 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005063 if status != 'open':
5064 return 1
5065 return 0
5066
5067
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005068@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005069def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005070 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005071 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005072 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005073 '-b', '--bot', action='append',
5074 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5075 'times to specify multiple builders. ex: '
5076 '"-b win_rel -b win_layout". See '
5077 'the try server waterfall for the builders name and the tests '
5078 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005079 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005080 '-B', '--bucket', default='',
5081 help=('Buildbucket bucket to send the try requests.'))
5082 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005083 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005084 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005085 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005086 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005087 help='Revision to use for the try job; default: the revision will '
5088 'be determined by the try recipe that builder runs, which usually '
5089 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005090 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005091 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005092 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005093 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005094 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005095 '--category', default='git_cl_try', help='Specify custom build category.')
5096 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005097 '--project',
5098 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005099 'in recipe to determine to which repository or directory to '
5100 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005101 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005102 '-p', '--property', dest='properties', action='append', default=[],
5103 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005104 'key2=value2 etc. The value will be treated as '
5105 'json if decodable, or as string otherwise. '
5106 'NOTE: using this may make your try job not usable for CQ, '
5107 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005108 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005109 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5110 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005111 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005112 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005113 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005114 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005115 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005116 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005117
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005118 if options.master and options.master.startswith('luci.'):
5119 parser.error(
5120 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005121 # Make sure that all properties are prop=value pairs.
5122 bad_params = [x for x in options.properties if '=' not in x]
5123 if bad_params:
5124 parser.error('Got properties with missing "=": %s' % bad_params)
5125
maruel@chromium.org15192402012-09-06 12:38:29 +00005126 if args:
5127 parser.error('Unknown arguments: %s' % args)
5128
Koji Ishii31c14782018-01-08 17:17:33 +09005129 cl = Changelist(auth_config=auth_config, issue=options.issue,
5130 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005131 if not cl.GetIssue():
5132 parser.error('Need to upload first')
5133
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005134 if cl.IsGerrit():
5135 # HACK: warm up Gerrit change detail cache to save on RPCs.
5136 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5137
tandriie113dfd2016-10-11 10:20:12 -07005138 error_message = cl.CannotTriggerTryJobReason()
5139 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005140 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005141
borenet6c0efe62016-10-19 08:13:29 -07005142 if options.bucket and options.master:
5143 parser.error('Only one of --bucket and --master may be used.')
5144
qyearsley1fdfcb62016-10-24 13:22:03 -07005145 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005146
qyearsleydd49f942016-10-28 11:57:22 -07005147 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5148 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005149 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005150 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005151 print('git cl try with no bots now defaults to CQ dry run.')
5152 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5153 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005154
borenet6c0efe62016-10-19 08:13:29 -07005155 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005156 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005157 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005158 'of bot requires an initial job from a parent (usually a builder). '
5159 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005160 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005161 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005162
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005163 patchset = cl.GetMostRecentPatchset()
tandrii568043b2016-10-11 07:49:18 -07005164 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005165 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005166 except BuildbucketResponseException as ex:
5167 print('ERROR: %s' % ex)
5168 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005169 return 0
5170
5171
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005172@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005173def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005174 """Prints info about try jobs associated with current CL."""
5175 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005176 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005177 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005178 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005179 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005180 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005181 '--color', action='store_true', default=setup_color.IS_TTY,
5182 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005183 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005184 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5185 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005186 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005187 '--json', help=('Path of JSON output file to write try job results to,'
5188 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005189 parser.add_option_group(group)
5190 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005191 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005192 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005193 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005194 if args:
5195 parser.error('Unrecognized args: %s' % ' '.join(args))
5196
5197 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005198 cl = Changelist(
5199 issue=options.issue, codereview=options.forced_codereview,
5200 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005201 if not cl.GetIssue():
5202 parser.error('Need to upload first')
5203
tandrii221ab252016-10-06 08:12:04 -07005204 patchset = options.patchset
5205 if not patchset:
5206 patchset = cl.GetMostRecentPatchset()
5207 if not patchset:
5208 parser.error('Codereview doesn\'t know about issue %s. '
5209 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005210 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005211 cl.GetIssue())
5212
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005213 try:
tandrii221ab252016-10-06 08:12:04 -07005214 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005215 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005216 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005217 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005218 if options.json:
5219 write_try_results_json(options.json, jobs)
5220 else:
5221 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005222 return 0
5223
5224
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005225@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005226@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005227def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005228 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005229 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005230 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005231 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005232
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005233 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005234 if args:
5235 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005236 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005237 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005238 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005239 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005240
5241 # Clear configured merge-base, if there is one.
5242 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005243 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005244 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005245 return 0
5246
5247
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005248@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005249def CMDweb(parser, args):
5250 """Opens the current CL in the web browser."""
5251 _, args = parser.parse_args(args)
5252 if args:
5253 parser.error('Unrecognized args: %s' % ' '.join(args))
5254
5255 issue_url = Changelist().GetIssueURL()
5256 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005257 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005258 return 1
5259
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005260 # Redirect I/O before invoking browser to hide its output. For example, this
5261 # allows to hide "Created new window in existing browser session." message
5262 # from Chrome. Based on https://stackoverflow.com/a/2323563.
5263 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005264 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005265 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005266 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005267 os.open(os.devnull, os.O_RDWR)
5268 try:
5269 webbrowser.open(issue_url)
5270 finally:
5271 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005272 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005273 return 0
5274
5275
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005276@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005277def CMDset_commit(parser, args):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00005278 """Sets the commit bit to trigger the CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005279 parser.add_option('-d', '--dry-run', action='store_true',
5280 help='trigger in dry run mode')
5281 parser.add_option('-c', '--clear', action='store_true',
5282 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005283 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005284 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005285 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005286 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005287 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005288 if args:
5289 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005290 if options.dry_run and options.clear:
5291 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5292
iannuccie53c9352016-08-17 14:40:40 -07005293 cl = Changelist(auth_config=auth_config, issue=options.issue,
5294 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005295 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005296 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005297 elif options.dry_run:
5298 state = _CQState.DRY_RUN
5299 else:
5300 state = _CQState.COMMIT
5301 if not cl.GetIssue():
5302 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005303 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005304 return 0
5305
5306
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005307@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005308def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005309 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005310 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005311 auth.add_auth_options(parser)
5312 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005313 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005314 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005315 if args:
5316 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005317 cl = Changelist(auth_config=auth_config, issue=options.issue,
5318 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005319 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005320 if not cl.GetIssue():
5321 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005322 cl.CloseIssue()
5323 return 0
5324
5325
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005326@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005327def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005328 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005329 parser.add_option(
5330 '--stat',
5331 action='store_true',
5332 dest='stat',
5333 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005334 auth.add_auth_options(parser)
5335 options, args = parser.parse_args(args)
5336 auth_config = auth.extract_auth_config_from_options(options)
5337 if args:
5338 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005339
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005340 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005341 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005342 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005343 if not issue:
5344 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005345
Aaron Gablea718c3e2017-08-28 17:47:28 -07005346 base = cl._GitGetBranchConfigValue('last-upload-hash')
5347 if not base:
5348 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5349 if not base:
5350 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5351 revision_info = detail['revisions'][detail['current_revision']]
5352 fetch_info = revision_info['fetch']['http']
5353 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5354 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005355
Aaron Gablea718c3e2017-08-28 17:47:28 -07005356 cmd = ['git', 'diff']
5357 if options.stat:
5358 cmd.append('--stat')
5359 cmd.append(base)
5360 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005361
5362 return 0
5363
5364
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005365@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005366def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005367 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005368 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005369 '--ignore-current',
5370 action='store_true',
5371 help='Ignore the CL\'s current reviewers and start from scratch.')
5372 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005373 '--ignore-self',
5374 action='store_true',
5375 help='Do not consider CL\'s author as an owners.')
5376 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005377 '--no-color',
5378 action='store_true',
5379 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005380 parser.add_option(
5381 '--batch',
5382 action='store_true',
5383 help='Do not run interactively, just suggest some')
Yang Guo6e269a02019-06-26 11:17:02 +00005384 # TODO: Consider moving this to another command, since other
5385 # git-cl owners commands deal with owners for a given CL.
5386 parser.add_option(
5387 '--show-all',
5388 action='store_true',
5389 help='Show all owners for a particular file')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005390 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005391 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005392 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005393
5394 author = RunGit(['config', 'user.email']).strip() or None
5395
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005396 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005397
Yang Guo6e269a02019-06-26 11:17:02 +00005398 if options.show_all:
5399 for arg in args:
5400 base_branch = cl.GetCommonAncestorWithUpstream()
5401 change = cl.GetChange(base_branch, None)
5402 database = owners.Database(change.RepositoryRoot(), file, os.path)
5403 database.load_data_needed_for([arg])
5404 print('Owners for %s:' % arg)
5405 for owner in sorted(database.all_possible_owners([arg], None)):
5406 print(' - %s' % owner)
5407 return 0
5408
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005409 if args:
5410 if len(args) > 1:
5411 parser.error('Unknown args')
5412 base_branch = args[0]
5413 else:
5414 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005415 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005416
5417 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005418 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5419
5420 if options.batch:
5421 db = owners.Database(change.RepositoryRoot(), file, os.path)
5422 print('\n'.join(db.reviewers_for(affected_files, author)))
5423 return 0
5424
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005425 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005426 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005427 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005428 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005429 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005430 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005431 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005432 override_files=change.OriginalOwnersFiles(),
5433 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005434
5435
Aiden Bennerc08566e2018-10-03 17:52:42 +00005436def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005437 """Generates a diff command."""
5438 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005439 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5440
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005441 if allow_prefix:
5442 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5443 # case that diff.noprefix is set in the user's git config.
5444 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5445 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005446 diff_cmd += ['--no-prefix']
5447
5448 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005449
5450 if args:
5451 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005452 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005453 diff_cmd.append(arg)
5454 else:
5455 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005456
5457 return diff_cmd
5458
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005459
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005460def MatchingFileType(file_name, extensions):
5461 """Returns true if the file name ends with one of the given extensions."""
5462 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005463
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005464
enne@chromium.org555cfe42014-01-29 18:21:39 +00005465@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005466@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005467def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005468 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005469 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005470 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005471 parser.add_option('--full', action='store_true',
5472 help='Reformat the full content of all touched files')
5473 parser.add_option('--dry-run', action='store_true',
5474 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005475 parser.add_option(
5476 '--python',
5477 action='store_true',
5478 default=None,
5479 help='Enables python formatting on all python files.')
5480 parser.add_option(
5481 '--no-python',
5482 action='store_true',
5483 dest='python',
5484 help='Disables python formatting on all python files. '
5485 'Takes precedence over --python. '
5486 'If neither --python or --no-python are set, python '
5487 'files that have a .style.yapf file in an ancestor '
5488 'directory will be formatted.')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005489 parser.add_option('--js', action='store_true',
5490 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005491 parser.add_option('--diff', action='store_true',
5492 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005493 parser.add_option('--presubmit', action='store_true',
5494 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005495 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005496
Daniel Chengc55eecf2016-12-30 03:11:02 -08005497 # Normalize any remaining args against the current path, so paths relative to
5498 # the current directory are still resolved as expected.
5499 args = [os.path.join(os.getcwd(), arg) for arg in args]
5500
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005501 # git diff generates paths against the root of the repository. Change
5502 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005503 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005504 if rel_base_path:
5505 os.chdir(rel_base_path)
5506
digit@chromium.org29e47272013-05-17 17:01:46 +00005507 # Grab the merge-base commit, i.e. the upstream commit of the current
5508 # branch when it was created or the last time it was rebased. This is
5509 # to cover the case where the user may have called "git fetch origin",
5510 # moving the origin branch to a newer commit, but hasn't rebased yet.
5511 upstream_commit = None
5512 cl = Changelist()
5513 upstream_branch = cl.GetUpstreamBranch()
5514 if upstream_branch:
5515 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5516 upstream_commit = upstream_commit.strip()
5517
5518 if not upstream_commit:
5519 DieWithError('Could not find base commit for this branch. '
5520 'Are you in detached state?')
5521
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005522 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5523 diff_output = RunGit(changed_files_cmd)
5524 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005525 # Filter out files deleted by this CL
5526 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005527
Christopher Lamc5ba6922017-01-24 11:19:14 +11005528 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005529 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005530
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005531 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5532 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5533 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005534 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005535
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005536 top_dir = os.path.normpath(
5537 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5538
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005539 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5540 # formatted. This is used to block during the presubmit.
5541 return_value = 0
5542
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005543 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005544 # Locate the clang-format binary in the checkout
5545 try:
5546 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005547 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005548 DieWithError(e)
5549
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005550 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005551 cmd = [clang_format_tool]
5552 if not opts.dry_run and not opts.diff:
5553 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005554 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005555 if opts.diff:
5556 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005557 else:
5558 env = os.environ.copy()
5559 env['PATH'] = str(os.path.dirname(clang_format_tool))
5560 try:
5561 script = clang_format.FindClangFormatScriptInChromiumTree(
5562 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005563 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005564 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005565
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005566 cmd = [sys.executable, script, '-p0']
5567 if not opts.dry_run and not opts.diff:
5568 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005569
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005570 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5571 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005572
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005573 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5574 if opts.diff:
5575 sys.stdout.write(stdout)
5576 if opts.dry_run and len(stdout) > 0:
5577 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005578
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005579 # Similar code to above, but using yapf on .py files rather than clang-format
5580 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005581 py_explicitly_disabled = opts.python is not None and not opts.python
5582 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005583 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5584 yapf_tool = os.path.join(depot_tools_path, 'yapf')
5585 if sys.platform.startswith('win'):
5586 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005587
Aiden Bennerc08566e2018-10-03 17:52:42 +00005588 # If we couldn't find a yapf file we'll default to the chromium style
5589 # specified in depot_tools.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005590 chromium_default_yapf_style = os.path.join(depot_tools_path,
5591 YAPF_CONFIG_FILENAME)
Aiden Bennerc08566e2018-10-03 17:52:42 +00005592 # Used for caching.
5593 yapf_configs = {}
5594 for f in python_diff_files:
5595 # Find the yapf style config for the current file, defaults to depot
5596 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005597 _FindYapfConfigFile(f, yapf_configs, top_dir)
5598
5599 # Turn on python formatting by default if a yapf config is specified.
5600 # This breaks in the case of this repo though since the specified
5601 # style file is also the global default.
5602 if opts.python is None:
5603 filtered_py_files = []
5604 for f in python_diff_files:
5605 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5606 filtered_py_files.append(f)
5607 else:
5608 filtered_py_files = python_diff_files
5609
5610 # Note: yapf still seems to fix indentation of the entire file
5611 # even if line ranges are specified.
5612 # See https://github.com/google/yapf/issues/499
5613 if not opts.full and filtered_py_files:
5614 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5615
5616 for f in filtered_py_files:
5617 yapf_config = _FindYapfConfigFile(f, yapf_configs, top_dir)
5618 if yapf_config is None:
5619 yapf_config = chromium_default_yapf_style
Aiden Bennerc08566e2018-10-03 17:52:42 +00005620
5621 cmd = [yapf_tool, '--style', yapf_config, f]
5622
5623 has_formattable_lines = False
5624 if not opts.full:
5625 # Only run yapf over changed line ranges.
5626 for diff_start, diff_len in py_line_diffs[f]:
5627 diff_end = diff_start + diff_len - 1
5628 # Yapf errors out if diff_end < diff_start but this
5629 # is a valid line range diff for a removal.
5630 if diff_end >= diff_start:
5631 has_formattable_lines = True
5632 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5633 # If all line diffs were removals we have nothing to format.
5634 if not has_formattable_lines:
5635 continue
5636
5637 if opts.diff or opts.dry_run:
5638 cmd += ['--diff']
5639 # Will return non-zero exit code if non-empty diff.
5640 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5641 if opts.diff:
5642 sys.stdout.write(stdout)
5643 elif len(stdout) > 0:
5644 return_value = 2
5645 else:
5646 cmd += ['-i']
5647 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005648
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005649 # Dart's formatter does not have the nice property of only operating on
5650 # modified chunks, so hard code full.
5651 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005652 try:
5653 command = [dart_format.FindDartFmtToolInChromiumTree()]
5654 if not opts.dry_run and not opts.diff:
5655 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005656 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005657
ppi@chromium.org6593d932016-03-03 15:41:15 +00005658 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005659 if opts.dry_run and stdout:
5660 return_value = 2
5661 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005662 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5663 'found in this checkout. Files in other languages are still '
5664 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005665
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005666 # Format GN build files. Always run on full build files for canonical form.
5667 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005668 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005669 if opts.dry_run or opts.diff:
5670 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005671 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005672 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5673 shell=sys.platform == 'win32',
5674 cwd=top_dir)
5675 if opts.dry_run and gn_ret == 2:
5676 return_value = 2 # Not formatted.
5677 elif opts.diff and gn_ret == 2:
5678 # TODO this should compute and print the actual diff.
5679 print("This change has GN build file diff for " + gn_diff_file)
5680 elif gn_ret != 0:
5681 # For non-dry run cases (and non-2 return values for dry-run), a
5682 # nonzero error code indicates a failure, probably because the file
5683 # doesn't parse.
5684 DieWithError("gn format failed on " + gn_diff_file +
5685 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005686
Ilya Shermane081cbe2017-08-15 17:51:04 -07005687 # Skip the metrics formatting from the global presubmit hook. These files have
5688 # a separate presubmit hook that issues an error if the files need formatting,
5689 # whereas the top-level presubmit script merely issues a warning. Formatting
5690 # these files is somewhat slow, so it's important not to duplicate the work.
5691 if not opts.presubmit:
5692 for xml_dir in GetDirtyMetricsDirs(diff_files):
5693 tool_dir = os.path.join(top_dir, xml_dir)
5694 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5695 if opts.dry_run or opts.diff:
5696 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005697 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005698 if opts.diff:
5699 sys.stdout.write(stdout)
5700 if opts.dry_run and stdout:
5701 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005702
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005703 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005704
Steven Holte2e664bf2017-04-21 13:10:47 -07005705def GetDirtyMetricsDirs(diff_files):
5706 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5707 metrics_xml_dirs = [
5708 os.path.join('tools', 'metrics', 'actions'),
5709 os.path.join('tools', 'metrics', 'histograms'),
5710 os.path.join('tools', 'metrics', 'rappor'),
5711 os.path.join('tools', 'metrics', 'ukm')]
5712 for xml_dir in metrics_xml_dirs:
5713 if any(file.startswith(xml_dir) for file in xml_diff_files):
5714 yield xml_dir
5715
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005716
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005717@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005718@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005719def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005720 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005721 _, args = parser.parse_args(args)
5722
5723 if len(args) != 1:
5724 parser.print_help()
5725 return 1
5726
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005727 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005728 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005729 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005730
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005731 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005732
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005733 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005734 output = RunGit(['config', '--local', '--get-regexp',
5735 r'branch\..*\.%s' % issueprefix],
5736 error_ok=True)
5737 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005738 if issue == target_issue:
5739 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005740
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005741 branches = []
5742 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005743 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005744 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005745 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005746 return 1
5747 if len(branches) == 1:
5748 RunGit(['checkout', branches[0]])
5749 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005750 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005751 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005752 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005753 which = raw_input('Choose by index: ')
5754 try:
5755 RunGit(['checkout', branches[int(which)]])
5756 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005757 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005758 return 1
5759
5760 return 0
5761
5762
maruel@chromium.org29404b52014-09-08 22:58:00 +00005763def CMDlol(parser, args):
5764 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005765 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005766 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5767 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5768 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005769 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005770 return 0
5771
5772
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005773class OptionParser(optparse.OptionParser):
5774 """Creates the option parse and add --verbose support."""
5775 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005776 optparse.OptionParser.__init__(
5777 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005778 self.add_option(
5779 '-v', '--verbose', action='count', default=0,
5780 help='Use 2 times for more debugging info')
5781
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005782 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005783 try:
5784 return self._parse_args(args)
5785 finally:
5786 # Regardless of success or failure of args parsing, we want to report
5787 # metrics, but only after logging has been initialized (if parsing
5788 # succeeded).
5789 global settings
5790 settings = Settings()
5791
5792 if not metrics.DISABLE_METRICS_COLLECTION:
5793 # GetViewVCUrl ultimately calls logging method.
5794 project_url = settings.GetViewVCUrl().strip('/+')
5795 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5796 metrics.collector.add('project_urls', [project_url])
5797
5798 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005799 # Create an optparse.Values object that will store only the actual passed
5800 # options, without the defaults.
5801 actual_options = optparse.Values()
5802 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5803 # Create an optparse.Values object with the default options.
5804 options = optparse.Values(self.get_default_values().__dict__)
5805 # Update it with the options passed by the user.
5806 options._update_careful(actual_options.__dict__)
5807 # Store the options passed by the user in an _actual_options attribute.
5808 # We store only the keys, and not the values, since the values can contain
5809 # arbitrary information, which might be PII.
5810 metrics.collector.add('arguments', actual_options.__dict__.keys())
5811
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005812 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005813 logging.basicConfig(
5814 level=levels[min(options.verbose, len(levels) - 1)],
5815 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5816 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005817
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005818 return options, args
5819
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005820
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005821def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005822 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005823 print('\nYour python version %s is unsupported, please upgrade.\n' %
5824 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005825 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005826
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005827 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005828 dispatcher = subcommand.CommandDispatcher(__name__)
5829 try:
5830 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005831 except auth.AuthenticationError as e:
5832 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005833 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005834 if e.code != 500:
5835 raise
5836 DieWithError(
5837 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5838 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005839 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005840
5841
5842if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005843 # These affect sys.stdout so do it outside of main() to simplify mocks in
5844 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005845 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005846 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005847 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005848 sys.exit(main(sys.argv[1:]))